import assert from "node:assert"; import { existsSync } from "node:fs"; import { readFile } from "node:fs/promises"; import * as nodeNet from "node:net"; import { setTimeout } from "node:timers/promises"; import { stripVTControlCharacters } from "node:util"; import dedent from "ts-dedent"; import { fetch } from "undici"; import { afterEach, beforeEach, describe, it, vi } from "vitest"; import { CLOUDFLARE_ACCOUNT_ID, E2E_ACCOUNT_WORKERS_DEV_DOMAIN, } from "./helpers/account-id"; import { WranglerE2ETestHelper } from "./helpers/e2e-wrangler-test"; import { fetchText } from "./helpers/fetch-text"; import { fetchWithETag } from "./helpers/fetch-with-etag"; import { generateResourceName } from "./helpers/generate-resource-name"; import { MYSQL_INITIAL_HANDSHAKE_PACKET, setupMysqlServer, } from "./helpers/mysql-echo-handler"; import { createPostgresEchoHandler, POSTGRES_SSL_REQUEST_PACKET, } from "./helpers/postgres-echo-handler"; import { retry } from "./helpers/retry"; import { waitFor, waitForLong } from "./helpers/wait-for"; import { getStartedWorkerdProcesses } from "./helpers/workerd-processes"; const HYPERDRIVE_DATABASES = [ { scheme: "postgresql", defaultPort: 5432, envVar: "HYPERDRIVE_DATABASE_URL", }, { scheme: "mysql", defaultPort: 3306, envVar: "HYPERDRIVE_MYSQL_DATABASE_URL", }, ] as const; /** * We use the same workerName for all of the tests in this suite in hopes of reducing flakes. * When creating a new worker, a .devprod-testing7928.workers.dev subdomain is created. * The platform API locks a database table for the zone (devprod-testing7928.workers.dev) while doing this. * Creating many workers in the same account/zone in quick succession can run up against the lock. * This test suite runs sequentially so does not cause lock issues for itself, but we run into lock issues * when multiple PRs have jobs running at the same time (or the same PR has the tests run across multiple OSes). */ const workerName = generateResourceName(); describe.each([ { cmd: "wrangler dev --port=0 --inspector-port=0" }, ...(CLOUDFLARE_ACCOUNT_ID ? [{ cmd: "wrangler dev --remote --port=0 --inspector-port=0" }] : []), ])("basic js dev: $cmd", ({ cmd }) => { it(`can modify Worker during ${cmd}`, async ({ expect }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" compatibility_flags = ["nodejs_compat"] [vars] KEY = "value" `, "src/index.ts": dedent` export default { fetch(request) { return new Response("Hello World!") } }`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(); await expect(fetch(url).then((r) => r.text())).resolves.toMatchSnapshot(); await helper.seed({ "src/index.ts": dedent` export default { fetch(request, env) { return new Response("Updated Worker! " + env.KEY) } }`, }); await worker.waitForReload(); // Regression test for issue where multiple request logs were being logged per request expect([...worker.currentOutput.matchAll(/GET /g)].length).toBe(1); await waitForLong(() => expect(fetchText(url)).resolves.toMatchSnapshot()); }); it("works with basic service worker", async ({ expect }) => { const helper = new WranglerE2ETestHelper(); const isLocal = cmd.includes("--remote") ? false : true; await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" # TODO: This is a workaround for an EWC bug where remote dev workers only log properly if they have bindings. # Remove the below line when MR:7727 is merged version_metadata = { binding = "METADATA" } `, "src/index.ts": dedent` addEventListener("fetch", (event) => { const { pathname } = new URL(event.request.url); if (pathname === "/") { event.respondWith(new Response("service worker")); } else if (pathname === "/error") { throw new Error("monkey"); } else { event.respondWith(new Response(null, { status: 404 })); } }); `, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(); let res = await fetch(url); expect(await res.text()).toBe("service worker"); res = await fetch(new URL("/error", url), { headers: { Accept: "text/plain" }, }); const text = await res.text(); if (isLocal) { expect(text).toContain("Error: monkey"); expect(text).toContain("src/index.ts:6:9"); } await worker.readUntil(/monkey/, 30_000); if (isLocal) { await worker.readUntil(/src\/index\.ts:6:9/, 30_000); } }); it(`hotkeys can be disabled with ${cmd}`, async ({ expect }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" compatibility_flags = ["nodejs_compat"] [vars] KEY = "value" `, "src/index.ts": dedent` export default { fetch(request) { return new Response("Hello World!") } }`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived( `${cmd} --show-interactive-dev-session=false` ); const { url } = await worker.waitForReady(); await waitForLong(() => expect(fetch(url).then((r) => r.text())).resolves.toMatchSnapshot() ); expect(worker.currentOutput).not.toContain("[b] open a browser"); }); describe(`--test-scheduled works with ${cmd}`, async () => { it("custom build", async ({ expect }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" [build] command = "true" `, "src/index.ts": dedent` export default { scheduled(event) { console.log("Event triggered") } }`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived(`${cmd} --test-scheduled`); const { url } = await worker.waitForReady(); await expect( fetch(`${url}/__scheduled`).then((r) => r.text()) ).resolves.toMatchSnapshot(); await worker.readUntil(/Event triggered/); }); it("no custom build", async ({ expect }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" `, "src/index.ts": dedent` export default { scheduled(event) { console.log("Event triggered") } }`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived(`${cmd} --test-scheduled`); const { url } = await worker.waitForReady(); await expect( fetch(`${url}/__scheduled`).then((r) => r.text()) ).resolves.toMatchSnapshot(); await worker.readUntil(/Event triggered/); }); }); describe(`scheduled worker warning with ${cmd}`, () => { it("shows warning with correct port when cron trigger is configured", async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" [triggers] crons = ["* * * * *"] `, "src/index.ts": dedent` export default { fetch(request) { return new Response("Hello World!") }, scheduled(event) { console.log("Scheduled event triggered"); } }`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(); const { hostname, port } = new URL(url); // The warning should contain the actual port, not "undefined" await waitFor(() => { expect(worker.currentOutput).toContain( "Scheduled Workers are not automatically triggered" ); expect(worker.currentOutput).toContain( `curl "http://${hostname}:${port}/cdn-cgi/handler/scheduled"` ); expect(worker.currentOutput).not.toContain("undefined"); }); }); it("does not show warning when --test-scheduled is enabled", async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" [triggers] crons = ["* * * * *"] `, "src/index.ts": dedent` export default { fetch(request) { return new Response("Hello World!") }, scheduled(event) { console.log("Scheduled event triggered"); } }`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived(`${cmd} --test-scheduled`); await worker.waitForReady(); // The warning should NOT appear when testScheduled is enabled expect(worker.currentOutput).not.toContain( "Scheduled Workers are not automatically triggered" ); }); }); describe("Workers + Assets", () => { it(`can modify User Worker during ${cmd}`, async ({ expect }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" compatibility_flags = ["nodejs_compat"] [assets] directory = "public" `, "src/index.ts": dedent` export default { fetch(request) { return new Response("Hello World!") } }`, "public/readme.md": dedent` Welcome to Workers + Assets readme!`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived(cmd); // Remote mode with assets involves session creation + asset upload + // bundle upload to edge-preview, which can be slow on Windows CI. const { url } = await worker.waitForReady(30_000); await expect(fetch(url).then((r) => r.text())).resolves.toMatchSnapshot(); await helper.seed({ "src/index.ts": dedent` export default { fetch(request, env) { return new Response("Updated Worker!") } }`, }); await worker.waitForReload(30_000); await expect(fetchText(url)).resolves.toMatchSnapshot(); }); it(`can modify assets during ${cmd}`, async ({ expect }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" compatibility_flags = ["nodejs_compat"] [assets] directory = "public" `, "src/index.ts": dedent` export default { fetch(request) { return new Response("Hello World!") } }`, "public/readme.md": dedent` Welcome to Workers + Assets readme!`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(30_000); await expect(fetch(url).then((r) => r.text())).resolves.toMatchSnapshot(); await helper.seed({ "public/readme.md": dedent` Welcome to updated Workers + Assets readme!`, }); await worker.waitForReload(30_000); await expect(fetchText(url)).resolves.toMatchSnapshot(); }); }); }); // This fails on Windows because of https://github.com/cloudflare/workerd/issues/1664 it.runIf(process.platform !== "win32")( `leaves no orphaned workerd processes with port conflict`, async ({ expect }) => { const initial = new WranglerE2ETestHelper(); await initial.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" `, "src/index.ts": dedent` export default { fetch(request) { return new Response("Hello World!") } }`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const initialWorker = initial.runLongLived(`wrangler dev`); const { url: initialWorkerUrl } = await initialWorker.waitForReady(); const port = new URL(initialWorkerUrl).port; const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" `, "src/index.ts": dedent` export default { fetch(request) { return new Response("Hello World!") } }`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const beginProcesses = getStartedWorkerdProcesses(helper.tmpPath); // If a port isn't specified, Wrangler will start up on a different random port. In this test we want to force an address-in-use error const worker = helper.runLongLived(`wrangler dev --port ${port}`); const exitCode = await worker.exitCode; expect(exitCode).not.toBe(0); const endProcesses = getStartedWorkerdProcesses(helper.tmpPath); expect(beginProcesses.length).toBe(0); expect(endProcesses.length).toBe(0); } ); // Skipping remote python tests because they consistently flake with timeouts // Unskip once remote dev with python workers is more stable describe.each([{ cmd: "wrangler dev" }])( "basic python dev: $cmd", { timeout: 90_000 }, ({ cmd }) => { it(`can modify entrypoint during ${cmd}`, async ({ expect }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "index.py" compatibility_date = "2023-01-01" compatibility_flags = ["python_workers"] `, "arithmetic.py": dedent` def mul(a,b): return a*b`, "index.py": dedent` from arithmetic import mul from js import Response def on_fetch(request): return Response.new(f"py hello world {mul(2,3)}")`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(); await expect(fetchText(url)).resolves.toBe("py hello world 6"); await helper.seed({ "index.py": dedent` from js import Response def on_fetch(request): return Response.new('Updated Python Worker value')`, }); await worker.waitForReload(); // TODO(soon): work out why python workers need this retry before returning new content const { text } = await retry( (s) => s.status !== 200 || s.text === "py hello world 6", async () => { const r = await fetch(url); return { text: await r.text(), status: r.status }; } ); expect(text).toBe("Updated Python Worker value"); }); it(`can modify imports during ${cmd}`, async ({ expect }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "index.py" compatibility_date = "2023-01-01" compatibility_flags = ["python_workers"] `, "arithmetic.py": dedent` def mul(a,b): return a*b`, "index.py": dedent` from arithmetic import mul from js import Response def on_fetch(request): return Response.new(f"py hello world {mul(2,3)}")`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(); await expect(fetchText(url)).resolves.toBe("py hello world 6"); await helper.seed({ "arithmetic.py": dedent` def mul(a,b): return a+b`, }); await worker.waitForReload(); // TODO(soon): work out why python workers need this retry before returning new content const { text } = await retry( (s) => s.status !== 200 || s.text === "py hello world 6", async () => { const r = await fetch(url); return { text: await r.text(), status: r.status }; } ); expect(text).toBe("py hello world 5"); }); it(`can print during ${cmd}`, async ({ expect }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "index.py" compatibility_date = "2023-01-01" compatibility_flags = ["python_workers"] `, "arithmetic.py": dedent` def mul(a,b): return a*b`, "index.py": dedent` from arithmetic import mul from js import Response, console def on_fetch(request): console.log(f"hello {mul(2,3)}") print(f"foobar {mul(4,3)}") console.log(f"end") return Response.new(f"py hello world {mul(2,3)}")`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(); await expect(fetchText(url)).resolves.toBe("py hello world 6"); await worker.readUntil(/hello 6/); await worker.readUntil(/foobar 12/); await worker.readUntil(/end/); }); it(`prints additional modules when vendored modules are present during ${cmd}`, async () => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "index.py" compatibility_date = "2023-01-01" compatibility_flags = ["python_workers"] `, "arithmetic.py": dedent` def mul(a,b): return a*b`, "index.py": dedent` from arithmetic import mul from js import Response def on_fetch(request): return Response.new(f"py hello world {mul(2,3)}")`, "python_modules/mod1.py": "print(42)", "python_modules/mod2.py": "def hello(): return 42", "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived(cmd); // Check that the additional modules output includes the vendored modules // This needs to be done before waitForReady() since that consumes the output stream await worker.readUntil(/Attaching additional modules:/); await worker.readUntil(/Vendored Modules/); await worker.waitForReady(); }); it(`can exclude vendored module during ${cmd}`, async ({ expect }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.py" compatibility_date = "2023-01-01" compatibility_flags = ["python_workers"] [python_modules] exclude = ["excluded_module.py"] `, "src/arithmetic.py": dedent` def mul(a,b): return a*b`, "python_modules/excluded_module.py": dedent` def excluded(a,b): return a*b`, "src/index.py": dedent` from arithmetic import mul from js import Response def on_fetch(request): print(f"hello {mul(2,3)}") try: import excluded_module print("excluded_module found") except ImportError: print("excluded_module not found") print(f"end") return Response.new(f"py hello world {mul(2,3)}")`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(); await expect(fetchText(url)).resolves.toBe("py hello world 6"); await worker.readUntil(/hello 6/); await worker.readUntil(/excluded_module not found/); await worker.readUntil(/end/); }); } ); // When `wrangler dev` is force-killed via tree-kill at test teardown, // workerd's outbound TCP connection to the mock database server is reset // rather than gracefully closed. With the Hyperdrive proxy now skipped for // `sslmode=disable` (the default), that RST surfaces directly on the // per-connection socket of the test's `nodeNet.Server`. Without an `error` // listener Node escalates it to `uncaughtException`, which Vitest catches as // an unhandled error and fails the run even though the test itself passed. // The same teardown race can also surface as EPIPE, if the server's initial // handshake write (e.g. `MYSQL_INITIAL_HANDSHAKE_PACKET`) lands after the // other end has already been torn down. function ignoreEconnreset(err: NodeJS.ErrnoException): void { if (err.code !== "ECONNRESET" && err.code !== "EPIPE") { throw err; } } describe.each(HYPERDRIVE_DATABASES)( "hyperdrive dev tests ($scheme)", ({ scheme, defaultPort, envVar }) => { let server: nodeNet.Server; beforeEach(async () => { if (scheme === "mysql") { server = nodeNet.createServer().listen(); setupMysqlServer(server); } else { server = nodeNet .createServer((socket) => { socket.on("data", createPostgresEchoHandler(socket)); }) .listen(); } // Attach a no-op-on-ECONNRESET listener to every accepted socket. // See `ignoreEconnreset` for context. server.on("connection", (socket) => { socket.on("error", ignoreEconnreset); }); }); it("matches expected configuration parameters", async ({ expect }) => { const helper = new WranglerE2ETestHelper(); let port: number = defaultPort; if (server.address() && typeof server.address() !== "string") { port = (server.address() as nodeNet.AddressInfo).port; } await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-10-25" [[hyperdrive]] binding = "HYPERDRIVE" id = "hyperdrive_id" localConnectionString = "${scheme}://user:%21pass@127.0.0.1:${port}/some_db" `, "src/index.ts": dedent` export default { async fetch(request, env) { if (request.url.includes("connect")) { const conn = env.HYPERDRIVE.connect(); await conn.writable.getWriter().write(new TextEncoder().encode("test string")); } return new Response(env.HYPERDRIVE?.connectionString ?? "no") } }`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived("wrangler dev"); const { url } = await worker.waitForReady(); const text = await fetchText(url); assert(text); const hyperdrive = new URL(text); expect(hyperdrive.pathname).toBe("/some_db"); expect(hyperdrive.username).toBe("user"); expect(hyperdrive.password).toBe("!pass"); expect(hyperdrive.host).not.toBe("localhost"); }); it("connects to a socket", async ({ expect }) => { const helper = new WranglerE2ETestHelper(); let port: number = defaultPort; if (server.address() && typeof server.address() !== "string") { port = (server.address() as nodeNet.AddressInfo).port; } await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-10-25" [[hyperdrive]] binding = "HYPERDRIVE" id = "hyperdrive_id" localConnectionString = "${scheme}://user:pass@127.0.0.1:${port}/some_db" `, "src/index.ts": dedent` export default { async fetch(request, env) { if (request.url.includes("connect")) { const conn = env.HYPERDRIVE.connect(); await conn.writable.getWriter().write(new TextEncoder().encode("test string")); } return new Response(env.HYPERDRIVE?.connectionString ?? "no") } }`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const socketMsgPromise = new Promise((resolve, _) => { server.on("connection", (socket) => { // For MySQL, send initial handshake first if (scheme === "mysql") { socket.write(MYSQL_INITIAL_HANDSHAKE_PACKET); } socket.on("data", (chunk) => { if ( scheme === "postgresql" && chunk.equals(POSTGRES_SSL_REQUEST_PACKET) ) { socket.write("N"); return; } expect(new TextDecoder().decode(chunk)).toBe("test string"); server.close(); resolve({}); }); }); }); const worker = helper.runLongLived("wrangler dev"); const { url } = await worker.waitForReady(); await fetch(`${url}/connect`); await socketMsgPromise; }); it("uses HYPERDRIVE_LOCAL_CONNECTION_STRING for the localConnectionString variable in the binding", async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); let port: number = defaultPort; if (server.address() && typeof server.address() !== "string") { port = (server.address() as nodeNet.AddressInfo).port; } await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-10-25" [[hyperdrive]] binding = "HYPERDRIVE" id = "hyperdrive_id" `, "src/index.ts": dedent` export default { async fetch(request, env) { if (request.url.includes("connect")) { const conn = env.HYPERDRIVE.connect(); await conn.writable.getWriter().write(new TextEncoder().encode("test string")); } return new Response(env.HYPERDRIVE?.connectionString ?? "no") } }`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived("wrangler dev", { env: { ...process.env, CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE: `${scheme}://user:pass@127.0.0.1:${port}/some_db`, }, }); const { url } = await worker.waitForReady(); const socketMsgPromise = new Promise((resolve, reject) => { server.on("connection", (socket) => { // For MySQL, send initial handshake first if (scheme === "mysql") { socket.write(MYSQL_INITIAL_HANDSHAKE_PACKET); } socket.on("data", (chunk) => { if ( scheme === "postgresql" && POSTGRES_SSL_REQUEST_PACKET.equals(chunk) ) { socket.write("N"); return; } expect(new TextDecoder().decode(chunk)).toBe("test string"); server.close(); resolve({}); }); socket.on("error", (err) => { console.error("Socket error:", err); reject(err); }); }); }); await fetch(`${url}/connect`); await socketMsgPromise; }); it.skipIf(!CLOUDFLARE_ACCOUNT_ID || !process.env[envVar])( "does not require local connection string when running `wrangler dev --remote`", async () => { const helper = new WranglerE2ETestHelper(); const { id } = await helper.hyperdrive(false, scheme); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-10-25" [[hyperdrive]] binding = "HYPERDRIVE" id = "${id}" `, "src/index.ts": dedent` export default { async fetch(request, env) { if (request.url.includes("connect")) { const conn = env.HYPERDRIVE.connect(); } return new Response(env.HYPERDRIVE?.connectionString ?? "no") } }`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived("wrangler dev --remote"); const { url } = await worker.waitForReady(); await fetch(`${url}/connect`); } ); afterEach(() => { if (server.listening) { server.close(); } }); } ); describe("queue dev tests", () => { it("matches expected configuration parameters", async ({ expect }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2024-04-04" [[queues.producers]] binding = "QUEUE" queue = "test-queue" delivery_delay = 2 `, "src/index.ts": dedent` export default { async fetch(request, env) { env.QUEUE.send(); return new Response('sent'); } }`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived("wrangler dev"); const { url } = await worker.waitForReady(); const text = await fetchText(url); expect(text).toBe("sent"); }); }); describe("writes debug logs to hidden file", () => { it("writes to file when --log-level = debug", async ({ expect }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" `, "src/index.ts": dedent /* javascript */ ` export default { fetch(req, env) { return new Response('A' + req.url); }, }; `, "package.json": dedent` { "name": "a", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived("wrangler dev --log-level debug"); const match = await worker.readUntil( /🪵 {2}Writing logs to "(?.+\.log)"/ ); const filepath = match.groups?.filepath; assert(filepath); await setTimeout(1000); // wait a bit to ensure Wrangler starts writing expect(existsSync(filepath)).toBe(true); }); it("does NOT write to file when --log-level != debug", async ({ expect }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" `, "src/index.ts": dedent /* javascript */ ` export default { fetch(req, env) { return new Response('A' + req.url); }, }; `, "package.json": dedent` { "name": "a", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived("wrangler dev"); await worker.waitForReady(); expect(worker.output).not.toContain("Writing logs to"); }); }); describe("analytics engine", () => { describe.each([{ cmd: "wrangler dev" }])( "mock analytics engine datasets: $cmd", ({ cmd }) => { describe("module worker", () => { it("analytics engine datasets are mocked in dev", async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2022-08-08" [[analytics_engine_datasets]] binding = "ANALYTICS_BINDING" dataset = "ANALYTICS_DATASET" `, "src/index.ts": dedent` export default { fetch(request, env) { // let's make an analytics call env.ANALYTICS_BINDING.writeDataPoint({ 'blobs': ["Seattle", "USA", "pro_sensor_9000"], // City, State 'doubles': [25, 0.5], 'indexes': ["a3cd45"] }); // and return a response return new Response("successfully wrote datapoint from module worker"); } }`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(); const text = await fetchText(url); expect(text).toContain( `successfully wrote datapoint from module worker` ); }); }); describe("service worker", async () => { it("using analytics engine datasets logs a warning in dev", async () => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2024-08-08" [[analytics_engine_datasets]] binding = "ANALYTICS_BINDING" dataset = "ANALYTICS_DATASET" `, "src/index.ts": dedent` addEventListener("fetch", (event) => { // let's make an analytics call ANALYTICS_BINDING.writeDataPoint({ blobs: ["Seattle", "USA", "pro_sensor_9000"], // City, State doubles: [25, 0.5], indexes: ["a3cd45"], }); // and return a response event.respondWith(new Response("successfully wrote datapoint from service worker")); }); `, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived(cmd); await worker.readUntil( /Analytics Engine is not supported locally when using the service-worker format/ ); }); }); } ); }); describe.skipIf(CLOUDFLARE_ACCOUNT_ID !== "8d783f274e1f82dc46744c297b015a2f")( "zone selection", () => { it("defaults to a workers.dev preview", async ({ expect }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" compatibility_flags = ["nodejs_compat"]`, "src/index.ts": dedent` export default { fetch(request) { return new Response(request.url) } }`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived("wrangler dev --remote"); const { url } = await worker.waitForReady(); const text = await fetchText(url); expect(text).toContain(E2E_ACCOUNT_WORKERS_DEV_DOMAIN); }); it("respects dev.host setting", async ({ expect }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" compatibility_flags = ["nodejs_compat"] [dev] host = "wrangler-testing.testing.devprod.cloudflare.dev"`, "src/index.ts": dedent` export default { fetch(request) { return new Response(request.url) } }`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived("wrangler dev --remote"); const { url } = await worker.waitForReady(); const text = await fetchText(url); expect(text).toMatchInlineSnapshot( `"https://wrangler-testing.testing.devprod.cloudflare.dev/"` ); }); it("infers host from first route", async ({ expect }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" compatibility_flags = ["nodejs_compat"] [[routes]] pattern = "wrangler-testing.testing.devprod.cloudflare.dev/*" zone_name = "testing.devprod.cloudflare.dev" `, "src/index.ts": dedent` export default { fetch(request) { return new Response(request.url) } }`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived("wrangler dev --remote"); const { url } = await worker.waitForReady(); const text = await fetchText(url); expect(text).toMatchInlineSnapshot( `"https://wrangler-testing.testing.devprod.cloudflare.dev/"` ); }); it("fails with useful error message if host is not routable", async () => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" compatibility_flags = ["nodejs_compat"] [[routes]] pattern = "not-a-domain.testing.devprod.cloudflare.dev/*" zone_name = "testing.devprod.cloudflare.dev" `, "src/index.ts": dedent` export default { fetch(request) { return new Response(request.url) } }`, "package.json": dedent` { "name": "worker", "version": "0.0.0", "private": true } `, }); const worker = helper.runLongLived("wrangler dev --remote"); const { url } = await worker.waitForReady(); await fetchText(url); await worker.readUntil(/ERROR/); }); } ); describe("custom builds", () => { it("does not hang when custom build does not cause esbuild to run", async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" compatibility_date = "2023-01-01" main = "src/index.ts" build.command = "echo 'hello'" build.watch_dir = "custom_src" `, "src/index.ts": dedent` export default { async fetch(request) { return new Response("Hello, World!") } }`, }); const worker = helper.runLongLived("wrangler dev"); const { url } = await worker.waitForReady(); await fetch(url); let text = await fetchText(url); expect(text).toMatchInlineSnapshot(`"Hello, World!"`); await helper.seed({ "custom_src/foo.txt": "", }); await worker.readUntil(/echo 'hello'/); text = await fetchText(url); expect(text).toMatchInlineSnapshot(`"Hello, World!"`); }); it("does not infinite-loop custom build with assets", async ({ expect }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" compatibility_date = "2023-01-01" main = "src/index.ts" build.command = "echo 'hello' > ./public/index.html" [assets] directory = "./public" `, "src/index.ts": dedent` export default { async fetch(request) { return new Response("Hello, World!") } } `, "public/other.html": "ensure ./public exists", }); const worker = helper.runLongLived("wrangler dev"); // first build on startup await worker.readUntil(/\[custom build\] Running/, 5_000); // second build for first watcher notification (can be optimised away, leaving as-is for now) await worker.readUntil(/\[custom build\] Running/, 5_000); // Need to get the url in this order because waitForReady calls readUntil // which keeps track of where it's read up to so far, // so the expect(waitUntil).reject assertion below // will eat up the "Ready on http://localhost:8787" message if called before. // This could cause a flake if eg the 2nd custom build starts after ready. const { url } = await worker.waitForReady(); // assert no more custom builds happen // regression: https://github.com/cloudflare/workers-sdk/issues/6876 await expect( worker.readUntil(/\[custom build\] Running/, 5_000) ).rejects.toThrow(); // now check assets are still fetchable, even after updates const res = await fetch(url); await expect(res.text()).resolves.toContain("hello"); await helper.seed({ "public/index.html": "world", }); const resText = await retry( (text) => text.includes("hello"), async () => { const res2 = await fetch(url); return res2.text(); } ); await expect(resText).toBe("world"); }); }); describe("watch mode", () => { describe.each([{ cmd: "wrangler dev" }])( "Workers watch mode: $cmd", ({ cmd }) => { it(`supports modifying the Worker script during dev session`, async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/workerA.ts" compatibility_date = "2023-01-01" `, "src/workerA.ts": dedent` export default { fetch(request) { return new Response("Hello from user Worker A!") } }`, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(); let text = await fetchText(url); expect(text).toBe("Hello from user Worker A!"); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/workerB.ts" compatibility_date = "2023-01-01" `, "src/workerB.ts": dedent` export default { fetch(request) { return new Response("Hello from user Worker B!") } }`, }); await worker.waitForReload(); text = await retry( (s) => s != "Hello from user Worker B!", async () => { return await fetchText(url); } ); expect(text).toBe("Hello from user Worker B!"); }); } ); describe.each([{ cmd: "wrangler dev" }])( "Workers + Assets watch mode: $cmd", ({ cmd }) => { it(`supports modifying existing assets during dev session`, async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" compatibility_date = "2023-01-01" [assets] directory = "./public" `, "public/index.html": dedent`

Hello Workers + Assets

`, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(); let { response, cachedETags } = await fetchWithETag( `${url}/index.html`, {} ); const originalETag = response.headers.get("etag"); expect(await response.text()).toBe("

Hello Workers + Assets

"); await helper.seed({ "public/index.html": dedent`

Hello Updated Workers + Assets

`, }); await worker.waitForReload(); ({ response, cachedETags } = await retry( (s) => s.response.status !== 200, async () => { return await fetchWithETag(`${url}/index.html`, cachedETags); } )); expect(await response.text()).toBe( "

Hello Updated Workers + Assets

" ); // expect a new eTag back because the content for this path has changed expect(response.headers.get("etag")).not.toBe(originalETag); }); it(`supports adding new assets during dev session`, async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" compatibility_date = "2023-01-01" [assets] directory = "./public" `, "public/index.html": dedent`

Hello Workers + Assets

`, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(); let { response, cachedETags } = await fetchWithETag( `${url}/index.html`, {} ); expect(await response.text()).toBe("

Hello Workers + Assets

"); await helper.seed({ "public/about.html": dedent`About Workers + Assets`, "public/workers/index.html": dedent`Cloudflare Workers!`, }); await worker.waitForReload(); // re-calculating the asset manifest / reverse assets map might not be // done at this point, so retry until they are available ({ response, cachedETags } = await retry( (s) => s.response.status !== 200, async () => { return await fetchWithETag(`${url}/about.html`, cachedETags); } )); expect(await response.text()).toBe("About Workers + Assets"); ({ response, cachedETags } = await fetchWithETag( `${url}/workers/index.html`, cachedETags )); expect(await response.text()).toBe("Cloudflare Workers!"); // expect 304 for the original asset as the content has not changed ({ response, cachedETags } = await fetchWithETag( `${url}/index.html`, cachedETags )); expect(response.status).toBe(304); }); it(`supports removing existing assets during dev session`, async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" compatibility_date = "2023-01-01" [assets] directory = "./public" `, "public/index.html": dedent`

Hello Workers + Assets

`, "public/about.html": dedent`About Workers + Assets`, "public/workers/index.html": dedent`Cloudflare Workers!`, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(); let { response, cachedETags } = await fetchWithETag( `${url}/index.html`, {} ); expect(await response.text()).toBe("

Hello Workers + Assets

"); ({ response, cachedETags } = await fetchWithETag( `${url}/about.html`, cachedETags )); expect(await response.text()).toBe("About Workers + Assets"); ({ response, cachedETags } = await fetchWithETag( `${url}/workers/index.html`, cachedETags )); expect(await response.text()).toBe("Cloudflare Workers!"); await helper.removeFiles(["public/index.html"]); await worker.waitForReload(); // re-calculating the asset manifest / reverse assets map might not be // done at this point, so retry until they are available ({ response, cachedETags } = await retry( (s) => s.response.status !== 404, async () => { return await fetchWithETag(`${url}/index.html`, cachedETags); } )); expect(response.status).toBe(404); }); it("supports adding new metafiles during dev session", async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" compatibility_date = "2023-01-01" [assets] directory = "./public" `, "public/index.html": dedent`

Hello Workers + Assets

`, "public/foo.html": dedent`

Foo

`, "public/bar.html": dedent`

Bar

`, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(); let response = await fetch(`${url}/index.html`); expect(response.headers.has("X-Custom")).toBeFalsy(); expect(await response.text()).toBe("

Hello Workers + Assets

"); response = await fetch(`${url}/foo`, { redirect: "manual" }); expect(response.status).toBe(200); expect(await response.text()).toBe("

Foo

"); await helper.seed({ "public/_headers": dedent`/\n X-Header: Custom-Value`, "public/_redirects": dedent`/foo /bar`, }); await worker.waitForReload(); // re-calculating the asset manifest / reverse assets map might not be // done at this point, so retry until they are available response = await retry( (r) => r.status !== 302, async () => { return await fetch(`${url}/foo`, { redirect: "manual" }); } ); expect(response.status).toBe(302); expect(response.headers.get("Location")).toBe("/bar"); response = await fetch(`${url}/`); expect(response.headers.get("X-Header")).toBe("Custom-Value"); }); it(`supports modifying the assets directory in wrangler.toml during dev session`, async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" compatibility_date = "2023-01-01" [assets] directory = "./public" `, "public/index.html": dedent`

Hello Workers + Assets

`, }); await helper.seed({ "public2/index.html": dedent`

Hola Workers + Assets

`, "public2/about/index.html": dedent`

Read more about Workers + Assets

`, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(); let { response, cachedETags } = await fetchWithETag( `${url}/index.html`, {} ); expect(await response.text()).toBe("

Hello Workers + Assets

"); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" compatibility_date = "2023-01-01" [assets] directory = "./public2" `, }); await worker.waitForReload(); ({ response, cachedETags } = await retry( (s) => s.response.status !== 200, async () => { return await fetchWithETag(`${url}/index.html`, cachedETags); } )); expect(await response.text()).toBe("

Hola Workers + Assets

"); ({ response, cachedETags } = await fetchWithETag( `${url}/about/index.html`, {} )); expect(await response.text()).toBe( "

Read more about Workers + Assets

" ); }); it(`supports switching from Workers without assets to assets-only Workers during the current dev session`, async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" `, "src/index.ts": dedent` export default { fetch(request) { return new Response("Hello from user Worker!") } }`, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(); let response = await fetch(`${url}/hey`); expect(response.status).toBe(200); expect(await response.text()).toBe("Hello from user Worker!"); response = await fetch(`${url}/index.html`); expect(response.status).toBe(200); expect(await response.text()).toBe("Hello from user Worker!"); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" compatibility_date = "2023-01-01" [assets] directory = "./public" `, "public/index.html": dedent`

Hello Workers + Assets

`, }); await worker.waitForReload(); // verify response from Asset Worker const { status, text } = await retry( (s) => s.text !== "

Hello Workers + Assets

", async () => { const fetchResponse = await fetch(url); return { status: fetchResponse.status, text: await fetchResponse.text(), }; } ); expect(status).toBe(200); expect(text).toBe("

Hello Workers + Assets

"); response = await fetch(`${url}/index.html`); expect(response.status).toBe(200); expect(await response.text()).toBe("

Hello Workers + Assets

"); // verify we no longer get a response from the User Worker response = await fetch(`${url}/hey`); expect(response.status).toBe(404); }); it(`supports switching from Workers without assets to Workers with assets during the current dev session`, async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" `, "src/index.ts": dedent` export default { fetch(request) { return new Response("Hello from user Worker!") } }`, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(); let response = await fetch(`${url}/hey`); expect(response.status).toBe(200); expect(await response.text()).toBe("Hello from user Worker!"); response = await fetch(`${url}/index.html`); expect(response.status).toBe(200); expect(await response.text()).toBe("Hello from user Worker!"); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" [assets] directory = "./public" `, "public/index.html": dedent`

Hello Workers + Assets

`, }); await worker.waitForReload(); // verify response from Asset Worker const { status, text } = await retry( (s) => s.text !== "

Hello Workers + Assets

", async () => { const fetchResponse = await fetch(url); return { status: fetchResponse.status, text: await fetchResponse.text(), }; } ); expect(status).toBe(200); expect(text).toBe("

Hello Workers + Assets

"); response = await fetch(`${url}/index.html`); expect(response.status).toBe(200); expect(await response.text()).toBe("

Hello Workers + Assets

"); // verify response from the User Worker response = await fetch(`${url}/hey`); expect(response.status).toBe(200); expect(await response.text()).toBe("Hello from user Worker!"); }); it(`supports switching from assets-only Workers to Workers with assets during the current dev session`, async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" compatibility_date = "2023-01-01" [assets] directory = "./public" `, "public/index.html": dedent`

Hello Workers + Assets

`, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(); // verify response from Asset Worker let response = await fetch(`${url}/index.html`); expect(response.status).toBe(200); expect(await response.text()).toBe("

Hello Workers + Assets

"); // verify no response from route that will be handled by the // User Worker in the future response = await fetch(`${url}/hey`); expect(response.status).toBe(404); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" [assets] directory = "./public" `, "src/index.ts": dedent` export default { fetch(request) { return new Response("Hello from user Worker!") } }`, }); await worker.waitForReload(); // verify we still get the correct response for the Asset Worker response = await fetch(`${url}/index.html`); expect(response.status).toBe(200); expect(await response.text()).toBe("

