1"use strict";(globalThis.webpackChunk_lumenize_website=globalThis.webpackChunk_lumenize_website||[]).push([[3470],{70849(e,t,n){n.r(t),n.d(t,{assets:()=>a,contentTitle:()=>c,default:()=>h,frontMatter:()=>i,metadata:()=>s,toc:()=>l});const s=JSON.parse('{"id":"testing/usage","title":"Usage","description":"\ud83d\udcd8 Doc-testing \u2013 Why do these examples look like tests?","source":"@site/docs/testing/usage.mdx","sourceDirName":"testing","slug":"/testing/usage","permalink":"/docs/testing/usage","draft":false,"unlisted":false,"editUrl":"https://github.com/lumenize/lumenize/tree/main/website/docs/testing/usage.mdx","tags":[],"version":"current","frontMatter":{"generated_by":"doc-testing"},"sidebar":"docsSidebar","previous":{"title":"Using Resend instead","permalink":"/docs/auth/using-resend-instead"},"next":{"title":"Agents","permalink":"/docs/testing/agents"}}');var o=n(62540),r=n(43023);const i={generated_by:"doc-testing"},c="Usage",a={},l=[{value:"Imports",id:"imports",level:2},{value:"Version(s)",id:"versions",level:2},{value:"Basic Usage",id:"basic-usage",level:2},{value:"Installation",id:"installation",level:2},{value:"src/index.ts",id:"srcindexts",level:2},{value:"test/test-harness.ts",id:"testtest-harnessts",level:2},{value:"test/wrangler.jsonc",id:"testwranglerjsonc",level:2},{value:"vitest.config.js",id:"vitestconfigjs",level:2},{value:"Your tests",id:"your-tests",level:2},{value:"WebSocket",id:"websocket",level:2},{value:"StructuredClone types",id:"structuredclone-types",level:2},{value:"Cookies",id:"cookies",level:2},{value:"Simulate browser context Origin behavior",id:"simulate-browser-context-origin-behavior",level:2},{value:"CORS preflight OPTIONS",id:"cors-preflight-options",level:2},{value:"Discover all public members of DO",id:"discover-all-public-members-of-do",level:2},{value:"Quirks",id:"quirks",level:2},{value:"Try it out",id:"try-it-out",level:2}];function d(e){const t={code:"code",h1:"h1",h2:"h2",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,r.R)(),...e.components},{Details:n}=t;return n||function(e,t){throw new Error("Expected "+(t?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.header,{children:(0,o.jsx)(t.h1,{id:"usage",children:"Usage"})}),"\n",(0,o.jsxs)(n,{children:[(0,o.jsxs)("summary",{children:[(0,o.jsx)("strong",{children:"\ud83d\udcd8 Doc-testing"})," \u2013 Why do these examples look like tests?"]}),(0,o.jsxs)(t.p,{children:["This documentation uses ",(0,o.jsx)(t.strong,{children:"testable code examples"})," to ensure accuracy and reliability:"]}),(0,o.jsxs)(t.ul,{children:["\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.strong,{children:"Guaranteed accuracy"}),": All examples are real, working code that runs against the actual package(s)"]}),"\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.strong,{children:"Guaranteed latest comparisons"}),": Further, our release script won't allow us to release a new\nversion of Lumenize, without prompting us to update any doc-tested comparison package\n(e.g. Cap'n Web)"]}),"\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.strong,{children:"Always up-to-date"}),": When a package changes, the tests fail and the docs must be updated"]}),"\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.strong,{children:"Copy-paste confidence"}),": What you see is what works - no outdated or broken examples"]}),"\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.strong,{children:"Real-world patterns"}),": Tests show complete, runnable scenarios, not just snippets"]}),"\n"]}),(0,o.jsxs)(t.p,{children:["Ignore the test boilerplate (",(0,o.jsx)(t.code,{children:"it()"}),", ",(0,o.jsx)(t.code,{children:"describe()"}),", etc.) - focus on the code inside."]})]}),"\n",(0,o.jsxs)(t.p,{children:[(0,o.jsx)(t.code,{children:"@lumenize/testing"})," is a superset of functionality of ",(0,o.jsx)(t.code,{children:"cloudflare:test"})," with a\nmore de\u2728light\u2728ful DX. While ",(0,o.jsx)(t.code,{children:"cloudflare:test"}),"'s ",(0,o.jsx)(t.code,{children:"runInDurableObject"}),"\nallows you to work with ",(0,o.jsx)(t.code,{children:"ctx"}),"/",(0,o.jsx)(t.code,{children:"state"}),", ",(0,o.jsx)(t.code,{children:"@lumenize/testing"})," also allows you to\ndo that plus:"]}),"\n",(0,o.jsxs)(t.ul,{children:["\n",(0,o.jsx)(t.li,{children:"Inspect or manipulate instance variables"}),"\n",(0,o.jsx)(t.li,{children:"Call instance methods directly from your test"}),"\n",(0,o.jsx)(t.li,{children:"Greatly enhances your ability to test DOs via WebSockets"}),"\n",(0,o.jsx)(t.li,{children:"Simulate browser behavior with cookie management and realistic CORS\nsimulation"}),"\n",(0,o.jsxs)(t.li,{children:["Honors input/output gates (",(0,o.jsx)(t.code,{children:"runInDurableObject"})," does not) to test for race\nconditions"]}),"\n",(0,o.jsx)(t.li,{children:"Does all of the above with a fraction of the boilerplate"}),"\n"]}),"\n",(0,o.jsxs)(t.p,{children:[(0,o.jsx)(t.code,{children:"@lumenize/testing"})," provides:"]}),"\n",(0,o.jsxs)(t.ul,{children:["\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.code,{children:"createTesting
1Client"}),": An RPC client that allows you to alter and inspect\nDO state (",(0,o.jsx)(t.code,{children:"ctx"}),"..., custom methods/properties, etc.)"]}),"\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.code,{children:"Browser"}),": Simulates browser behavior for testing","\n",(0,o.jsxs)(t.ul,{children:["\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.code,{children:"browser.fetch"})," --\x3e cookie-aware fetch (no Origin header)"]}),"\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.code,{children:"browser.WebSocket"})," --\x3e cookie-aware WebSocket constructor (no Origin\nheader)"]}),"\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.code,{children:"browser.context(origin)"})," --\x3e returns ",(0,o.jsx)(t.code,{children:"{ fetch, WebSocket }"}),"\n",(0,o.jsxs)(t.ul,{children:["\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.code,{children:"fetch"})," and ",(0,o.jsx)(t.code,{children:"WebSocket"})," from same context share cookies"]}),"\n",(0,o.jsx)(t.li,{children:"Simulates requests from a context/page loaded from the given origin"}),"\n",(0,o.jsx)(t.li,{children:"Perfect for testing CORS and Origin validation logic"}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,o.jsx)(t.h2,{id:"imports",children:"Imports"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",metastring:"test",children:"import { it, expect, vi } from 'vitest';\nimport { createTestingClient, Browser } from '@lumenize/testing';\nimport { MyDO } from '../src';\n"})}),"\n",(0,o.jsx)(t.h2,{id:"versions",children:"Version(s)"}),"\n",(0,o.jsx)(t.p,{children:"This test asserts the installed version(s) and our release script warns if we\naren't using the latest version published to npm, so this living documentation\nshould always be up to date."}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",metastring:"test",children:"import lumenizeTestingPackage from '../../../../packages/testing/package.json';\nit('detects package version', () => {\n expect(lumenizeTestingPackage.version).toBe('0.24.0');\n});\n"})}),"\n",(0,o.jsx)(t.h2,{id:"basic-usage",children:"Basic Usage"}),"\n",(0,o.jsx)(t.p,{children:"Now, let's show basic usage following the basic pattern for all tests:"}),"\n",(0,o.jsxs)(t.ol,{children:["\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.strong,{children:"Setup test"}),". initialize testing client, test variables, etc."]}),"\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.strong,{children:"Setup state"}),". storage, instance variables, etc."]}),"\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.strong,{children:"Interact as a user/caller would"}),". call ",(0,o.jsx)(t.code,{children:"fetch"}),", custom methods, etc."]}),"\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.strong,{children:"Assert on output"}),". check responses"]}),"\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.strong,{children:"Assert state"}),". check that storage and instance variables are as expected"]}),"\n"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",metastring:"test",children:"it('shows basic 5-step test', async () => {\n // 1. Create RPC testing client and Browser instance\n using client = createTestingClient<typeof MyDO>('MY_DO', '5-step');\n const browser = new Browser();\n\n // 2. Pre-populate storage via RPC to call async KV API\n await client.ctx.storage.put('count', 10);\n\n // 3. Make a fetch and RPC call to increment\n const resp = await browser.fetch('https://test.com/my-do/5-step/increment');\n const rpcResult = await client.increment();\n\n // 4. Confirm that results are as expected\n expect(await resp.text()).toBe('11');\n expect(rpcResult).toBe(12); // Notice this is a number not a string\n\n // 5. Verify that storage is correct via RPC\n expect(await client.ctx.storage.kv.get('count')).toBe(12);\n});\n"})}),"\n",(0,o.jsxs)(t.p,{children:["Next, we'll walk through a series of more advanced scenarios, but first let's\nshow you how to configure your system to use ",(0,o.jsx)(t.code,{children:"@lumenize/testing"}),"."]}),"\n",(0,o.jsx)(t.h2,{id:"installation",children:"Installation"}),"\n",(0,o.jsx)(t.p,{children:"First let's install some tools"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-bash",metastring:"npm2yarn",children:"npm install --save-dev [email protected]\nnpm install --save-dev @vitest/[email protected]\nnpm install --save-dev @cloudflare/vitest-pool-workers\nnpm install --save-dev @lumenize/testing\nnpm install --save-dev @lumenize/routing\n"})}),"\n",(0,o.jsx)(t.h2,{id:"srcindexts",children:"src/index.ts"}),"\n",(0,o.jsx)(t.p,{children:"Let's say you have this Worker and Durable Object:"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",metastring:"src/index.ts",children:"import { DurableObject } from \"cloudflare:workers\";\nimport { routeDORequest } from '@lumenize/routing';\n\nconst handleLogin = (request: Request): Response | undefined => {\n const url = new URL(request.url);\n if (!url.pathname.endsWith('/login')) return undefined;\n \n const user = url.searchParams.get('user');\n if (user === 'test') {\n return new Response('OK', {\n headers: { 'Set-Cookie': 'token=abc123; Path=/' }\n });\n }\n return new Response('Invalid', { status: 401 });\n};\n\nconst handleProtectedCookieEcho = (request: Request): Response | undefined => {\n const url = new URL(request.url);\n if (!url.pathname.endsWith('/protected-cookie-echo')) return undefined;\n \n const cookies = request.headers.get('Cookie') || '';\n return new Response(`Cookies: ${cookies}`, {\n status: cookies.includes('token=') ? 200 : 401\n });\n};\n\n// Worker\nexport default {\n async fetch(request, env, ctx) {\n // CORS-protected route with prefix /cors/\n // Array form shown; also supports cors: true for permissive mode\n // See https://lumenize.com/docs/routing/route-do-request for routing details\n // See https://lumenize.com/docs/routing/cors-support for CORS configuration\n const routeCORSRequest = (req: Request, e: Env) => routeDORequest(req, e, {\n prefix: '/cors/',\n cors: { origin: ['https://safe.com', 'https://app.example.com'] },\n });\n \n // Worker handlers follow the hono convention:\n // - return Response if the handler wants to handle the route\n // - return undefined to fall through\n return (\n handleLogin(request) ||\n handleProtectedCookieEcho(request) ||\n await routeCORSRequest(request, env) ||\n await routeDORequest(request, env) ||\n new Response(\"Not Found\", { status: 404 })\n );\n }\n} satisfies ExportedHandler<Env>;\n\n// Durable Object\nexport class MyDO extends DurableObject<Env>{\n constructor(ctx: DurableObjectState, env: Env) {\n super(ctx, env);\n\n this.ctx.setWebSocketAutoResponse(\n new WebSocketRequestResponsePair('ar-ping', 'ar-pong'),\n );\n }
1\n\n increment(): number {\n let count = (this.ctx.storage.kv.get<number>(\"count\")) ?? 0;\n this.ctx.storage.kv.put(\"count\", ++count);\n return count;\n }\n\n echo(value: any): any { return value; }\n\n async fetch(request: Request) {\n const url = new URL(request.url); \n \n if (url.pathname.endsWith('/increment')) {\n const count = this.increment();\n return new Response(count.toString(), { \n headers: { 'Content-Type': 'text/plain' } \n });\n }\n\n if (request.headers.get(\"Upgrade\")?.toLowerCase() === \"websocket\") {\n const webSocketPair = new WebSocketPair();\n const [client, server] = Object.values(webSocketPair);\n \n // Handle sub-protocol selection\n const requestedProtocols = request.headers.get('Sec-WebSocket-Protocol');\n const responseHeaders = new Headers();\n let selectedProtocol: string | undefined;\n if (requestedProtocols) {\n const protocols = requestedProtocols.split(',').map(p => p.trim());\n if (protocols.includes('b')) {\n selectedProtocol = 'b';\n responseHeaders.set('Sec-WebSocket-Protocol', selectedProtocol);\n }\n }\n \n const name = url.pathname.split('/').at(-1) ?? 'No name in path'\n \n // Collect all request headers for testing\n const headersObj: Record<string, string> = {};\n request.headers.forEach((value, key) => {\n headersObj[key] = value;\n });\n \n const attachment = { \n name, \n headers: headersObj\n };\n \n this.ctx.acceptWebSocket(server, [name]);\n server.serializeAttachment(attachment);\n\n return new Response(null, {\n status: 101,\n webSocket: client,\n headers: responseHeaders\n });\n }\n\n return new Response('Not found', { status: 404 });\n }\n\n webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {\n if (message === 'increment') {\n return ws.send(this.increment().toString());\n }\n\n if (message === 'test-server-close') { \n return ws.close(4001, \"Server initiated close for testing\");\n }\n }\n\n webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) {\n this.ctx.storage.kv.put(\"lastWebSocketClose\", { code, reason, wasClean });\n ws.close(code, reason);\n }\n};\n\n"})}),"\n",(0,o.jsx)(t.h2,{id:"testtest-harnessts",children:"test/test-harness.ts"}),"\n",(0,o.jsx)(t.p,{children:"Create a test folder and drop this simple test harness into it:"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",metastring:"test/test-harness.ts",children:"import * as sourceModule from '../src';\nimport { instrumentDOProject } from '@lumenize/testing';\n\n// Auto-detects all DurableObject subclasses via prototype chain\n// walking \u2014 no need to list doClassNames, even with multiple DOs.\n// WorkerEntrypoints and other non-DO classes are passed through\n// unwrapped on the result object.\nconst instrumented = instrumentDOProject(sourceModule);\n\n// Wrangler requires DO classes as named exports.\n// For multiple DOs: export const { MyDO, AnotherDO } = instrumented.dos;\nexport const { MyDO } = instrumented.dos;\nexport default instrumented;\n\n"})}),"\n",(0,o.jsx)(t.h2,{id:"testwranglerjsonc",children:"test/wrangler.jsonc"}),"\n",(0,o.jsxs)(t.p,{children:["Take your existing wrangler.jsonc and make a copy of it in the test folder.\nThen change the ",(0,o.jsx)(t.code,{children:"main"})," setting to the ",(0,o.jsx)(t.code,{children:"./test-harness.ts"}),". So:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-json",metastring:"test/wrangler.jsonc",children:'{\n "name": "testing-plain-do",\n "main": "./test-harness.ts", // The only change from your real wrangler.jsonc\n "compatibility_date": "2026-03-12",\n "compatibility_flags": [\n "nodejs_compat"\n ],\n "migrations": [\n {\n "new_sqlite_classes": [\n "MyDO"\n ],\n "tag": "v1"\n }\n ],\n "durable_objects": {\n "bindings": [\n {\n "class_name": "MyDO",\n "name": "MY_DO"\n }\n ]\n }\n}\n'})}),"\n",(0,o.jsx)(t.h2,{id:"vitestconfigjs",children:"vitest.config.js"}),"\n",(0,o.jsxs)(t.p,{children:["Then add to your ",(0,o.jsx)(t.code,{children:"vite"})," config, if applicable, or create a ",(0,o.jsx)(t.code,{children:"vitest"})," config that\nlooks something like this:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-javascript",metastring:"vitest.config.js",children:"import { cloudflareTest } from \"@cloudflare/vitest-pool-workers\";\n\nimport { defineConfig } from \"vitest/config\";
1\n\nexport default defineConfig({\n plugins: [cloudflareTest({\n wrangler: { configPath: \"./test/wrangler.jsonc\" },\n })],\n\n test: {\n // 2 second global timeout\n testTimeout: 2000,\n\n // Use `vitest --run --coverage` to get test coverage report(s)\n coverage: {\n provider: \"istanbul\", // Cannot use V8\n reporter: ['text', 'json', 'html'],\n include: ['**/src/**'],\n exclude: [\n '**/node_modules/**', \n '**/dist/**', \n '**/build/**', \n '**/*.config.ts',\n '**/scratch/**'\n ],\n }\n }\n});\n\n"})}),"\n",(0,o.jsx)(t.h2,{id:"your-tests",children:"Your tests"}),"\n",(0,o.jsx)(t.p,{children:"Then write your tests using vitest as you would normally. The rest of this\ndocument are examples of tests you might write for the Worker and DO above."}),"\n",(0,o.jsx)(t.h2,{id:"websocket",children:"WebSocket"}),"\n",(0,o.jsxs)(t.p,{children:["One of the biggest shortcomings of ",(0,o.jsx)(t.code,{children:"cloudflare:test"})," and perhaps the primary\nmotivator for using ",(0,o.jsx)(t.code,{children:"@lumenize/testing"})," is support for testing your DO's\nWebSocket implementation. With ",(0,o.jsx)(t.code,{children:"@lumenize/testing"}),":"]}),"\n",(0,o.jsxs)(t.ul,{children:["\n",(0,o.jsx)(t.li,{children:"Use browser-compatible WebSocket API"}),"\n",(0,o.jsxs)(t.li,{children:["Routes WebSocket upgrade through Worker so that gets tested (unlike\n",(0,o.jsx)(t.code,{children:"runInDurableObject"}),")"]}),"\n",(0,o.jsx)(t.li,{children:"Test WebSocket sub-protocol selection"}),"\n",(0,o.jsx)(t.li,{children:'Interact with server-side WebSockets (getWebSockets("tag"), etc.)'}),"\n",(0,o.jsx)(t.li,{children:"Assert on WebSocket attachments"}),"\n",(0,o.jsxs)(t.li,{children:["Test your ",(0,o.jsx)(t.code,{children:"WebSocketRequestResponsePair"})," (impossible with\n",(0,o.jsx)(t.code,{children:"runInDurableObject"}),")"]}),"\n"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",metastring:"test",children:"it('shows testing WebSocket functionality', async () => {\n // Create RPC client to inspect server-side WebSocket state\n using client = createTestingClient<typeof MyDO>('MY_DO', 'test-ws');\n\n // Create WebSocket client\n const WebSocket = new Browser().WebSocket;\n \n // Create a WebSocket and wait for it to open\n const ws = new WebSocket('wss://test.com/my-do/test-ws', ['a', 'b']) as any;\n let wsOpened = false\n ws.onopen = () => wsOpened = true;\n await vi.waitFor(() => expect(wsOpened).toBe(true));\n\n // Verify the selected protocol matches what server chose\n expect(ws.protocol).toBe('b');\n\n // Send 'increment' message and verify response\n let incrementResponse: string | null = null;\n ws.onmessage = (event: any) => {\n incrementResponse = event.data;\n };\n ws.send('increment');\n await vi.waitFor(() => expect(incrementResponse).toBe('1'));\n \n // Trigger server-initiated close and verify close event\n let closeCode: number | null = null;\n ws.onclose = (event: any) => {\n closeCode = event.code;\n };\n ws.send('test-server-close');\n await vi.waitFor(() => expect(expect(closeCode).toBe(4001)));\n\n // Access getWebSockets using tag that matches DO instance name\n const webSocketsOnServer = await client.ctx.getWebSockets('test-ws');\n expect(webSocketsOnServer.length).toBe(1);\n\n // Assert on ws attachment\n const { deserializeAttachment } = webSocketsOnServer[0];\n const attachment = await deserializeAttachment();\n expect(attachment).toMatchObject({\n name: 'test-ws', // From URL path: /my-do/test-ws\n headers: expect.objectContaining({\n 'upgrade': 'websocket',\n 'sec-websocket-protocol': 'a, b'\n })\n });\n\n // Tests ctx.setWebSocketAutoResponse w/ new connection to the same DO\n const ws2 = new WebSocket('wss://test.com/my-do/test-ws') as any;\n let autoResponseReceived = false;\n let ws2Opened = false;\n ws2.onopen = () => ws2Opened = true;\n await vi.waitFor(() => expect(ws2Opened).toBe(true));\n \n ws2.send('ar-ping');\n ws2.onmessage = async (event: any) =>
1 {\n expect(event.data).toBe('ar-pong');\n autoResponseReceived = true;\n };\n await vi.waitFor(() => expect(autoResponseReceived).toBe(true));\n\n ws.close();\n});\n"})}),"\n",(0,o.jsx)(t.h2,{id:"structuredclone-types",children:"StructuredClone types"}),"\n",(0,o.jsx)(t.p,{children:"All structured clone types are supported (like Cloudflare native RPC)."}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",metastring:"test",children:"it('shows RPC working with StructuredClone types', async () => {\n using client = createTestingClient<typeof MyDO>('MY_DO', 'sc');\n\n // Map (and all StructuredClone types) works with storage\n const testMap = new Map<string, any>([['key1', 'value1'], ['key2', 42]]);\n await client.ctx.storage.kv.put('testMap', testMap);\n const retrievedMap = await client.ctx.storage.kv.get('testMap');\n expect(retrievedMap).toEqual(testMap);\n \n // Map (and all StructuredClone types) also works with custom method echo()\n const echoedMap = await client.echo(testMap);\n expect(echoedMap).toEqual(testMap);\n\n // Set\n const testSet = new Set<any>([1, 2, 3, 'four']);\n expect(await client.echo(testSet)).toEqual(testSet);\n\n // Date\n const testDate = new Date('2025-10-12T12:00:00Z');\n expect(await client.echo(testDate)).toEqual(testDate);\n\n // Circular reference\n const circular: any = { name: 'circular' };\n circular.self = circular;\n expect(await client.echo(circular)).toEqual(circular);\n});\n"})}),"\n",(0,o.jsx)(t.h2,{id:"cookies",children:"Cookies"}),"\n",(0,o.jsxs)(t.p,{children:[(0,o.jsx)(t.code,{children:"Browser"})," allows cookies to be shared between ",(0,o.jsx)(t.code,{children:"fetch"})," and ",(0,o.jsx)(t.code,{children:"WebSocket"})," just\nlike in a real browser. Use ",(0,o.jsx)(t.code,{children:"setCookie()"})," and ",(0,o.jsx)(t.code,{children:"getCookie()"})," for testing\nand debugging."]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",metastring:"test",children:"it('shows cookie sharing between fetch and WebSocket', async () => {\n // Create client and browser instances\n using client = createTestingClient<typeof MyDO>('MY_DO', 'cookies');\n const browser = new Browser();\n \n // Login via fetch - sets session cookie\n await browser.fetch('https://test.com/login?user=test');\n \n // Verify cookie was stored in the browser\n expect(browser.getCookie('token')).toBe('abc123');\n \n // Manually add additional cookies - domain is inferred from first fetch\n browser.setCookie('extra', 'manual-value');\n \n // Make another fetch request - gets BOTH cookies automatically\n const res = await browser.fetch('https://test.com/protected-cookie-echo');\n const text = await res.text();\n expect(text).toContain('token=abc123'); // From login\n expect(text).toContain('extra=manual-value'); // Manually added\n \n // WebSocket connection also gets BOTH cookies automatically!\n const ws = new browser.WebSocket('wss://test.com/my-do/cookies') as any;\n \n let wsOpened = false;\n ws.onopen = () => { wsOpened = true; };\n \n await vi.waitFor(() => expect(wsOpened).toBe(true));\n \n // Verify server received the cookies in the WebSocket upgrade request\n const wsList = await client.ctx.getWebSockets('cookies');\n const attachment = await wsList[0].deserializeAttachment();\n expect(attachment.headers.cookie).toContain('token=abc123');\n expect(attachment.headers.cookie).toContain('extra=manual-value');\n \n ws.close();\n});\n"})}),"\n",(0,o.jsx)(t.h2,{id:"simulate-browser-context-origin-behavior",children:"Simulate browser context Origin behavior"}),"\n",(0,o.jsxs)(t.p,{children:[(0,o.jsx)(t.code,{children:"Browser.context()"})," allows you to test CORS/Origin validation logic in your\nWorker or Durable Object. The ",(0,o.jsx)(t.code,{children:"context().fetch"})," method automatically validates\nCORS headers for cross-origin requests and throws a ",(0,o.jsx)(t.code,{children:"TypeError"})," (just like a\nreal browser) when the server doesn't return proper CORS headers or when the\norigin doesn't match."]}),"\n",(0,o.jsx)(t.p,{children:"This test also shows off the non-standard extension to the WebSocket API that\nallows you to inspect the underlying HTTP Request and Response objects, which\nis useful for debugging and asserting."}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",metastring:"test",children:"it('shows testing Origin validation using browser.context()', async () => {\n const browser = new Browser();\n \n // Create a context with Origin header\n const context = browser.context('https://safe.com');\n \n // WebSocket upgrade includes Origin header\n const ws = new context.WebSocket('wss://safe.com/cors/my-do/ws-test') as any;\n let wsOpened = false;\n ws.onopen = () => { wsOpened = true; };\n await vi.waitFor(() => expect(wsOpened).toBe(true));\n // Note: browser standard WebSocket doesn't have request/response properties, \n // but they're useful for debugging and asserting.\n expect(ws.request.headers.get('Origin')).toBe('https://safe.com');\n const acaoHeader = ws.response.headers.get('Access-Control-Allow-Origin');\n expect(acaoHeader).toBe('https://safe.com');\n ws.close();\n \n // HTTP request also includes Origin header - allowed\n let res = await context.fetch('https://safe.com/cors/my-do/test/increment');\n const acaoHeaderFromFetch = res.headers.get('Access-Control-Allow-Origin');\n expect(acaoHeaderFromFetch).toBe('https://safe.com');\n \n // Now let's test a blocked Origin evil.com\n\n // Set up: Pre-populate count to verify DO is never called\n using client = createTesting
1Client<typeof MyDO>('MY_DO', 'blocked');\n await client.ctx.storage.put('count', 42);\n\n // Blocked origin - server rejects with 403 without CORS headers\n // Browser.context().fetch validates CORS headers and throws TypeError\n // when CORS validation fails, just like a real browser would\n const pg = browser.context('https://evil.com');\n \n // Expect TypeError due to CORS error\n await expect(async () => {\n await pg.fetch('https://safe.com/cors/my-do/blocked/increment');\n }).rejects.toThrow(TypeError);\n await expect(async () => {\n await pg.fetch('https://safe.com/cors/my-do/blocked/increment');\n }).rejects.toThrow('CORS error');\n \n // Verify DO was never called - count is still 42 (not 43)\n const count = await client.ctx.storage.get('count');\n expect(count).toBe(42);\n});\n"})}),"\n",(0,o.jsx)(t.h2,{id:"cors-preflight-options",children:"CORS preflight OPTIONS"}),"\n",(0,o.jsx)(t.p,{children:'Real browsers automatically send preflight OPTIONS requests for "non-simple"\ncross-origin requests (e.g., requests with custom headers, non-simple content\ntypes like application/json, or non-simple methods like PUT/DELETE/PATCH).'}),"\n",(0,o.jsxs)(t.p,{children:[(0,o.jsx)(t.code,{children:"Browser.context(origin).fetch"})," also sends preflight OPTIONS requests under\nthe same conditions that real browsers do! This section demonstrates that\nbehavior using requests with a custom header."]}),"\n",(0,o.jsxs)(t.p,{children:["The context object includes a non-standard ",(0,o.jsx)(t.code,{children:"lastPreflight"})," property which is\nuseful for testing or debugging. It lets you inspect the most recent preflight."]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",metastring:"test",children:"it('shows testing CORS preflight OPTIONS requests', async () => {\n const browser = new Browser();\n const appContext = browser.context('https://app.example.com');\n\n // Common requestOptions will trigger preflight if cross-origin\n const requestOptions = { headers: { 'X-Custom-Header': 'test-value' }};\n \n // Same-origin - no preflight needed even with custom header\n await appContext.fetch(\n 'https://app.example.com/my-do/preflight/increment', \n requestOptions\n );\n expect(appContext.lastPreflight).toBeNull(); // No preflight for same-origin\n \n // Cross-origin with custom header - triggers automatic preflight!\n const postResponse = await appContext.fetch(\n 'https://safe.com/cors/my-do/preflight/increment',\n requestOptions\n );\n expect(appContext.lastPreflight?.success).toBe(true); // preflight succeeded\n expect(postResponse.ok).toBe(true); // request worked\n expect(postResponse.headers.get('Access-Control-Allow-Origin'))\n .toBe('https://app.example.com'); // CORS header reflects the origin\n \n // Cross-origin from disallowed evil.com - preflight fails!\n const evilContext = browser.context('https://evil.com');\n await expect(async () => {\n await evilContext.fetch(\n 'https://safe.com/cors/my-do/preflight/increment',\n requestOptions\n );\n }).rejects.toThrow('CORS error');\n expect(evilContext.lastPreflight?.success).toBe(false); // preflight failed\n});\n"})}),"\n",(0,o.jsx)(t.h2,{id:"discover-all-public-members-of-do",children:"Discover all public members of DO"}),"\n",(0,o.jsxs)(t.p,{children:[(0,o.jsx)(t.code,{children:"createTestingClient.__asObject()"})," allows you to discover all public members on\nthe DO instance (env, ctx, custom methods)"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",metastring:"test",children:'it(\'shows DO inspection and function discovery using __asObject()\', async () => {\n using client = createTestingClient<typeof MyDO>(\'MY_DO\', \'asObject\');\n\n const instanceAsObject = await client.__asObject?.();\n \n expect(instanceAsObject).toMatchObject({\n // DO methods are discoverable\n increment: "increment [Function]",\n \n // DurableObjectState context with complete API\n ctx: {\n storage: {\n get: "get [Function]",\n // ... other storage methods available\n sql: {\n databaseSize: expect.any(Number), // Assert on non-function properties\n // ... other ctx.sql methods\n },\n kv: {\n get: "get [Function]",\n // ... other storage methods available\n },\n },\n getWebSockets: "getWebSockets [Function]",\n // ... other ctx methods available\n },\n \n // Environment object with DO bindings\n env: {\n MY_DO: {\
1n getByName: "getByName [Function]",\n // ... other binding methods available\n },\n // ... other environment bindings available\n }\n });\n});\n'})}),"\n",(0,o.jsx)(t.h2,{id:"quirks",children:"Quirks"}),"\n",(0,o.jsxs)(t.p,{children:[(0,o.jsx)(t.code,{children:"createTestingClient"})," has these quirks:"]}),"\n",(0,o.jsxs)(t.ul,{children:["\n",(0,o.jsxs)(t.li,{children:["Even non-async function calls require ",(0,o.jsx)(t.code,{children:"await"})]}),"\n",(0,o.jsxs)(t.li,{children:["Property access is synchronous on ",(0,o.jsx)(t.code,{children:"__asObject()"}),", but..."]}),"\n",(0,o.jsxs)(t.li,{children:["Even static property access requires ",(0,o.jsx)(t.code,{children:"await"})," outside of ",(0,o.jsx)(t.code,{children:"__asObject()"})]}),"\n",(0,o.jsx)(t.li,{children:"Operation chaining - result of one call, is used in another."}),"\n"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",metastring:"test",children:"it('requires await for even non-async function calls', async () => {\n using client = createTestingClient<typeof MyDO>('MY_DO', 'quirks');\n\n // All calls require await even if the function is not async\n\n // Using `async ctx.storage.put(...)` requires await in both RPC and the DO\n await client.ctx.storage.put('key', 'value');\n\n // Using non-async `ctx.storage.kv.get(...)`\n // does not require await in DO but does in RPC\n const asyncResult = await client.ctx.storage.kv.get('key');\n expect(asyncResult).toBe('value');\n \n // Property access can be chained and destructured (returns a new Proxy)\n // We call this object chaining and nesting (OCAN) and it allows you to\n // do multiple operations with one round trip over the network.\n // See: https://lumenize.com/docs/rpc/operation-chaining-and-nesting\n const storage = client.ctx.storage;\n const { sql } = storage;\n \n // Static properties can be accessed directly but still require await\n expect(typeof (await sql.databaseSize)).toBe('number');\n \n // __asObject() is only callable from the root client, not nested proxies\n // and it returns the complete nested structure as plain data\n const fullObject = await client.__asObject?.();\n \n // No `await` needed to access nested static properties from __asObject()\n expect(typeof fullObject.ctx.storage.sql.databaseSize).toBe('number');\n expect(fullObject.ctx.storage.sql.databaseSize).toBe(await sql.databaseSize);\n});\n"})}),"\n",(0,o.jsx)(t.h2,{id:"try-it-out",children:"Try it out"}),"\n",(0,o.jsx)(t.p,{children:"To run it as a vitest:"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-bash",children:"vitest --run\n"})}),"\n",(0,o.jsx)(t.p,{children:"You can even see how much of the code is covered by these tests:"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-bash",children:"vitest --run --coverage\n"})}),"\n",(0,o.jsx)(t.p,{children:"It should look something like this:"}),"\n",(0,o.jsx)("img",{src:"/img/coverage-report.png",alt:"Test coverage report",style:{maxWidth:"500px",height:"auto"}}),"\n",(0,o.jsx)(t.p,{children:"With the right vitest configuration, it'll even show you the coverage of your\nclient-side code in the same report."})]})}function h(e={}){const{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(d,{...e})}):d(e)}},43023(e,t,n){n.d(t,{R:()=>i,x:()=>c});var s=n(63696);const o={},r=s.createContext(o);function i(e){const t=s.useContext(r);return s.useMemo(function(){return"function"==typeof e?e(t):{...t,...e}},[t,e])}function c(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:i(e.components),s.createElement(r.Provider,{value:t},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.