Hello Workers + Assets

"); // verify response from User Worker response = await fetch(`${url}/hey`); expect(response.status).toBe(200); expect(await response.text()).toBe("Hello from user Worker!"); }); it(`supports switching from Workers with assets to assets-only Workers during the current dev session`, async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" [assets] directory = "./public" `, "public/index.html": dedent`

Hello Workers + Assets

`, "src/index.ts": dedent` export default { fetch(request) { return new Response("Hello from user Worker!") } }`, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(); // verify response from Asset Worker let response = await fetch(`${url}/index.html`); expect(response.status).toBe(200); expect(await response.text()).toBe("

Hello Workers + Assets

"); // verify response from User Worker response = await fetch(`${url}/hey`); expect(response.status).toBe(200); expect(await response.text()).toBe("Hello from user Worker!"); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" compatibility_date = "2023-01-01" [assets] directory = "./public" `, }); await worker.waitForReload(); // verify we still get the correct response from Asset Worker response = await fetch(`${url}/index.html`); expect(response.status).toBe(200); expect(await response.text()).toBe("

Hello Workers + Assets

"); // verify we no longer get a response from the User Worker response = await fetch(`${url}/hey`); expect(response.status).toBe(404); }); it("debounces runtime restarts when assets are modified", async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" compatibility_date = "2023-01-01" main = "src/index.ts" [assets] directory = "./public" `, "src/index.ts": dedent` export default { async fetch(request) { return new Response("Hello, World!") } } `, "public/index.html": "Hello from Assets", }); const worker = helper.runLongLived("wrangler dev"); const { url } = await worker.waitForReady(); // Modify assets multiple times in quick succession await helper.seed({ "public/a.html": "a", }); await helper.seed({ "public/b.html": "b", }); await helper.seed({ "public/c.html": "c", }); await worker.waitForReload(); // The three changes should be debounced, so only one reload should occur await expect(worker.waitForReload(5_000)).rejects.toThrow(); // now check assets are still fetchable await expect(fetchText(url)).resolves.toBe("Hello from Assets"); }); it(`warns on mounted paths when routes are configured in the configuration file`, async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" compatibility_date = "2023-01-01" [assets] directory = "./public" `, "public/index.html": dedent`

Hello Workers + Assets

`, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(); const { response } = await fetchWithETag(`${url}/index.html`, {}); expect(await response.text()).toBe("

Hello Workers + Assets

"); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" compatibility_date = "2023-01-01" route = "example.com/path/*" [assets] directory = "./public" `, }); await worker.readUntil( /Warning: The following routes will attempt to serve Assets on a configured path:/ ); }); } ); describe.each([{ cmd: "wrangler dev --assets=dist" }])( "Workers + Assets watch mode: $cmd", ({ cmd }) => { it(`supports modifying assets during dev session`, async ({ expect }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" compatibility_date = "2023-01-01" `, "dist/index.html": dedent`

Hello Workers + Assets

`, "dist/about.html": dedent`

Read more about Workers + Assets

`, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(); let { response, cachedETags } = await fetchWithETag( `${url}/index.html`, {} ); const originalETag = response.headers.get("etag"); expect(await response.text()).toBe("

Hello Workers + Assets

"); ({ response, cachedETags } = await fetchWithETag( `${url}/about.html`, cachedETags )); expect(await response.text()).toBe( "

Read more about Workers + Assets

" ); // change + add await helper.seed({ "dist/index.html": dedent`

Hello Updated Workers + Assets

`, "dist/hello.html": dedent`

Hya Workers!

`, }); await worker.waitForReload(); // re-calculating the asset manifest / reverse assets map might not be // done at this point, so retry until they are available ({ response, cachedETags } = await retry( (s) => s.response.status !== 200, async () => { return await fetchWithETag(`${url}/hello.html`, cachedETags); } )); expect(await response.text()).toBe("

Hya Workers!

"); ({ response, cachedETags } = await fetchWithETag( `${url}/index.html`, cachedETags )); expect(await response.text()).toBe( "

Hello Updated Workers + Assets

" ); expect(response.headers.get("etag")).not.toBe(originalETag); // unchanged -> expect 304 ({ response, cachedETags } = await fetchWithETag( `${url}/about.html`, cachedETags )); expect(response.status).toBe(304); // remove await helper.removeFiles(["dist/about.html"]); await worker.waitForReload(); // re-calculating the asset manifest / reverse assets map might not be // done at this point, so retry until they are available ({ response, cachedETags } = await retry( (s) => s.response.status !== 404, async () => { return await fetchWithETag(`${url}/about.html`, cachedETags); } )); expect(response.status).toBe(404); }); it(`supports switching from assets-only Workers to Workers with assets during the current dev session`, async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" compatibility_date = "2023-01-01" `, "dist/index.html": dedent`

Hello Workers + Assets

`, "src/index.ts": dedent` export default { fetch(request) { return new Response("Hello from user Worker!") } }`, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(); // verify response from Asset Worker let response = await fetch(`${url}/index.html`); expect(response.status).toBe(200); expect(await response.text()).toBe("

Hello Workers + Assets

"); // verify no response from route that will be handled by the // User Worker in the future response = await fetch(`${url}/hey`); expect(response.status).toBe(404); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" `, }); await worker.waitForReload(); // verify response from Asset Worker response = await fetch(`${url}/index.html`); expect(response.status).toBe(200); expect(await response.text()).toBe("

Hello Workers + Assets

"); // verify response from User Worker response = await fetch(`${url}/hey`); expect(response.status).toBe(200); expect(await response.text()).toBe("Hello from user Worker!"); }); it(`supports switching from Workers with assets to assets-only Workers during the current dev session`, async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" `, "dist/index.html": dedent`

Hello Workers + Assets

`, "src/index.ts": dedent` export default { fetch(request) { return new Response("Hello from user Worker!") } }`, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(); // verify response from Asset Worker let response = await fetch(`${url}/index.html`); expect(response.status).toBe(200); expect(await response.text()).toBe("

Hello Workers + Assets

"); // verify response from User Worker response = await fetch(`${url}/hey`); expect(response.status).toBe(200); expect(await response.text()).toBe("Hello from user Worker!"); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" compatibility_date = "2023-01-01" `, }); await worker.waitForReload(); response = await fetch(`${url}/index.html`); // verify response from Asset expect(response.status).toBe(200); expect(await response.text()).toBe("

Hello Workers + Assets

"); // verify no response from User Worker response = await fetch(`${url}/hey`); expect(response.status).toBe(404); }); it(`warns on mounted paths when routes are configured in the configuration file`, async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" compatibility_date = "2023-01-01" `, "dist/index.html": dedent`

Hello Workers + Assets

`, }); const worker = helper.runLongLived(cmd); const { url } = await worker.waitForReady(); const { response } = await fetchWithETag(`${url}/index.html`, {}); expect(await response.text()).toBe("

Hello Workers + Assets

"); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" compatibility_date = "2023-01-01" route = "example.com/path/*" `, }); await worker.readUntil( /Warning: The following routes will attempt to serve Assets on a configured path:/ ); }); } ); }); describe("email local dev", () => { it("should save file on reply", async ({ expect }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2025-03-17" `, "src/index.ts": dedent` import { EmailMessage } from "cloudflare:email"; export default { async email(emailMessage) { await emailMessage.reply( new EmailMessage( "someone-else@example.com", "someone@example.com", \`From: someone else To: someone In-Reply-To: Message-ID: MIME-Version: 1.0 Content-Type: text/plain This is a random email body. \`) ); } } `, }); const worker = helper.runLongLived("wrangler dev"); const { url } = await worker.waitForReady(); const response = await fetch( `${url}/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com`, { body: dedent` From: someone To: someone else MIME-Version: 1.0 Message-ID: Content-Type: text/plain This is a random email body. `, method: "POST", } ); expect(response.status).toBe(200); await waitFor(() => { expect(worker.currentOutput).toContain( "Email handler replied to sender with the following message:" ); }); const pathRegexp = new RegExp( "Email handler replied to sender with the following message:\\s*(\\S*)" ); const maybeReplyPath = await vi.waitUntil( () => pathRegexp.exec(stripVTControlCharacters(worker.currentOutput))?.[1], { interval: 100, timeout: 5000 } ); expect(await readFile(maybeReplyPath, "utf-8")).toMatchInlineSnapshot(` "References: From: someone else To: someone In-Reply-To: Message-ID: MIME-Version: 1.0 Content-Type: text/plain This is a random email body. " `); }); it("should print reject with reason", async ({ expect }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2025-03-17" `, "src/index.ts": dedent` import { EmailMessage } from "cloudflare:email"; export default { async email(emailMessage) { await emailMessage.setReject('I dont like this email') } }`, }); const worker = helper.runLongLived("wrangler dev"); const { url } = await worker.waitForReady(); const response = await fetch( `${url}/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com`, { body: `From: someone To: someone else MIME-Version: 1.0 Message-ID: Content-Type: text/plain This is a random email body. `, method: "POST", } ); expect(await response.text()).toMatchInlineSnapshot( `"Worker rejected email with the following reason: I dont like this email"` ); expect(response.status).toBe(400); }); it("should print forward email", async ({ expect }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2025-03-17" `, "src/index.ts": dedent` import { EmailMessage } from "cloudflare:email"; export default { async email(emailMessage) { await emailMessage.forward('mark.s@example.com') } }`, }); const worker = helper.runLongLived("wrangler dev"); const { url } = await worker.waitForReady(); const response = await fetch( `${url}/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com`, { body: `From: someone To: someone else MIME-Version: 1.0 Message-ID: Content-Type: text/plain This is a random email body. `, method: "POST", } ); expect(response.status).toBe(200); await waitFor(() => { expect(worker.currentOutput).toContain( `Email handler forwarded message with` ); expect(worker.currentOutput).toContain(`rcptTo: mark.s@example.com`); }); }); it("should save file on send_email", async ({ expect }) => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2025-03-17" send_email = [{name = "SEND_EMAIL"}] `, "src/index.ts": dedent` import { EmailMessage } from "cloudflare:email"; export default { async fetch(request, env, ctx) { const url = new URL(request.url); await env.SEND_EMAIL.send(new EmailMessage( url.searchParams.get("from"), url.searchParams.get("to"), request.body )) return new Response("ok") }, };`, }); const worker = helper.runLongLived("wrangler dev"); const { url } = await worker.waitForReady(); const response = await fetch( `${url}?from=someone@example.com&to=someone-else@example.com`, { body: `From: someone To: someone else Message-ID: MIME-Version: 1.0 Content-Type: text/plain This is a random email body. `, method: "POST", } ); expect(response.status).toBe(200); await waitFor(() => expect(worker.currentOutput).toContain( "send_email binding called with the following message" ) ); const pathRegexp = new RegExp( "send_email binding called with the following message:\\s*(\\S*)" ); const maybeReplyPath = await vi.waitUntil( () => pathRegexp.exec(stripVTControlCharacters(worker.currentOutput))?.[1], { interval: 100, timeout: 5000 } ); expect(await readFile(maybeReplyPath, "utf-8")).toMatchInlineSnapshot(` "From: someone To: someone else Message-ID: MIME-Version: 1.0 Content-Type: text/plain This is a random email body. " `); }); }); describe(".env support in local dev", () => { const seedFiles = { "wrangler.jsonc": JSON.stringify({ name: workerName, main: "src/index.ts", compatibility_date: "2025-07-01", vars: { WRANGLER_ENV_VAR_0: "default-0", WRANGLER_ENV_VAR_1: "default-1", WRANGLER_ENV_VAR_2: "default-2", WRANGLER_ENV_VAR_3: "default-3", }, }), "src/index.ts": dedent` export default { fetch(request, env) { return new Response(JSON.stringify(env, null, 2)); } } `, }; it("should load environment variables from .env file", async ({ expect }) => { const helper = new WranglerE2ETestHelper(); await helper.seed(seedFiles); await helper.seed({ ".env": dedent` WRANGLER_ENV_VAR_1=env-1 WRANGLER_ENV_VAR_2=env-2 `, }); const worker = helper.runLongLived("wrangler dev"); const { url } = await worker.waitForReady(); expect(await (await fetch(url)).text()).toMatchInlineSnapshot(` "{ "WRANGLER_ENV_VAR_0": "default-0", "WRANGLER_ENV_VAR_3": "default-3", "WRANGLER_ENV_VAR_1": "env-1", "WRANGLER_ENV_VAR_2": "env-2" }" `); }); it("should not load local dev variables from .env files if there is a .dev.vars file", async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed(seedFiles); await helper.seed({ ".env": dedent` WRANGLER_ENV_VAR_1=env-1 WRANGLER_ENV_VAR_2=env-2 `, }); await helper.seed({ ".dev.vars": dedent` WRANGLER_ENV_VAR_1=dev-vars-1 WRANGLER_ENV_VAR_2=dev-vars-2 `, }); const worker = helper.runLongLived("wrangler dev"); const { url } = await worker.waitForReady(); expect(await (await fetch(url)).text()).toMatchInlineSnapshot(` "{ "WRANGLER_ENV_VAR_0": "default-0", "WRANGLER_ENV_VAR_3": "default-3", "WRANGLER_ENV_VAR_1": "dev-vars-1", "WRANGLER_ENV_VAR_2": "dev-vars-2" }" `); }); it("should not load dev variables from .env files if CLOUDFLARE_LOAD_DEV_VARS_FROM_DOT_ENV is set to false", async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed(seedFiles); await helper.seed({ ".env": dedent` WRANGLER_ENV_VAR_1=env-1 WRANGLER_ENV_VAR_2=env-2 `, }); const worker = helper.runLongLived("wrangler dev", { env: { ...process.env, CLOUDFLARE_LOAD_DEV_VARS_FROM_DOT_ENV: "false" }, }); const { url } = await worker.waitForReady(); expect(await (await fetch(url)).text()).toMatchInlineSnapshot(` "{ "WRANGLER_ENV_VAR_0": "default-0", "WRANGLER_ENV_VAR_1": "default-1", "WRANGLER_ENV_VAR_2": "default-2", "WRANGLER_ENV_VAR_3": "default-3" }" `); }); it("should load environment variables from .env.staging if it exists and --env=staging", async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed(seedFiles); await helper.seed({ ".env.staging": dedent` WRANGLER_ENV_VAR_2=staging-2 WRANGLER_ENV_VAR_3=staging-3 `, }); const worker = helper.runLongLived("wrangler dev --env=staging"); const { url } = await worker.waitForReady(); expect(await (await fetch(url)).text()).toMatchInlineSnapshot(` "{ "WRANGLER_ENV_VAR_0": "default-0", "WRANGLER_ENV_VAR_1": "default-1", "WRANGLER_ENV_VAR_2": "staging-2", "WRANGLER_ENV_VAR_3": "staging-3" }" `); }); it("should prefer to load environment variables from .env.staging over .env, if --env=staging", async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed(seedFiles); await helper.seed({ ".env": dedent` WRANGLER_ENV_VAR_1=env-1 WRANGLER_ENV_VAR_2=env-2 `, }); await helper.seed({ ".env.staging": dedent` WRANGLER_ENV_VAR_2=staging-2 WRANGLER_ENV_VAR_3=staging-3 `, }); const worker = helper.runLongLived("wrangler dev --env=staging"); const { url } = await worker.waitForReady(); expect(await (await fetch(url)).text()).toMatchInlineSnapshot(` "{ "WRANGLER_ENV_VAR_0": "default-0", "WRANGLER_ENV_VAR_1": "env-1", "WRANGLER_ENV_VAR_2": "staging-2", "WRANGLER_ENV_VAR_3": "staging-3" }" `); }); it("should load environment variables from .env file if --env=xxx and .env.xxx does not exist", async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed(seedFiles); await helper.seed({ ".env": dedent` WRANGLER_ENV_VAR_1=env-1 WRANGLER_ENV_VAR_2=env-2 `, }); await helper.seed({ ".env.staging": dedent` WRANGLER_ENV_VAR_2=staging-2 WRANGLER_ENV_VAR_3=staging-3 `, }); const worker = helper.runLongLived("wrangler dev --env=xxx"); const { url } = await worker.waitForReady(); expect(await (await fetch(url)).text()).toMatchInlineSnapshot(` "{ "WRANGLER_ENV_VAR_0": "default-0", "WRANGLER_ENV_VAR_3": "default-3", "WRANGLER_ENV_VAR_1": "env-1", "WRANGLER_ENV_VAR_2": "env-2" }" `); }); it("should prefer to load environment variables from .env.local over .env", async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed(seedFiles); await helper.seed({ ".env": dedent` WRANGLER_ENV_VAR_1=env-1 WRANGLER_ENV_VAR_2=env-2 `, }); await helper.seed({ ".env.local": dedent` WRANGLER_ENV_VAR_2=local-2 WRANGLER_ENV_VAR_3=local-3 `, }); const worker = helper.runLongLived("wrangler dev"); const { url } = await worker.waitForReady(); expect(await (await fetch(url)).text()).toMatchInlineSnapshot(` "{ "WRANGLER_ENV_VAR_0": "default-0", "WRANGLER_ENV_VAR_1": "env-1", "WRANGLER_ENV_VAR_2": "local-2", "WRANGLER_ENV_VAR_3": "local-3" }" `); }); it("should prefer to load environment variables from .env.staging.local over .env.staging, etc", async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed(seedFiles); await helper.seed({ ".env": dedent` WRANGLER_ENV_VAR_1=env-1 WRANGLER_ENV_VAR_2=env-2 `, }); await helper.seed({ ".env.local": dedent` WRANGLER_ENV_VAR_2=local-2 WRANGLER_ENV_VAR_3=local-3 `, }); await helper.seed({ ".env.staging": dedent` WRANGLER_ENV_VAR_3=staging-3 WRANGLER_ENV_VAR_4=staging-4 `, }); await helper.seed({ ".env.staging.local": dedent` WRANGLER_ENV_VAR_4=staging-local-4 WRANGLER_ENV_VAR_5=staging-local-5 `, }); const worker = helper.runLongLived("wrangler dev --env=staging"); const { url } = await worker.waitForReady(); expect(await (await fetch(url)).text()).toMatchInlineSnapshot(` "{ "WRANGLER_ENV_VAR_0": "default-0", "WRANGLER_ENV_VAR_1": "env-1", "WRANGLER_ENV_VAR_2": "local-2", "WRANGLER_ENV_VAR_3": "staging-3", "WRANGLER_ENV_VAR_4": "staging-local-4", "WRANGLER_ENV_VAR_5": "staging-local-5" }" `); }); it("should load environment variables from process.env if CLOUDFLARE_INCLUDE_PROCESS_ENV is true", async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed(seedFiles); await helper.seed({ ".env": dedent` WRANGLER_ENV_VAR_1=env-1 WRANGLER_ENV_VAR_2=env-2 `, }); const worker = helper.runLongLived("wrangler dev", { env: { CLOUDFLARE_INCLUDE_PROCESS_ENV: "true", ...process.env }, }); const { url } = await worker.waitForReady(); // We could dump out all the bindings but that would be a lot of noise, and also may change between OSes and runs. // Instead, we know that the `CLOUDFLARE_INCLUDE_PROCESS_ENV` variable should be present, so we just check for that. expect(await (await fetch(url)).text()).contains( '"CLOUDFLARE_INCLUDE_PROCESS_ENV": "true"' ); expect(await (await fetch(url)).text()).contains( '"WRANGLER_ENV_VAR_0": "default-0"' ); expect(await (await fetch(url)).text()).contains( '"WRANGLER_ENV_VAR_1": "env-1"' ); }); it("should load environment variables from the .env files pointed to by `--env-file`", async ({ expect, }) => { const helper = new WranglerE2ETestHelper(); await helper.seed(seedFiles); await helper.seed({ ".env": dedent` WRANGLER_ENV_VAR_1=env-1 WRANGLER_ENV_VAR_2=env-2 `, }); await helper.seed({ ".env.local": dedent` WRANGLER_ENV_VAR_2=local-2 WRANGLER_ENV_VAR_3=local-3 `, }); await helper.seed({ "other/.env": dedent` WRANGLER_ENV_VAR_1=other-env-1 WRANGLER_ENV_VAR_2=other-env-2 `, }); await helper.seed({ "other/.env.local": dedent` WRANGLER_ENV_VAR_2=other-local-2 WRANGLER_ENV_VAR_3=other-local-3 `, }); const worker = helper.runLongLived( "wrangler dev --env-file=other/.env --env-file=other/.env.local" ); const { url } = await worker.waitForReady(); expect(await (await fetch(url)).text()).toMatchInlineSnapshot(` "{ "WRANGLER_ENV_VAR_0": "default-0", "WRANGLER_ENV_VAR_1": "other-env-1", "WRANGLER_ENV_VAR_2": "other-local-2", "WRANGLER_ENV_VAR_3": "other-local-3" }" `); }); });