From 20703551d9bb058c7b94bb92254f356382f82b1e Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Mon, 16 Mar 2026 10:37:42 +0000 Subject: [PATCH 01/18] [wrangler] use consistent waitFor/waitForLong helpers in e2e tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract shared waitFor() and waitForLong() helpers wrapping vi.waitFor() with tuned defaults - waitFor(): 100ms interval, 5s timeout — for polling synchronous state (e.g. console output) - waitForLong(): 500ms interval, 10s timeout — for polling HTTP endpoints - Add ESLint rule to enforce using these helpers instead of bare vi.waitFor() in e2e tests - Migrate all e2e tests to use the shared helpers --- .../wrangler/e2e/assets-multiworker.test.ts | 250 +++++++----------- packages/wrangler/e2e/containers.dev.test.ts | 47 ++-- packages/wrangler/e2e/deployments.test.ts | 83 +++--- packages/wrangler/e2e/dev-registry.test.ts | 161 ++++------- packages/wrangler/e2e/dev.test.ts | 197 +++++++------- .../wrangler/e2e/helpers/e2e-wrangler-test.ts | 47 ++-- packages/wrangler/e2e/helpers/fetch-json.ts | 29 ++ packages/wrangler/e2e/helpers/wait-for.ts | 34 +++ packages/wrangler/e2e/multiworker-dev.test.ts | 164 +++++------- packages/wrangler/e2e/pages-dev.test.ts | 52 ++-- .../dev-remote-bindings.test.ts | 67 +++-- .../e2e/start-worker-auth-opts.test.ts | 5 +- packages/wrangler/e2e/startWorker.test.ts | 4 +- .../wrangler/e2e/unenv-preset/preset.test.ts | 40 +-- 14 files changed, 558 insertions(+), 622 deletions(-) create mode 100644 packages/wrangler/e2e/helpers/fetch-json.ts create mode 100644 packages/wrangler/e2e/helpers/wait-for.ts diff --git a/packages/wrangler/e2e/assets-multiworker.test.ts b/packages/wrangler/e2e/assets-multiworker.test.ts index e1560052fc..d9ce3ac15d 100644 --- a/packages/wrangler/e2e/assets-multiworker.test.ts +++ b/packages/wrangler/e2e/assets-multiworker.test.ts @@ -1,37 +1,19 @@ import dedent from "ts-dedent"; import { fetch } from "undici"; -import { beforeEach, describe, it, vi } from "vitest"; +import { beforeEach, describe, it } from "vitest"; import { WranglerE2ETestHelper } from "./helpers/e2e-wrangler-test"; +import { fetchJson } from "./helpers/fetch-json"; import { fetchText } from "./helpers/fetch-text"; import { generateResourceName } from "./helpers/generate-resource-name"; import { seed as baseSeed, makeRoot } from "./helpers/setup"; -import type { RequestInit } from "undici"; - -async function fetchJson(url: string, info?: RequestInit): Promise { - return vi.waitFor( - async () => { - const text: string = await fetch(url, { - headers: { "MF-Disable-Pretty-Error": "true" }, - ...info, - }).then((r) => r.text()); - try { - return JSON.parse(text) as T; - } catch (cause) { - const err = new Error(`Failed to parse JSON from:\n${text}`); - err.cause = cause; - throw err; - } - }, - { timeout: 5_000, interval: 250 } - ); -} +import { waitForLong } from "./helpers/wait-for"; async function startWorkersDevRegistry( wranglerDev: string, helper: WranglerE2ETestHelper, assetWorker: string, regularWorker: string, - regularWorkerFirst = true + regularWorkerFirst = true, ) { const workerA = helper.runLongLived(wranglerDev, { cwd: regularWorkerFirst ? assetWorker : regularWorker, @@ -51,11 +33,11 @@ async function startWorkersMultiworker( helper: WranglerE2ETestHelper, assetWorker: string, regularWorker: string, - regularWorkerFirst = true + regularWorkerFirst = true, ) { const worker = helper.runLongLived( `${wranglerDev} -c wrangler.toml -c ${regularWorkerFirst ? assetWorker : regularWorker}/wrangler.toml`, - { cwd: regularWorkerFirst ? regularWorker : assetWorker } + { cwd: regularWorkerFirst ? regularWorker : assetWorker }, ); const { url } = await worker.waitForReady(5_000); return url; @@ -87,7 +69,7 @@ describe.each( style: MultiworkerStyle; start: typeof startWorkersDevRegistry | typeof startWorkersMultiworker; wranglerDev: string; - }[] + }[], )( "workers with assets ($style, $wranglerDev) ", async ({ start, style, wranglerDev }) => { @@ -226,17 +208,17 @@ describe.each( helper, assetWorker, regularWorker, - false + false, ); await expect(fetchText(`${url}/asset`)).resolves.toBe( - "

have an asset directly

" + "

have an asset directly

", ); await expect(fetchText(`${url}/asset-via-binding`)).resolves.toBe( - "

have an asset via a binding

" + "

have an asset via a binding

", ); await expect( - fetch(`${url}/worker`).then((r) => r.text()) + fetch(`${url}/worker`).then((r) => r.text()), ).resolves.toBe("hello world from a worker with assets"); }); @@ -248,21 +230,17 @@ describe.each( helper, assetWorker, regularWorker, - false + false, ); - await vi.waitFor( - async () => - await expect( - fetch(`${url}/hello-from-dee`).then((r) => r.text()) - ).resolves.toBe("hello world from dee"), - { interval: 1000, timeout: 5_000 } + await waitForLong(() => + expect( + fetch(`${url}/hello-from-dee`).then((r) => r.text()), + ).resolves.toBe("hello world from dee"), ); - await vi.waitFor( - async () => - await expect(fetchText(`${url}/count`)).resolves.toBe("6"), - { interval: 1000, timeout: 10_000 } + await waitForLong(() => + expect(fetchText(`${url}/count`)).resolves.toBe("6"), ); }); @@ -274,28 +252,22 @@ describe.each( helper, assetWorker, regularWorker, - false + false, ); - await vi.waitFor( - async () => - await expect(fetchText(`${url}/do-rpc`)).resolves.toBe( - "Hello through DO RPC" - ), - { interval: 1000, timeout: 10_000 } - ); - await vi.waitFor( - async () => - await expect( - fetchJson(`${url}/do`, { - headers: { - "X-Reset-Count": "true", - }, - }) - ).resolves.toMatchObject({ count: 1 }), - { interval: 1000, timeout: 10_000 } + await waitForLong(() => + expect(fetchText(`${url}/do-rpc`)).resolves.toBe( + "Hello through DO RPC", + ), ); - } + await expect( + fetchJson(`${url}/do`, { + headers: { + "X-Reset-Count": "true", + }, + }), + ).resolves.toMatchObject({ count: 1 }); + }, ); }); @@ -337,14 +309,12 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker + regularWorker, ); - await vi.waitFor( - async () => - await expect(fetchText(`${url}/asset`)).resolves.toBe( - "

have an asset directly

" - ), - { interval: 1000, timeout: 5_000 } + await waitForLong(() => + expect(fetchText(`${url}/asset`)).resolves.toBe( + "

have an asset directly

", + ), ); }); @@ -353,14 +323,12 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker + regularWorker, ); - await vi.waitFor( - async () => - await expect(fetch(`${url}/not-an-asset`)).resolves.toMatchObject({ - status: 404, - }), - { interval: 1000, timeout: 5_000 } + await waitForLong(() => + expect(fetch(`${url}/not-an-asset`)).resolves.toMatchObject({ + status: 404, + }), ); }); @@ -369,15 +337,13 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker + regularWorker, ); - await vi.waitFor( - async () => - await expect(fetchText(`${url}/rpc`)).resolves.toContain( - // Cannot call RPC methods on assets-only workers - "The RPC receiver does not implement the method" - ), - { interval: 1000, timeout: 5_000 } + await waitForLong(() => + expect(fetchText(`${url}/rpc`)).resolves.toContain( + // Cannot call RPC methods on assets-only workers + "The RPC receiver does not implement the method", + ), ); }); }); @@ -467,14 +433,12 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker + regularWorker, ); - await vi.waitFor( - async () => - await expect(fetchText(`${url}/asset`)).resolves.toBe( - "

have an asset directly

" - ), - { interval: 1000, timeout: 5_000 } + await waitForLong(() => + expect(fetchText(`${url}/asset`)).resolves.toBe( + "

have an asset directly

", + ), ); }); @@ -483,14 +447,12 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker + regularWorker, ); - await vi.waitFor( - async () => - await expect(fetchText(`${url}/not-an-asset`)).resolves.toBe( - "hello world from a worker with assets" - ), - { interval: 1000, timeout: 5_000 } + await waitForLong(() => + expect(fetchText(`${url}/not-an-asset`)).resolves.toBe( + "hello world from a worker with assets", + ), ); }); @@ -499,14 +461,12 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker + regularWorker, ); - await vi.waitFor( - async () => - await expect(fetchText(`${url}/asset-via-binding`)).resolves.toBe( - "

have an asset via a binding

" - ), - { interval: 1000, timeout: 5_000 } + await waitForLong(() => + expect(fetchText(`${url}/asset-via-binding`)).resolves.toBe( + "

have an asset via a binding

", + ), ); }); @@ -515,12 +475,10 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker + regularWorker, ); - await vi.waitFor( - async () => - await expect(fetchText(`${url}/rpc`)).resolves.toBe("2"), - { interval: 1000, timeout: 5_000 } + await waitForLong(() => + expect(fetchText(`${url}/rpc`)).resolves.toBe("2"), ); }); }); @@ -549,14 +507,12 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker + regularWorker, ); - await vi.waitFor( - async () => - await expect(fetchText(`${url}/asset`)).resolves.toBe( - "

have an asset directly

" - ), - { interval: 1000, timeout: 5_000 } + await waitForLong(() => + expect(fetchText(`${url}/asset`)).resolves.toBe( + "

have an asset directly

", + ), ); }); @@ -565,14 +521,12 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker + regularWorker, ); - await vi.waitFor( - async () => - await expect(fetchText(`${url}/not-an-asset`)).resolves.toBe( - "hello world from a worker with assets" - ), - { interval: 1000, timeout: 5_000 } + await waitForLong(() => + expect(fetchText(`${url}/not-an-asset`)).resolves.toBe( + "hello world from a worker with assets", + ), ); }); @@ -581,14 +535,12 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker + regularWorker, ); - await vi.waitFor( - async () => - await expect(fetchText(`${url}/asset-via-binding`)).resolves.toBe( - "

have an asset via a binding

" - ), - { interval: 1000, timeout: 5_000 } + await waitForLong(() => + expect(fetchText(`${url}/asset-via-binding`)).resolves.toBe( + "

have an asset via a binding

", + ), ); }); @@ -597,12 +549,10 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker + regularWorker, ); - await vi.waitFor( - async () => - await expect(fetchText(`${url}/rpc`)).resolves.toBe("2"), - { interval: 1000, timeout: 5_000 } + await waitForLong(() => + expect(fetchText(`${url}/rpc`)).resolves.toBe("2"), ); }); }); @@ -645,14 +595,12 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker + regularWorker, ); - await vi.waitFor( - async () => - await expect(fetchText(`${url}/asset`)).resolves.toBe( - "hello world from a worker with assets" - ), - { interval: 1000, timeout: 5_000 } + await waitForLong(() => + expect(fetchText(`${url}/asset`)).resolves.toBe( + "hello world from a worker with assets", + ), ); }); @@ -661,14 +609,12 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker + regularWorker, ); - await vi.waitFor( - async () => - await expect(fetchText(`${url}/asset-via-binding`)).resolves.toBe( - "

have an asset via a binding

" - ), - { interval: 1000, timeout: 5_000 } + await waitForLong(() => + expect(fetchText(`${url}/asset-via-binding`)).resolves.toBe( + "

have an asset via a binding

", + ), ); }); @@ -677,15 +623,13 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker + regularWorker, ); - await vi.waitFor( - async () => - await expect(fetchText(`${url}/rpc`)).resolves.toBe("2"), - { interval: 1000, timeout: 5_000 } + await waitForLong(() => + expect(fetchText(`${url}/rpc`)).resolves.toBe("2"), ); }); }); }); - } + }, ); diff --git a/packages/wrangler/e2e/containers.dev.test.ts b/packages/wrangler/e2e/containers.dev.test.ts index 53d8672927..c5fd0853ed 100644 --- a/packages/wrangler/e2e/containers.dev.test.ts +++ b/packages/wrangler/e2e/containers.dev.test.ts @@ -10,6 +10,7 @@ import { dedent } from "../src/utils/dedent"; import { CLOUDFLARE_ACCOUNT_ID } from "./helpers/account-id"; import { WranglerE2ETestHelper } from "./helpers/e2e-wrangler-test"; import { generateResourceName } from "./helpers/generate-resource-name"; +import { waitFor, waitForLong } from "./helpers/wait-for"; const imageSource = ["pull", "build"] as const; @@ -244,7 +245,7 @@ for (const source of imageSource) { const worker = helper.runLongLived("wrangler dev"); const ready = await worker.waitForReady(); - await vi.waitFor(async () => { + await waitFor(async () => { const response = await fetch(`${ready.url}/status`); expect(response.status).toBe(200); const status = await response.json(); @@ -256,24 +257,21 @@ for (const source of imageSource) { expect(response.status).toBe(200); expect(text).toBe("Container create request sent..."); - await vi.waitFor(async () => { + await waitFor(async () => { response = await fetch(`${ready.url}/status`); expect(response.status).toBe(200); const status = await response.json(); expect(status).toBe(true); }); - await vi.waitFor( - async () => { - response = await fetch(`${ready.url}/fetch`, { - signal: AbortSignal.timeout(3_000), - headers: { "MF-Disable-Pretty-Error": "true" }, - }); - text = await response.text(); - expect(text).toBe("Hello World! Have an env var! I'm an env var!"); - }, - { timeout: 5_000 } - ); + await waitFor(async () => { + response = await fetch(`${ready.url}/fetch`, { + signal: AbortSignal.timeout(3_000), + headers: { "MF-Disable-Pretty-Error": "true" }, + }); + text = await response.text(); + expect(text).toBe("Hello World! Have an env var! I'm an env var!"); + }); // Set up egress HTTP interception so the container can call back to the worker response = await fetch(`${ready.url}/setup-intercept`, { @@ -285,18 +283,15 @@ for (const source of imageSource) { expect(text).toBe("Intercept setup done"); // Fetch through the container's /intercept route which curls back to the worker - await vi.waitFor( - async () => { - response = await fetch(`${ready.url}/fetch-intercept`, { - signal: AbortSignal.timeout(5_000), - headers: { "MF-Disable-Pretty-Error": "true" }, - }); - text = await response.text(); - expect(response.status).toBe(200); - expect(text).toBe("hello from worker"); - }, - { timeout: 10_000 } - ); + await waitForLong(async () => { + response = await fetch(`${ready.url}/fetch-intercept`, { + signal: AbortSignal.timeout(5_000), + headers: { "MF-Disable-Pretty-Error": "true" }, + }); + text = await response.text(); + expect(response.status).toBe(200); + expect(text).toBe("hello from worker"); + }); // Check that a container is running using `docker ps` const ids = getContainerIds("e2econtainer"); @@ -334,7 +329,7 @@ for (const source of imageSource) { const ready = await worker.waitForReady(); // check that the container can still start - await vi.waitFor(async () => { + await waitFor(async () => { const response = await fetch(`${ready.url}/status`); expect(response.status).toBe(200); const status = await response.json(); diff --git a/packages/wrangler/e2e/deployments.test.ts b/packages/wrangler/e2e/deployments.test.ts index c2edc88e5f..3ebbd528a1 100644 --- a/packages/wrangler/e2e/deployments.test.ts +++ b/packages/wrangler/e2e/deployments.test.ts @@ -11,7 +11,6 @@ import { describe, expect, it, - vi, } from "vitest"; /* eslint-enable no-restricted-imports */ import { CLOUDFLARE_ACCOUNT_ID } from "./helpers/account-id"; @@ -19,6 +18,7 @@ import { WranglerE2ETestHelper } from "./helpers/e2e-wrangler-test"; import { generateResourceName } from "./helpers/generate-resource-name"; import { normalizeOutput, validateAssetUploadLogs } from "./helpers/normalize"; import { retry } from "./helpers/retry"; +import { waitForLong } from "./helpers/wait-for"; const TIMEOUT = 50_000; @@ -65,7 +65,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( const response = await retry( (resp) => !resp.ok, - async () => await fetch(deployedUrl) + async () => await fetch(deployedUrl), ); await expect(response.text()).resolves.toEqual("Hello World!"); }); @@ -100,7 +100,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( const response = await retry( (resp) => !resp.ok, - async () => await fetch(deployedUrl) + async () => await fetch(deployedUrl), ); await expect(response.text()).resolves.toEqual("Updated Worker!"); }); @@ -129,7 +129,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( it("rolls back", async ({ expect }) => { const output = await helper.run( - `wrangler rollback --message "A test message"` + `wrangler rollback --message "A test message"`, ); expect(normalizeOutput(output.stdout)).toMatchInlineSnapshot(` "├ Fetching latest deployment @@ -195,7 +195,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( Message: -" `); }); - } + }, ); type AssetTestCase = { @@ -223,7 +223,7 @@ function generateInitialAssets(workerName: string) { async function checkAssets(testCases: AssetTestCase[], deployedUrl: string) { for (const testCase of testCases) { - await vi.waitFor( + await waitForLong( async () => { const r = await fetch(new URL(testCase.path, deployedUrl)); const text = await r.text(); @@ -232,25 +232,22 @@ async function checkAssets(testCases: AssetTestCase[], deployedUrl: string) { if (testCase.content) { expect( text, - `expected content for ${testCase.path} to be ${testCase.content}` + `expected content for ${testCase.path} to be ${testCase.content}`, ).toContain(testCase.content); } if (testCase.redirect) { expect( new URL(url).pathname, - `expected redirect for ${testCase.path} to be ${testCase.redirect}` + `expected redirect for ${testCase.path} to be ${testCase.redirect}`, ).toEqual(new URL(testCase.redirect, deployedUrl).pathname); } else { expect( new URL(url).pathname, - `unexpected pathname for ${testCase.path}` + `unexpected pathname for ${testCase.path}`, ).toEqual(new URL(testCase.path, deployedUrl).pathname); } }, - { - interval: 1_000, - timeout: 40_000, - } + { timeout: 40_000 }, ); } } @@ -323,7 +320,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)("Workers + Assets deployment", () => { const r = await fetch(new URL("/try-404", deployedUrl)); const temp = { text: await r.text(), status: r.status }; return temp; - } + }, ); expect(text).toBeFalsy(); }); @@ -396,7 +393,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)("Workers + Assets deployment", () => { const r = await fetch(new URL("/try-404", deployedUrl)); const temp = { text: await r.text(), status: r.status }; return temp; - } + }, ); expect(text).toContain("

404.html

"); }); @@ -422,7 +419,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)("Workers + Assets deployment", () => { validateAssetUploadLogs( output, ["/404.html", "/index.html", "/[boop].html"], - { includeDebug: true } + { includeDebug: true }, ); const deployedUrl = getDeployedUrl(output); @@ -455,7 +452,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)("Workers + Assets deployment", () => { const r = await fetch(new URL("/try-404", deployedUrl)); const temp = { text: await r.text(), status: r.status }; return temp; - } + }, ); expect(text).toBeFalsy(); }); @@ -596,10 +593,10 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)("Workers + Assets deployment", () => { afterEach(async () => { // clean up dispatch Worker await helper.bestEffortRun( - `wrangler delete -c dispatch-worker/wrangler.toml` + `wrangler delete -c dispatch-worker/wrangler.toml`, ); await helper.bestEffortRun( - `wrangler dispatch-namespace delete ${dispatchNamespaceName}` + `wrangler dispatch-namespace delete ${dispatchNamespaceName}`, ); }); @@ -618,16 +615,16 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)("Workers + Assets deployment", () => { // create a dispatch namespace && verify output let output = await helper.run( - `wrangler dispatch-namespace create ${dispatchNamespaceName}` + `wrangler dispatch-namespace create ${dispatchNamespaceName}`, ); let normalizedStdout = normalizeOutput(output.stdout); expect(normalizedStdout).toContain( - `Created dispatch namespace "tmp-e2e-dispatch-00000000-0000-0000-0000-000000000000" with ID "00000000-0000-0000-0000-000000000000"` + `Created dispatch namespace "tmp-e2e-dispatch-00000000-0000-0000-0000-000000000000" with ID "00000000-0000-0000-0000-000000000000"`, ); // upload user Worker to the dispatch namespace && verify output output = await helper.run( - `wrangler deploy --dispatch-namespace ${dispatchNamespaceName}` + `wrangler deploy --dispatch-namespace ${dispatchNamespaceName}`, ); validateAssetUploadLogs(output, [ "/404.html", @@ -637,7 +634,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)("Workers + Assets deployment", () => { // deploy dispatch Worker && verify output output = await helper.run( - `wrangler deploy -c dispatch-worker/wrangler.toml` + `wrangler deploy -c dispatch-worker/wrangler.toml`, ); normalizedStdout = normalizeOutput(output.stdout); expect(normalizedStdout).toEqual(`Total Upload: xx KiB / gzip: xx KiB @@ -679,7 +676,7 @@ Current Version ID: 00000000-0000-0000-0000-000000000000`); const r = await fetch(new URL("/try-404", deployedUrl)); const temp = { text: await r.text(), status: r.status }; return temp; - } + }, ); expect(text).toBeFalsy(); }); @@ -715,16 +712,16 @@ Current Version ID: 00000000-0000-0000-0000-000000000000`); // create a dispatch namespace && verify output let output = await helper.run( - `wrangler dispatch-namespace create ${dispatchNamespaceName}` + `wrangler dispatch-namespace create ${dispatchNamespaceName}`, ); let normalizedStdout = normalizeOutput(output.stdout); expect(normalizedStdout).toContain( - `Created dispatch namespace "tmp-e2e-dispatch-00000000-0000-0000-0000-000000000000" with ID "00000000-0000-0000-0000-000000000000"` + `Created dispatch namespace "tmp-e2e-dispatch-00000000-0000-0000-0000-000000000000" with ID "00000000-0000-0000-0000-000000000000"`, ); // upload user Worker to the dispatch namespace && verify output output = await helper.run( - `wrangler deploy --dispatch-namespace ${dispatchNamespaceName}` + `wrangler deploy --dispatch-namespace ${dispatchNamespaceName}`, ); validateAssetUploadLogs(output, [ "/404.html", @@ -734,7 +731,7 @@ Current Version ID: 00000000-0000-0000-0000-000000000000`); // deploy dispatch Worker && verify output output = await helper.run( - `wrangler deploy -c dispatch-worker/wrangler.toml` + `wrangler deploy -c dispatch-worker/wrangler.toml`, ); normalizedStdout = normalizeOutput(output.stdout); expect(normalizedStdout).toEqual(`Total Upload: xx KiB / gzip: xx KiB @@ -779,7 +776,7 @@ Current Version ID: 00000000-0000-0000-0000-000000000000`); const r = await fetch(new URL("/try-404", deployedUrl)); const temp = { text: await r.text(), status: r.status }; return temp; - } + }, ); expect(text).toContain("

404.html

"); }); @@ -810,16 +807,16 @@ Current Version ID: 00000000-0000-0000-0000-000000000000`); // create a dispatch namespace && verify output let output = await helper.run( - `wrangler dispatch-namespace create ${dispatchNamespaceName}` + `wrangler dispatch-namespace create ${dispatchNamespaceName}`, ); let normalizedStdout = normalizeOutput(output.stdout); expect(normalizedStdout).toContain( - `Created dispatch namespace "tmp-e2e-dispatch-00000000-0000-0000-0000-000000000000" with ID "00000000-0000-0000-0000-000000000000"` + `Created dispatch namespace "tmp-e2e-dispatch-00000000-0000-0000-0000-000000000000" with ID "00000000-0000-0000-0000-000000000000"`, ); // upload user Worker to the dispatch namespace && verify output output = await helper.run( - `wrangler deploy --dispatch-namespace ${dispatchNamespaceName}` + `wrangler deploy --dispatch-namespace ${dispatchNamespaceName}`, ); validateAssetUploadLogs(output, [ "/404.html", @@ -829,7 +826,7 @@ Current Version ID: 00000000-0000-0000-0000-000000000000`); // deploy dispatch Worker && verify output output = await helper.run( - `wrangler deploy -c dispatch-worker/wrangler.toml` + `wrangler deploy -c dispatch-worker/wrangler.toml`, ); normalizedStdout = normalizeOutput(output.stdout); expect(normalizedStdout).toEqual(`Total Upload: xx KiB / gzip: xx KiB @@ -959,7 +956,7 @@ describe.skipIf(skipContainersTest)("containers", () => { // clean up user Worker after each test const deleteWorker = helper.bestEffortRun(`wrangler delete`); const deleteContainer = helper.bestEffortRun( - `wrangler containers delete ${applicationId}` + `wrangler containers delete ${applicationId}`, ); await Promise.allSettled([deleteWorker, deleteContainer]); }); @@ -973,39 +970,37 @@ describe.skipIf(skipContainersTest)("containers", () => { deployedUrl = getDeployedUrl(outputOne); const matchApplicationId = outputOne.stdout.match( - /([(]Application ID: (?.+?)[)])/ + /([(]Application ID: (?.+?)[)])/, ); applicationId = matchApplicationId?.groups?.applicationId; assert(matchApplicationId?.groups); const outputTwo = await helper.run(`wrangler deploy`); expect(outputTwo.stdout).toContain(`No changes to be made`); - } + }, ); it( "can fetch DO container", { timeout: 60 * 2 * 1000 }, async ({ expect }) => { - await vi.waitFor( + await waitForLong( async () => { const response = await fetch(`${deployedUrl}/do`, { signal: AbortSignal.timeout(5_000), }); if (!response.ok) { throw new Error( - "Durable object transient error: " + (await response.text()) + "Durable object transient error: " + (await response.text()), ); } expect(await response.text()).toEqual("hello from container"); }, - - // big timeout for containers - // (3m) - { timeout: 60 * 2 * 1000, interval: 1000 } + // big timeout for containers (2m) + { timeout: 60 * 2 * 1000, interval: 1000 }, ); - } + }, ); }); @@ -1014,7 +1009,7 @@ describe.skipIf(skipContainersTest)("containers", () => { */ function getDeployedUrl(output: { stdout: string }) { const match = output.stdout.match( - /(?https:\/\/tmp-e2e-.+?\..+?\.workers\.dev)/ + /(?https:\/\/tmp-e2e-.+?\..+?\.workers\.dev)/, ); assert(match?.groups); return match.groups.url; diff --git a/packages/wrangler/e2e/dev-registry.test.ts b/packages/wrangler/e2e/dev-registry.test.ts index 3542463f5a..7590fb66c3 100644 --- a/packages/wrangler/e2e/dev-registry.test.ts +++ b/packages/wrangler/e2e/dev-registry.test.ts @@ -1,36 +1,14 @@ import getPort from "get-port"; import dedent from "ts-dedent"; import { fetch, Request } from "undici"; -import { beforeEach, describe, it, vi } from "vitest"; +import { beforeEach, describe, it } from "vitest"; import { WranglerE2ETestHelper } from "./helpers/e2e-wrangler-test"; +import { fetchJson } from "./helpers/fetch-json"; import { fetchText } from "./helpers/fetch-text"; import { generateResourceName } from "./helpers/generate-resource-name"; import { normalizeOutput } from "./helpers/normalize"; import { seed as baseSeed, makeRoot } from "./helpers/setup"; -import type { RequestInit } from "undici"; - -async function fetchJson(url: string, info?: RequestInit): Promise { - const request = new Request(url, info); - const headers = new Headers(request.headers); - - headers.set("MF-Disable-Pretty-Error", "true"); - - return vi.waitFor( - async () => { - const text: string = await fetch(request, { - headers, - }).then((r) => r.text()); - try { - return JSON.parse(text) as T; - } catch (cause) { - const err = new Error(`Failed to parse JSON from:\n${text}`); - err.cause = cause; - throw err; - } - }, - { timeout: 10_000, interval: 250 } - ); -} +import { waitFor, waitForLong } from "./helpers/wait-for"; describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { let workerName: string; @@ -167,7 +145,7 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { const { url } = await worker.waitForReady(5_000); await expect(fetch(url).then((r) => r.text())).resolves.toBe( - "hello world" + "hello world", ); }); @@ -179,13 +157,14 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { const workerA = helper.runLongLived(cmd, { cwd: a }); const { url } = await workerA.waitForReady(5_000); - await vi.waitFor( - async () => await expect(fetchText(url)).resolves.toBe("hello world"), - { interval: 1000, timeout: 10_000 } + await waitForLong(() => + expect(fetchText(url)).resolves.toBe("hello world"), ); - expect(normalizeOutput(workerA.currentOutput)).toContain( - "connect to other Wrangler or Vite dev processes running locally" + await waitFor(async () => + expect(normalizeOutput(workerA.currentOutput)).toContain( + "connect to other Wrangler or Vite dev processes running locally", + ), ); }); @@ -196,9 +175,8 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { const workerB = helper.runLongLived(cmd, { cwd: b }); await workerB.waitForReady(5_000); - await vi.waitFor( - async () => await expect(fetchText(url)).resolves.toBe("hello world"), - { interval: 1000, timeout: 10_000 } + await waitForLong(() => + expect(fetchText(url)).resolves.toBe("hello world"), ); }); }); @@ -229,12 +207,10 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { const { url } = await workerA.waitForReady(5_000); - await vi.waitFor( - async () => - await expect(fetchText(`${url}/service`)).resolves.toBe( - "Hello from service worker" - ), - { interval: 1000, timeout: 10_000 } + await waitForLong(() => + expect(fetchText(`${url}/service`)).resolves.toBe( + "Hello from service worker", + ), ); }); @@ -249,14 +225,12 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { await workerC.waitForReady(5_000); - await vi.waitFor( - async () => - await expect(fetchText(`${url}/service`)).resolves.toBe( - "Hello from service worker" - ), - { interval: 1000, timeout: 10_000 } + await waitForLong(() => + expect(fetchText(`${url}/service`)).resolves.toBe( + "Hello from service worker", + ), ); - } + }, ); }); @@ -315,13 +289,10 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { await expect(fetchText(`${url}`)).resolves.toBe("hello from a"); - await vi.waitFor( - async () => { - await fetchText(`${url}`); - expect(workerB.currentOutput).includes("received tail event"); - }, - { interval: 1000, timeout: 10_000 } - ); + await waitForLong(async () => { + await fetchText(`${url}`); + expect(workerB.currentOutput).includes("received tail event"); + }); }); }); @@ -358,7 +329,7 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { headers: { "X-Reset-Count": "true", }, - }) + }), ).resolves.toMatchObject({ count: 1 }); }); @@ -372,18 +343,14 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { await workerA.waitForReady(5_000); - await vi.waitFor( - async () => - await expect( - fetchJson(`${url}/do`, { - headers: { - "X-Reset-Count": "true", - }, - }) - ).resolves.toMatchObject({ count: 1 }), - { interval: 1000, timeout: 10_000 } - ); - } + await expect( + fetchJson(`${url}/do`, { + headers: { + "X-Reset-Count": "true", + }, + }), + ).resolves.toMatchObject({ count: 1 }); + }, ); it("can fetch remote DO attached to a through b (start a, start b)", async ({ @@ -396,16 +363,14 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { const { url } = await workerB.waitForReady(5_000); - await vi.waitFor( - async () => - await expect( - fetch(`${url}/do`, { - headers: { - "X-Reset-Count": "true", - }, - }).then((r) => r.json()) - ).resolves.toMatchObject({ count: 1 }), - { interval: 1000, timeout: 10_000 } + await waitForLong(() => + expect( + fetch(`${url}/do`, { + headers: { + "X-Reset-Count": "true", + }, + }).then((r) => r.json()), + ).resolves.toMatchObject({ count: 1 }), ); }); }); @@ -439,7 +404,7 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { const { url } = await worker.waitForReady(5_000); await expect(fetch(url).then((r) => r.text())).resolves.toBe( - "hello world" + "hello world", ); }); @@ -447,13 +412,13 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { const port = await getPort(); const worker = helper.runLongLived( `${cmd.replace("wrangler dev", "wrangler pages dev")} --port ${port}`, - { cwd: a } + { cwd: a }, ); const { url } = await worker.waitForReady(5_000); await expect(fetch(url).then((r) => r.text())).resolves.toBe( - "Hello from Pages" + "Hello from Pages", ); }); @@ -465,20 +430,18 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { const port = await getPort(); const workerA = helper.runLongLived( `${cmd.replace("wrangler dev", "wrangler pages dev")} --port ${port}`, - { cwd: a } + { cwd: a }, ); const { url } = await workerA.waitForReady(5_000); - await vi.waitFor( - async () => - await expect(fetchText(`${url}/service`)).resolves.toBe( - "hello world" - ), - { interval: 1000, timeout: 10_000 } + await waitForLong(() => + expect(fetchText(`${url}/service`)).resolves.toBe("hello world"), ); - expect(normalizeOutput(workerA.currentOutput)).toContain( - "connect to other Wrangler or Vite dev processes running locally" + await waitFor(async () => + expect(normalizeOutput(workerA.currentOutput)).toContain( + "connect to other Wrangler or Vite dev processes running locally", + ), ); }); @@ -486,19 +449,15 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { const port = await getPort(); const workerA = helper.runLongLived( `${cmd.replace("wrangler dev", "wrangler pages dev")} --port ${port}`, - { cwd: a } + { cwd: a }, ); const { url } = await workerA.waitForReady(5_000); const workerB = helper.runLongLived(cmd, { cwd: b }); await workerB.waitForReady(5_000); - await vi.waitFor( - async () => - await expect(fetchText(`${url}/service`)).resolves.toBe( - "hello world" - ), - { interval: 1000, timeout: 10_000 } + await waitForLong(() => + expect(fetchText(`${url}/service`)).resolves.toBe("hello world"), ); }); @@ -512,19 +471,15 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { const port = await getPort(); const workerA = helper.runLongLived( `${cmd.replace("wrangler dev", "wrangler pages dev")} dist --service BEE=${workerName2} --port ${port}`, - { cwd: a } + { cwd: a }, ); const { url } = await workerA.waitForReady(5_000); const workerB = helper.runLongLived(cmd, { cwd: b }); await workerB.waitForReady(5_000); - await vi.waitFor( - async () => - await expect(fetchText(`${url}/service`)).resolves.toBe( - "hello world" - ), - { interval: 1000, timeout: 10_000 } + await waitForLong(() => + expect(fetchText(`${url}/service`)).resolves.toBe("hello world"), ); }); }); diff --git a/packages/wrangler/e2e/dev.test.ts b/packages/wrangler/e2e/dev.test.ts index c081d405bd..da050bc4a6 100644 --- a/packages/wrangler/e2e/dev.test.ts +++ b/packages/wrangler/e2e/dev.test.ts @@ -1,8 +1,8 @@ -import assert, { fail } from "node:assert"; +import assert from "node:assert"; import { existsSync } from "node:fs"; import { readFile } from "node:fs/promises"; import * as nodeNet from "node:net"; -import { scheduler, setTimeout } from "node:timers/promises"; +import { setTimeout } from "node:timers/promises"; import dedent from "ts-dedent"; import { fetch } from "undici"; import { afterEach, beforeEach, describe, it } from "vitest"; @@ -20,6 +20,7 @@ import { 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 = [ @@ -97,7 +98,7 @@ describe.each([ // Regression test for issue where multiple request logs were being logged per request expect([...worker.currentOutput.matchAll(/GET /g)].length).toBe(1); - await expect(fetchText(url)).resolves.toMatchSnapshot(); + await waitForLong(() => expect(fetchText(url)).resolves.toMatchSnapshot()); }); it("works with basic service worker", async ({ expect }) => { @@ -171,14 +172,16 @@ describe.each([ `, }); const worker = helper.runLongLived( - `${cmd} --show-interactive-dev-session=false` + `${cmd} --show-interactive-dev-session=false`, ); const { url } = await worker.waitForReady(); - await expect(fetch(url).then((r) => r.text())).resolves.toMatchSnapshot(); + await waitForLong(() => + expect(fetch(url).then((r) => r.text())).resolves.toMatchSnapshot(), + ); - await expect(worker.currentOutput).not.toContain("[b] open a browser"); + expect(worker.currentOutput).not.toContain("[b] open a browser"); }); describe(`--test-scheduled works with ${cmd}`, async () => { @@ -211,7 +214,7 @@ describe.each([ const { url } = await worker.waitForReady(); await expect( - fetch(`${url}/__scheduled`).then((r) => r.text()) + fetch(`${url}/__scheduled`).then((r) => r.text()), ).resolves.toMatchSnapshot(); await worker.readUntil(/Event triggered/); @@ -244,7 +247,7 @@ describe.each([ const { url } = await worker.waitForReady(); await expect( - fetch(`${url}/__scheduled`).then((r) => r.text()) + fetch(`${url}/__scheduled`).then((r) => r.text()), ).resolves.toMatchSnapshot(); await worker.readUntil(/Event triggered/); @@ -288,13 +291,15 @@ describe.each([ const { hostname, port } = new URL(url); // The warning should contain the actual port, not "undefined" - 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"); + 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 ({ @@ -333,7 +338,7 @@ describe.each([ // The warning should NOT appear when testScheduled is enabled expect(worker.currentOutput).not.toContain( - "Scheduled Workers are not automatically triggered" + "Scheduled Workers are not automatically triggered", ); }); }); @@ -497,7 +502,7 @@ it.runIf(process.platform !== "win32")( expect(beginProcesses.length).toBe(0); expect(endProcesses.length).toBe(0); - } + }, ); // Skipping remote python tests because they consistently flake with timeouts @@ -553,7 +558,7 @@ describe.each([{ cmd: "wrangler dev" }])( async () => { const r = await fetch(url); return { text: await r.text(), status: r.status }; - } + }, ); expect(text).toBe("Updated Python Worker value"); @@ -605,7 +610,7 @@ describe.each([{ cmd: "wrangler dev" }])( async () => { const r = await fetch(url); return { text: await r.text(), status: r.status }; - } + }, ); expect(text).toBe("py hello world 5"); @@ -739,7 +744,7 @@ describe.each([{ cmd: "wrangler dev" }])( await worker.readUntil(/excluded_module not found/); await worker.readUntil(/end/); }); - } + }, ); describe.each(HYPERDRIVE_DATABASES)( @@ -985,7 +990,7 @@ describe.each(HYPERDRIVE_DATABASES)( const { url } = await worker.waitForReady(); await fetch(`${url}/connect`); - } + }, ); afterEach(() => { @@ -993,7 +998,7 @@ describe.each(HYPERDRIVE_DATABASES)( server.close(); } }); - } + }, ); describe("queue dev tests", () => { @@ -1060,7 +1065,7 @@ describe("writes debug logs to hidden file", () => { const worker = helper.runLongLived("wrangler dev --log-level debug"); const match = await worker.readUntil( - /🪵 {2}Writing logs to "(?.+\.log)"/ + /🪵 {2}Writing logs to "(?.+\.log)"/, ); const filepath = match.groups?.filepath; @@ -1149,7 +1154,7 @@ describe("analytics engine", () => { const text = await fetchText(url); expect(text).toContain( - `successfully wrote datapoint from module worker` + `successfully wrote datapoint from module worker`, ); }); }); @@ -1192,11 +1197,11 @@ describe("analytics engine", () => { const worker = helper.runLongLived(cmd); await worker.readUntil( - /Analytics Engine is not supported locally when using the service-worker format/ + /Analytics Engine is not supported locally when using the service-worker format/, ); }); }); - } + }, ); }); @@ -1266,7 +1271,7 @@ describe.skipIf(CLOUDFLARE_ACCOUNT_ID !== "8d783f274e1f82dc46744c297b015a2f")( const text = await fetchText(url); expect(text).toMatchInlineSnapshot( - `"https://wrangler-testing.testing.devprod.cloudflare.dev/"` + `"https://wrangler-testing.testing.devprod.cloudflare.dev/"`, ); }); @@ -1304,7 +1309,7 @@ describe.skipIf(CLOUDFLARE_ACCOUNT_ID !== "8d783f274e1f82dc46744c297b015a2f")( const text = await fetchText(url); expect(text).toMatchInlineSnapshot( - `"https://wrangler-testing.testing.devprod.cloudflare.dev/"` + `"https://wrangler-testing.testing.devprod.cloudflare.dev/"`, ); }); @@ -1343,7 +1348,7 @@ describe.skipIf(CLOUDFLARE_ACCOUNT_ID !== "8d783f274e1f82dc46744c297b015a2f")( await fetchText(url); await worker.readUntil(/ERROR/); }); - } + }, ); describe("custom builds", () => { @@ -1424,7 +1429,7 @@ describe("custom builds", () => { // assert no more custom builds happen // regression: https://github.com/cloudflare/workers-sdk/issues/6876 await expect( - worker.readUntil(/\[custom build\] Running/, 5_000) + worker.readUntil(/\[custom build\] Running/, 5_000), ).rejects.toThrowError(); // now check assets are still fetchable, even after updates @@ -1441,7 +1446,7 @@ describe("custom builds", () => { async () => { const res2 = await fetch(url); return res2.text(); - } + }, ); await expect(resText).toBe("world"); }); @@ -1494,11 +1499,11 @@ describe("watch mode", () => { (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" }])( @@ -1525,7 +1530,7 @@ describe("watch mode", () => { let { response, cachedETags } = await fetchWithETag( `${url}/index.html`, - {} + {}, ); const originalETag = response.headers.get("etag"); expect(await response.text()).toBe("

Hello Workers + Assets

"); @@ -1540,10 +1545,10 @@ describe("watch mode", () => { (s) => s.response.status !== 200, async () => { return await fetchWithETag(`${url}/index.html`, cachedETags); - } + }, )); expect(await response.text()).toBe( - "

Hello Updated Workers + Assets

" + "

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); @@ -1569,7 +1574,7 @@ describe("watch mode", () => { const { url } = await worker.waitForReady(); let { response, cachedETags } = await fetchWithETag( `${url}/index.html`, - {} + {}, ); expect(await response.text()).toBe("

Hello Workers + Assets

"); @@ -1587,20 +1592,20 @@ describe("watch mode", () => { (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 + 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 + cachedETags, )); expect(response.status).toBe(304); }); @@ -1627,18 +1632,18 @@ describe("watch mode", () => { 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 + cachedETags, )); expect(await response.text()).toBe("About Workers + Assets"); ({ response, cachedETags } = await fetchWithETag( `${url}/workers/index.html`, - cachedETags + cachedETags, )); expect(await response.text()).toBe("Cloudflare Workers!"); @@ -1652,7 +1657,7 @@ describe("watch mode", () => { (s) => s.response.status !== 404, async () => { return await fetchWithETag(`${url}/index.html`, cachedETags); - } + }, )); expect(response.status).toBe(404); }); @@ -1702,7 +1707,7 @@ describe("watch mode", () => { (r) => r.status !== 302, async () => { return await fetch(`${url}/foo`, { redirect: "manual" }); - } + }, ); expect(response.status).toBe(302); expect(response.headers.get("Location")).toBe("/bar"); @@ -1737,7 +1742,7 @@ describe("watch mode", () => { let { response, cachedETags } = await fetchWithETag( `${url}/index.html`, - {} + {}, ); expect(await response.text()).toBe("

Hello Workers + Assets

"); @@ -1757,15 +1762,15 @@ describe("watch mode", () => { (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

" + "

Read more about Workers + Assets

", ); }); @@ -1821,7 +1826,7 @@ describe("watch mode", () => { status: fetchResponse.status, text: await fetchResponse.text(), }; - } + }, ); expect(status).toBe(200); expect(text).toBe("

Hello Workers + Assets

"); @@ -1888,7 +1893,7 @@ describe("watch mode", () => { status: fetchResponse.status, text: await fetchResponse.text(), }; - } + }, ); expect(status).toBe(200); expect(text).toBe("

Hello Workers + Assets

"); @@ -2101,10 +2106,10 @@ describe("watch mode", () => { `, }); await worker.readUntil( - /Warning: The following routes will attempt to serve Assets on a configured path:/ + /Warning: The following routes will attempt to serve Assets on a configured path:/, ); }); - } + }, ); describe.each([{ cmd: "wrangler dev --assets=dist" }])( @@ -2128,17 +2133,17 @@ describe("watch mode", () => { 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 + cachedETags, )); expect(await response.text()).toBe( - "

Read more about Workers + Assets

" + "

Read more about Workers + Assets

", ); // change + add @@ -2157,23 +2162,23 @@ describe("watch mode", () => { (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 + cachedETags, )); expect(await response.text()).toBe( - "

Hello Updated Workers + Assets

" + "

Hello Updated Workers + Assets

", ); expect(response.headers.get("etag")).not.toBe(originalETag); // unchanged -> expect 304 ({ response, cachedETags } = await fetchWithETag( `${url}/about.html`, - cachedETags + cachedETags, )); expect(response.status).toBe(304); @@ -2188,7 +2193,7 @@ describe("watch mode", () => { (s) => s.response.status !== 404, async () => { return await fetchWithETag(`${url}/about.html`, cachedETags); - } + }, )); expect(response.status).toBe(404); }); @@ -2323,10 +2328,10 @@ describe("watch mode", () => { `, }); await worker.readUntil( - /Warning: The following routes will attempt to serve Assets on a configured path:/ + /Warning: The following routes will attempt to serve Assets on a configured path:/, ); }); - } + }, ); }); @@ -2380,24 +2385,25 @@ This is a random email body. This is a random email body. `, method: "POST", - } + }, ); expect(response.status).toBe(200); - expect(worker.currentOutput).includes( - "Email handler replied to sender with the following message:" - ); + 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*)" + "Email handler replied to sender with the following message:\\s*(\\S*)", ); - const maybeReplyPath = pathRegexp.exec(worker.currentOutput)?.[1]; - - if (maybeReplyPath === undefined) { - fail("Reply message does not contain path"); - } + const maybeReplyPath = await vi.waitUntil( + () => pathRegexp.exec(worker.currentOutput)?.[1], + { interval: 100, timeout: 5000 }, + ); expect(await readFile(maybeReplyPath, "utf-8")).toMatchInlineSnapshot(` "References: @@ -2447,11 +2453,11 @@ 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"` + `"Worker rejected email with the following reason: I dont like this email"`, ); expect(response.status).toBe(400); @@ -2491,15 +2497,17 @@ Content-Type: text/plain This is a random email body. `, method: "POST", - } + }, ); expect(response.status).toBe(200); - expect(worker.currentOutput).includes( - `Email handler forwarded message with` - ); - expect(worker.currentOutput).includes(`rcptTo: mark.s@example.com`); + 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 }) => { @@ -2545,26 +2553,25 @@ Content-Type: text/plain This is a random email body. `, method: "POST", - } + }, ); expect(response.status).toBe(200); - await scheduler.wait(1000); - - expect(worker.currentOutput).includes( - "send_email binding called with the following message" + 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*)" + "send_email binding called with the following message:\\s*(\\S*)", ); - const maybeReplyPath = pathRegexp.exec(worker.currentOutput)?.[1]; - - if (maybeReplyPath === undefined || maybeReplyPath === null) { - fail("send_email message does not contain path"); - } + const maybeReplyPath = await vi.waitUntil( + () => pathRegexp.exec(worker.currentOutput)?.[1], + { interval: 100, timeout: 5000 }, + ); expect(await readFile(maybeReplyPath, "utf-8")).toMatchInlineSnapshot(` "From: someone @@ -2856,13 +2863,13 @@ describe(".env support in local dev", () => { // 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"' + '"CLOUDFLARE_INCLUDE_PROCESS_ENV": "true"', ); expect(await (await fetch(url)).text()).contains( - '"WRANGLER_ENV_VAR_0": "default-0"' + '"WRANGLER_ENV_VAR_0": "default-0"', ); expect(await (await fetch(url)).text()).contains( - '"WRANGLER_ENV_VAR_1": "env-1"' + '"WRANGLER_ENV_VAR_1": "env-1"', ); }); @@ -2897,7 +2904,7 @@ describe(".env support in local dev", () => { }); const worker = helper.runLongLived( - "wrangler dev --env-file=other/.env --env-file=other/.env.local" + "wrangler dev --env-file=other/.env --env-file=other/.env.local", ); const { url } = await worker.waitForReady(); expect(await (await fetch(url)).text()).toMatchInlineSnapshot(` diff --git a/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts b/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts index 5830a7dc77..ec77c79237 100644 --- a/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts +++ b/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts @@ -3,8 +3,7 @@ import crypto from "node:crypto"; import { cp } from "node:fs/promises"; import { setTimeout } from "node:timers/promises"; import { fetch } from "undici"; -// eslint-disable-next-line no-restricted-imports -import { expect, onTestFinished, vi } from "vitest"; +import { expect, onTestFinished } from "vitest"; import { generateLeafCertificate, generateMtlsCertName, @@ -12,6 +11,7 @@ import { } from "./cert"; import { generateResourceName } from "./generate-resource-name"; import { makeRoot, removeFiles, seed } from "./setup"; +import { waitForLong } from "./wait-for"; import { MINIFLARE_IMPORT, runWrangler, @@ -40,8 +40,8 @@ export class WranglerE2ETestHelper { /** Provide an alternative to `onTestFinished` to handle tearing down resources. */ public readonly onTeardown: ( fn: () => Awaitable, - timeoutMs?: number - ) => void = onTestFinished + timeoutMs?: number, + ) => void = onTestFinished, ) {} /** A temporary directory where files will be seeded and commands will be run. */ @@ -51,7 +51,7 @@ export class WranglerE2ETestHelper { async seed(files: Record): Promise; async seed(sourceDir: string): Promise; async seed( - filesOrSourceDir: Record | string + filesOrSourceDir: Record | string, ): Promise { if (typeof filesOrSourceDir === "string") { await cp(filesOrSourceDir, this.tmpPath, { recursive: true }); @@ -72,7 +72,7 @@ export class WranglerE2ETestHelper { cwd = this.tmpPath, stopOnTestFinished = true, ...options - }: WranglerCommandOptions & { stopOnTestFinished?: boolean } = {} + }: WranglerCommandOptions & { stopOnTestFinished?: boolean } = {}, ): WranglerLongLivedCommand { const wrangler = new WranglerLongLivedCommand(wranglerCommand, { cwd, @@ -89,7 +89,7 @@ export class WranglerE2ETestHelper { /** Run a Wrangler command that will execute and exit, such as `wrangler whoami` */ async run( wranglerCommand: string, - { cwd = this.tmpPath, ...options }: WranglerCommandOptions = {} + { cwd = this.tmpPath, ...options }: WranglerCommandOptions = {}, ) { return runWrangler(wranglerCommand, { cwd, ...options }); } @@ -103,14 +103,14 @@ export class WranglerE2ETestHelper { */ async bestEffortRun( wranglerCommand: string, - { timeout = 5_000, ...options }: WranglerCommandOptions = {} + { timeout = 5_000, ...options }: WranglerCommandOptions = {}, ) { try { return await this.run(wranglerCommand, { timeout, ...options }); } catch (e) { console.warn( `Best-effort cleanup "${wranglerCommand}" failed:`, - e instanceof Error ? e.message : e + e instanceof Error ? e.message : e, ); return undefined; } @@ -139,7 +139,7 @@ export class WranglerE2ETestHelper { const name = generateResourceName("dispatch"); if (isLocal) { throw new Error( - "Dispatch namespaces are not supported in local mode (yet)" + "Dispatch namespaces are not supported in local mode (yet)", ); } await this.run(`wrangler dispatch-namespace create ${name}`); @@ -192,7 +192,7 @@ export class WranglerE2ETestHelper { const name = resourceName ?? generateResourceName("vectorize"); if (!resourceName) { await this.run( - `wrangler vectorize create ${name} --dimensions ${dimensions} --metric ${metric}` + `wrangler vectorize create ${name} --dimensions ${dimensions} --metric ${metric}`, ); } this.onTeardown(async () => { @@ -207,7 +207,7 @@ export class WranglerE2ETestHelper { /** Create a Hyperdrive connection and clean it up during tear-down. */ async hyperdrive( isLocal: boolean, - scheme: "postgresql" | "mysql" = "postgresql" + scheme: "postgresql" | "mysql" = "postgresql", ): Promise<{ id: string; name: string }> { const name = generateResourceName("hyperdrive"); @@ -223,11 +223,11 @@ export class WranglerE2ETestHelper { assert( connectionString, - `${envVar} must be set in order to create a Hyperdrive resource for this test` + `${envVar} must be set in order to create a Hyperdrive resource for this test`, ); const result = await this.run( - `wrangler hyperdrive create ${name} --connection-string="${connectionString}"` + `wrangler hyperdrive create ${name} --connection-string="${connectionString}"`, ); const tomlMatch = /id = "([0-9a-f]{32})"/.exec(result.stdout); const jsonMatch = /"id": "([0-9a-f]{32})"/.exec(result.stdout); @@ -255,7 +255,7 @@ export class WranglerE2ETestHelper { const name = generateMtlsCertName(); const output = await this.run( - `wrangler cert upload mtls-certificate --name ${name} --cert "mtls_client_cert_file.pem" --key "mtls_client_private_key_file.pem"` + `wrangler cert upload mtls-certificate --name ${name} --cert "mtls_client_cert_file.pem" --key "mtls_client_private_key_file.pem"`, ); const match = output.stdout.match(/ID:\s+(?.*)$/m); const certificateId = match?.groups?.certId; @@ -310,11 +310,11 @@ export class WranglerE2ETestHelper { const configOption = configPath ? `-c ${configPath}` : ""; const workerNameOption = `--name ${workerName}`; const { stdout } = await this.run( - `wrangler deploy ${entryPoint} ${workerNameOption} ${configOption} --compatibility-date 2025-01-01 ${extraFlags.join(" ")}` + `wrangler deploy ${entryPoint} ${workerNameOption} ${configOption} --compatibility-date 2025-01-01 ${extraFlags.join(" ")}`, ); const urlMatcher = new RegExp( - `(?https:\\/\\/${workerName}\\..+?\\.workers\\.dev)` + `(?https:\\/\\/${workerName}\\..+?\\.workers\\.dev)`, ); const deployedUrl = stdout.match(urlMatcher)?.groups?.url; @@ -325,13 +325,10 @@ export class WranglerE2ETestHelper { await setTimeout(2_000); // Wait for the worker to become available - await vi.waitFor( - async () => { - const response = await fetch(deployedUrl); - expect(response.status).toBe(200); - }, - { timeout: 10_000, interval: 500 } - ); + await waitForLong(async () => { + const response = await fetch(deployedUrl); + expect(response.status).toBe(200); + }); const cleanup = async () => { await this.bestEffortRun(`wrangler delete --name ${workerName} --force`); @@ -343,7 +340,7 @@ export class WranglerE2ETestHelper { } catch (e) { throw new Error( "Failed to register cleanup for worker.\nPerhaps you called this outside an `it` block?\nIf so, pass `cleanOnTestFinished: false` and then use the returned `cleanup` helper yourself", - { cause: e } + { cause: e }, ); } return { deployedUrl, stdout }; diff --git a/packages/wrangler/e2e/helpers/fetch-json.ts b/packages/wrangler/e2e/helpers/fetch-json.ts new file mode 100644 index 0000000000..941c6ec918 --- /dev/null +++ b/packages/wrangler/e2e/helpers/fetch-json.ts @@ -0,0 +1,29 @@ +import { fetch, Request } from "undici"; +import { waitForLong } from "./wait-for"; +import type { RequestInit } from "undici"; + +/** + * Fetch a URL, parse the response as JSON, and retry until it succeeds. + * Uses `waitForLong` internally so it will keep retrying on network errors + * or non-JSON responses until the timeout is reached. + */ +export async function fetchJson( + url: string, + info?: RequestInit +): Promise { + return waitForLong(async () => { + const request = new Request(url, info); + const headers = new Headers(request.headers); + headers.set("MF-Disable-Pretty-Error", "true"); + const text: string = await fetch(request, { + headers, + }).then((r) => r.text()); + try { + return JSON.parse(text) as T; + } catch (cause) { + const err = new Error(`Failed to parse JSON from:\n${text}`); + err.cause = cause; + throw err; + } + }); +} diff --git a/packages/wrangler/e2e/helpers/wait-for.ts b/packages/wrangler/e2e/helpers/wait-for.ts new file mode 100644 index 0000000000..5b4fb36e32 --- /dev/null +++ b/packages/wrangler/e2e/helpers/wait-for.ts @@ -0,0 +1,34 @@ +import { vi } from "vitest"; + +type WaitForOptions = { interval?: number; timeout?: number }; + +/** + * Wrapper around `vi.waitFor()` for polling synchronous state (e.g. `currentOutput`). + * Defaults to `{ interval: 100, timeout: 5_000 }`. + */ +export function waitFor( + callback: () => T | Promise, + options?: WaitForOptions +): Promise { + return vi.waitFor(callback, { + interval: 100, + timeout: 5_000, + ...options, + }); +} + +/** + * Wrapper around `vi.waitFor()` with a slower poll interval and longer timeout than `waitFor`. + * Useful for polling HTTP endpoints (e.g. `fetch`/`fetchText`). + * Defaults to `{ interval: 500, timeout: 10_000 }`. + */ +export function waitForLong( + callback: () => T | Promise, + options?: WaitForOptions +): Promise { + return vi.waitFor(callback, { + interval: 500, + timeout: 10_000, + ...options, + }); +} diff --git a/packages/wrangler/e2e/multiworker-dev.test.ts b/packages/wrangler/e2e/multiworker-dev.test.ts index 4a4966f486..c9ea074676 100644 --- a/packages/wrangler/e2e/multiworker-dev.test.ts +++ b/packages/wrangler/e2e/multiworker-dev.test.ts @@ -1,31 +1,13 @@ import { randomUUID } from "node:crypto"; import dedent from "ts-dedent"; import { fetch } from "undici"; -import { beforeEach, describe, it, vi } from "vitest"; +import { beforeEach, describe, it } from "vitest"; import { WranglerE2ETestHelper } from "./helpers/e2e-wrangler-test"; +import { fetchJson } from "./helpers/fetch-json"; import { fetchText } from "./helpers/fetch-text"; import { generateResourceName } from "./helpers/generate-resource-name"; import { seed as baseSeed, makeRoot } from "./helpers/setup"; -import type { RequestInit } from "undici"; - -async function fetchJson(url: string, info?: RequestInit): Promise { - return vi.waitFor( - async () => { - const text: string = await fetch(url, { - headers: { "MF-Disable-Pretty-Error": "true" }, - ...info, - }).then((r) => r.text()); - try { - return JSON.parse(text) as T; - } catch (cause) { - const err = new Error(`Failed to parse JSON from:\n${text}`); - err.cause = cause; - throw err; - } - }, - { timeout: 10_000, interval: 250 } - ); -} +import { waitFor, waitForLong } from "./helpers/wait-for"; describe("multiworker", () => { let workerName: string; @@ -210,20 +192,19 @@ describe("multiworker", () => { const { url } = await worker.waitForReady(5_000); await expect(fetch(url).then((r) => r.text())).resolves.toBe( - "hello world" + "hello world", ); }); it("can fetch b through a", async ({ expect }) => { const workerA = helper.runLongLived( `wrangler dev -c wrangler.toml -c ${b}/wrangler.toml`, - { cwd: a } + { cwd: a }, ); const { url } = await workerA.waitForReady(5_000); - await vi.waitFor( + await waitForLong( async () => await expect(fetchText(url)).resolves.toBe("hello world"), - { interval: 1000, timeout: 10_000 } ); }); @@ -232,34 +213,30 @@ describe("multiworker", () => { }) => { const workerA = helper.runLongLived( `wrangler dev -c wrangler.toml -c ${b}/wrangler.toml`, - { cwd: a } + { cwd: a }, ); const { url } = await workerA.waitForReady(5_000); - await vi.waitFor( + await waitForLong( async () => await expect(fetchText(`${url}/count`)).resolves.toBe("6"), - { interval: 1000, timeout: 10_000 } ); }); it("can access service props through a binding", async ({ expect }) => { const workerA = helper.runLongLived( `wrangler dev -c wrangler.toml -c ${b}/wrangler.toml`, - { cwd: a } + { cwd: a }, ); const { url } = await workerA.waitForReady(5_000); - await vi.waitFor( - async () => { - const response = await fetch(`${url}/props`); - const props = await response.json(); - expect(props).toEqual({ - foo: 123, - bar: { baz: "hello from props" }, - }); - }, - { interval: 1000, timeout: 10_000 } - ); + await waitForLong(async () => { + const response = await fetch(`${url}/props`); + const props = await response.json(); + expect(props).toEqual({ + foo: 123, + bar: { baz: "hello from props" }, + }); + }); }); it("shows runtime error when fetching non-existent service", async ({ @@ -280,17 +257,16 @@ describe("multiworker", () => { const workerA = helper.runLongLived( `wrangler dev -c wrangler.toml -c ${b}/wrangler.toml`, - { cwd: a } + { cwd: a }, ); const { url } = await workerA.waitForReady(5_000); - await vi.waitFor( + await waitForLong( async () => await expect(fetchText(url)).resolves.toBe( - `Couldn't find a local dev session for the "default" entrypoint of service "${service}" to proxy to` + `Couldn't find a local dev session for the "default" entrypoint of service "${service}" to proxy to`, ), - { interval: 1000, timeout: 10_000 } ); }); }); @@ -313,16 +289,15 @@ describe("multiworker", () => { it("can fetch service worker c through a", async ({ expect }) => { const workerA = helper.runLongLived( `wrangler dev -c wrangler.toml -c ${c}/wrangler.toml`, - { cwd: a } + { cwd: a }, ); const { url } = await workerA.waitForReady(5_000); - await vi.waitFor( + await waitForLong( async () => await expect(fetchText(`${url}/service`)).resolves.toBe( - "Hello from service worker" + "Hello from service worker", ), - { interval: 1000, timeout: 10_000 } ); }); }); @@ -360,28 +335,24 @@ describe("multiworker", () => { headers: { "X-Reset-Count": "true", }, - }) + }), ).resolves.toMatchObject({ count: 1 }); }); it("can fetch remote DO attached to a through b", async ({ expect }) => { const workerB = helper.runLongLived( `wrangler dev -c wrangler.toml -c ${a}/wrangler.toml`, - { cwd: b } + { cwd: b }, ); const { url } = await workerB.waitForReady(5_000); - await vi.waitFor( - async () => - await expect( - fetchJson(`${url}/do`, { - headers: { - "X-Reset-Count": "true", - }, - }) - ).resolves.toMatchObject({ count: 1 }), - { interval: 1000, timeout: 10_000 } - ); + await expect( + fetchJson(`${url}/do`, { + headers: { + "X-Reset-Count": "true", + }, + }), + ).resolves.toMatchObject({ count: 1 }); }); it("can fetch remote DO attached to a through b with RPC", async ({ @@ -389,16 +360,15 @@ describe("multiworker", () => { }) => { const workerB = helper.runLongLived( `wrangler dev -c wrangler.toml -c ${a}/wrangler.toml`, - { cwd: b } + { cwd: b }, ); const { url } = await workerB.waitForReady(5_000); - await vi.waitFor( + await waitForLong( async () => await expect(fetchText(`${url}/do-rpc`)).resolves.toBe( - "Hello through DO RPC" + "Hello through DO RPC", ), - { interval: 1000, timeout: 10_000 } ); }); }); @@ -452,16 +422,14 @@ describe("multiworker", () => { it("tail event sent to b", async ({ expect }) => { const worker = helper.runLongLived( `wrangler dev -c wrangler.toml -c ${b}/wrangler.toml`, - { cwd: a } + { cwd: a }, ); const { url } = await worker.waitForReady(5_000); await expect(fetchText(`${url}`)).resolves.toBe("hello from a"); - await vi.waitFor( - async () => - expect(worker.currentOutput).includes("received tail event"), - { interval: 1000, timeout: 10_000 } + await waitFor(() => + expect(worker.currentOutput).includes("received tail event"), ); }); }); @@ -514,16 +482,15 @@ describe("multiworker", () => { it("logs tail event sent to b", async ({ expect }) => { const worker = helper.runLongLived( `wrangler dev -c wrangler.toml -c ${b}/wrangler.toml`, - { cwd: a } + { cwd: a }, ); const { url } = await worker.waitForReady(5_000); - await expect(fetchText(`${url}`)).resolves.toBe("hello from a"); - - await vi.waitFor( - async () => - expect(worker.currentOutput).includes("received tail stream event"), - { interval: 1000, timeout: 10_000 } + await waitForLong(() => + expect(fetchText(`${url}`)).resolves.toBe("hello from a"), + ); + await waitFor(() => + expect(worker.currentOutput).includes("received tail stream event"), ); }); }); @@ -559,46 +526,43 @@ describe("multiworker", () => { it("pages project assets", async ({ expect }) => { const pages = helper.runLongLived( `wrangler pages dev -c wrangler.toml -c ${b}/wrangler.toml -c ${c}/wrangler.toml`, - { cwd: a } + { cwd: a }, ); const { url } = await pages.waitForReady(5_000); - await vi.waitFor( + await waitForLong( async () => await expect(fetchText(`${url}`)).resolves.toBe( - "

hello pages assets

" + "

hello pages assets

", ), - { interval: 1000, timeout: 10_000 } ); }); it("pages project fetching service worker", async ({ expect }) => { const pages = helper.runLongLived( `wrangler pages dev -c wrangler.toml -c ${b}/wrangler.toml -c ${c}/wrangler.toml`, - { cwd: a } + { cwd: a }, ); const { url } = await pages.waitForReady(5_000); - await vi.waitFor( + await waitForLong( async () => await expect(fetchText(`${url}/cee`)).resolves.toBe( - "Hello from service worker" + "Hello from service worker", ), - { interval: 1000, timeout: 10_000 } ); }); it("pages project fetching module worker", async ({ expect }) => { const pages = helper.runLongLived( `wrangler pages dev -c wrangler.toml -c ${b}/wrangler.toml -c ${c}/wrangler.toml`, - { cwd: a } + { cwd: a }, ); const { url } = await pages.waitForReady(5_000); - await vi.waitFor( + await waitForLong( async () => await expect(fetchText(`${url}/bee`)).resolves.toBe("hello world"), - { interval: 1000, timeout: 10_000 } ); }); @@ -607,10 +571,10 @@ describe("multiworker", () => { }) => { const pages = helper.runLongLived( `wrangler pages dev -c wrangler.toml -c wrangler.toml`, - { cwd: a } + { cwd: a }, ); await pages.readUntil( - /You cannot use a Pages project as a service binding target/ + /You cannot use a Pages project as a service binding target/, ); }); }); @@ -669,20 +633,22 @@ describe("multiworker", () => { }) => { const worker = helper.runLongLived( `wrangler dev -c wrangler.toml -c ${b}/wrangler.toml`, - { cwd: a } + { cwd: a }, ); const { url } = await worker.waitForReady(5_000); const { hostname, port } = new URL(url); - // The warning should contain the actual port, not "undefined" - 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"); + await waitFor(() => { + // The warning should contain the actual port, not "undefined" + 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"); + }); }); }); }); diff --git a/packages/wrangler/e2e/pages-dev.test.ts b/packages/wrangler/e2e/pages-dev.test.ts index ddf693c311..1d5989edbc 100644 --- a/packages/wrangler/e2e/pages-dev.test.ts +++ b/packages/wrangler/e2e/pages-dev.test.ts @@ -8,6 +8,7 @@ import { describe, it } from "vitest"; import { WranglerE2ETestHelper } from "./helpers/e2e-wrangler-test"; import { fetchText } from "./helpers/fetch-text"; import { normalizeOutput } from "./helpers/normalize"; +import { waitFor } from "./helpers/wait-for"; const port = await getPort(); const inspectorPort = await getPort(); @@ -67,8 +68,10 @@ describe.sequential("wrangler pages dev", () => { const { url } = await worker.waitForReady(); const text = await fetchText(url); expect(text).toBe("Testing [--experimental-local]"); - expect(await worker.currentOutput).toContain( - `--experimental-local is no longer required and will be removed in a future version` + await waitFor(() => + expect(worker.currentOutput).toContain( + `--experimental-local is no longer required and will be removed in a future version` + ) ); }); @@ -93,21 +96,17 @@ describe.sequential("wrangler pages dev", () => { `${cmd} --inspector-port ${inspectorPort} . --port ${port} --service test --kv = --do test --d1 = --r2 =` ); await worker.waitForReady(); - expect(await worker.currentOutput).toContain( - `Could not parse Service binding: test` - ); - expect(await worker.currentOutput).toContain( - `Could not parse KV binding: =` - ); - expect(await worker.currentOutput).toContain( - `Could not parse Durable Object binding: test` - ); - expect(await worker.currentOutput).toContain( - `Could not parse R2 binding: =` - ); - expect(await worker.currentOutput).toContain( - `Could not parse D1 binding: =` - ); + await waitFor(() => { + expect(worker.currentOutput).toContain( + `Could not parse Service binding: test` + ); + expect(worker.currentOutput).toContain(`Could not parse KV binding: =`); + expect(worker.currentOutput).toContain( + `Could not parse Durable Object binding: test` + ); + expect(worker.currentOutput).toContain(`Could not parse R2 binding: =`); + expect(worker.currentOutput).toContain(`Could not parse D1 binding: =`); + }); }); it("should use bindings specified as args in the command line", async ({ @@ -126,6 +125,13 @@ describe.sequential("wrangler pages dev", () => { `${cmd} --inspector-port ${inspectorPort} . --port ${port} --service TEST_SERVICE=test-worker --kv TEST_KV --do TEST_DO=TestDurableObject@a --d1 TEST_D1 --r2 TEST_R2 --compatibility-date=2025-05-21` ); await worker.waitForReady(); + + await waitFor(() => + expect(worker.currentOutput).toContain( + "Your Worker has access to the following bindings:" + ) + ); + const bindingMessages = worker.currentOutput.split( "Your Worker has access to the following bindings:" ); @@ -354,6 +360,12 @@ describe.sequential("wrangler pages dev", () => { await worker.readUntil(/GET \/ 200 OK/); + await waitFor(() => + expect(worker.currentOutput).toContain( + "Your Worker has access to the following bindings:" + ) + ); + expect(normalizeOutput(worker.currentOutput)).toMatchInlineSnapshot(` "✨ Compiled Worker successfully Your Worker has access to the following bindings: @@ -459,6 +471,12 @@ describe.sequential("wrangler pages dev", () => { }); await worker.waitForReady(); + await waitFor(() => + expect(worker.currentOutput).toContain( + "Your Worker has access to the following bindings:" + ) + ); + // We only care about the list of bindings and warnings, so strip other output const [prestartOutput] = normalizeOutput(worker.currentOutput).split( "⎔ Starting local server..." diff --git a/packages/wrangler/e2e/remote-binding/dev-remote-bindings.test.ts b/packages/wrangler/e2e/remote-binding/dev-remote-bindings.test.ts index 5b5b8f4786..75e2e0efa2 100644 --- a/packages/wrangler/e2e/remote-binding/dev-remote-bindings.test.ts +++ b/packages/wrangler/e2e/remote-binding/dev-remote-bindings.test.ts @@ -3,13 +3,14 @@ import { resolve } from "node:path"; import { setTimeout } from "node:timers/promises"; import getPort from "get-port"; import dedent from "ts-dedent"; -import { beforeAll, describe, it, vi } from "vitest"; +import { beforeAll, describe, it } from "vitest"; import { CLOUDFLARE_ACCOUNT_ID } from "../helpers/account-id"; import { WranglerE2ETestHelper } from "../helpers/e2e-wrangler-test"; import { fetchText } from "../helpers/fetch-text"; import { generateResourceName } from "../helpers/generate-resource-name"; import { normalizeOutput } from "../helpers/normalize"; import { makeRoot, seed } from "../helpers/setup"; +import { waitFor } from "../helpers/wait-for"; describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( "wrangler dev - remote bindings", @@ -92,38 +93,38 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( const { url } = await worker.waitForReady(); await expect(fetchText(url)).resolves.toMatchInlineSnapshot( - `"REMOTE: Hello from a remote worker"` + `"REMOTE: Hello from a remote worker"`, ); const indexContent = await readFile( `${helper.tmpPath}/simple-service-binding.js`, - "utf8" + "utf8", ); await writeFile( `${helper.tmpPath}/simple-service-binding.js`, indexContent.replace( "REMOTE:", - "The remote worker responded with:" + "The remote worker responded with:", ), - "utf8" + "utf8", ); await setTimeout(500); await expect(fetchText(url)).resolves.toMatchInlineSnapshot( - `"The remote worker responded with: Hello from a remote worker"` + `"The remote worker responded with: Hello from a remote worker"`, ); await writeFile( `${helper.tmpPath}/simple-service-binding.js`, indexContent, - "utf8" + "utf8", ); await setTimeout(500); await expect(fetchText(url)).resolves.toMatchInlineSnapshot( - `"REMOTE: Hello from a remote worker"` + `"REMOTE: Hello from a remote worker"`, ); }); @@ -176,21 +177,21 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( const { url } = await worker.waitForReady(); await expect(fetchText(url)).resolves.toContain( - "This is a response from Workers AI." + "This is a response from Workers AI.", ); // This should only include logs from the user Wrangler session (i.e. a single list of attached bindings, and only one ready message) - const normalizedOutput = normalizeOutput(worker.currentOutput); - - expect(normalizedOutput).toMatchInlineSnapshot(` - "Your Worker has access to the following bindings: - Binding Resource Mode - env.AI AI remote - ▲ [WARNING] AI bindings always access remote resources, and so may incur usage charges even in local dev. To suppress this warning, set \`remote: true\` for the binding definition in your configuration file. - ⎔ Starting local server... - [wrangler:info] Ready on http://: - [wrangler:info] GET / 200 OK (TIMINGS)" - `); + await waitFor(() => + expect(normalizeOutput(worker.currentOutput)).toEqual(dedent` + Your Worker has access to the following bindings: + Binding Resource Mode + env.AI AI remote + ▲ [WARNING] AI bindings always access remote resources, and so may incur usage charges even in local dev. To suppress this warning, set \`remote: true\` for the binding definition in your configuration file. + ⎔ Starting local server... + [wrangler:info] Ready on http://: + [wrangler:info] GET / 200 OK (TIMINGS) + `), + ); }); describe("shows helpful error logs", () => { @@ -214,12 +215,10 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( const worker = helper.runLongLived("wrangler dev"); - await vi.waitFor( - () => - expect(worker.currentOutput).toContain( - "Service binding 'REMOTE_WORKER' references Worker 'non-existent-service-binding' which was not found." - ), - 7_000 + await waitFor(() => + expect(worker.currentOutput).toContain( + "Service binding 'REMOTE_WORKER' references Worker 'non-existent-service-binding' which was not found.", + ), ); }); @@ -243,12 +242,10 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( const worker = helper.runLongLived("wrangler dev"); - await vi.waitFor( - () => - expect(worker.currentOutput).toContain( - "KV namespace 'non-existent-kv' is not valid." - ), - 7_000 + await waitFor(() => + expect(worker.currentOutput).toContain( + "KV namespace 'non-existent-kv' is not valid.", + ), ); }); }); @@ -301,7 +298,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( }); const worker = helper.runLongLived( - `wrangler dev -c wrangler.json -c ${localTest}/wrangler.json` + `wrangler dev -c wrangler.json -c ${localTest}/wrangler.json`, ); const { url } = await worker.waitForReady(); @@ -313,7 +310,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( `); }); }); - } + }, ); async function spawnLocalWorker(helper: WranglerE2ETestHelper): Promise { @@ -335,7 +332,7 @@ async function spawnLocalWorker(helper: WranglerE2ETestHelper): Promise { // Note: we use a random port here otherwise for some reason in CI windows // allows the default port to be overridden by other processes `wrangler dev --port ${await getPort()}`, - { cwd: local } + { cwd: local }, ); await localWorker.waitForReady(); } diff --git a/packages/wrangler/e2e/start-worker-auth-opts.test.ts b/packages/wrangler/e2e/start-worker-auth-opts.test.ts index 504a173a73..73f0592aba 100644 --- a/packages/wrangler/e2e/start-worker-auth-opts.test.ts +++ b/packages/wrangler/e2e/start-worker-auth-opts.test.ts @@ -16,6 +16,7 @@ import { importWrangler, WranglerE2ETestHelper, } from "./helpers/e2e-wrangler-test"; +import { waitForLong } from "./helpers/wait-for"; import type { Worker } from "../src/api/startDevWorker"; import type { MockInstance } from "vitest"; @@ -267,7 +268,7 @@ async function fetchTimedTextFromWorker( try { assert(worker, "Worker is not defined"); - await vi.waitFor( + await waitForLong( async () => { responseText = await ( await worker.fetch("http://example.com", { @@ -275,7 +276,7 @@ async function fetchTimedTextFromWorker( }) ).text(); }, - { timeout: 20_000, interval: 700 } + { timeout: 20_000 } ); } catch { return null; diff --git a/packages/wrangler/e2e/startWorker.test.ts b/packages/wrangler/e2e/startWorker.test.ts index f4c2097afc..7a5bc46cbd 100644 --- a/packages/wrangler/e2e/startWorker.test.ts +++ b/packages/wrangler/e2e/startWorker.test.ts @@ -10,6 +10,7 @@ import { importWrangler, WranglerE2ETestHelper, } from "./helpers/e2e-wrangler-test"; +import { waitFor } from "./helpers/wait-for"; import type { DevToolsEvent } from "../src/api"; const OPTIONS = [ @@ -30,9 +31,6 @@ function waitForMessageContaining(ws: WebSocket, value: string): Promise { }); } -const waitFor: typeof vi.waitFor = (cb) => - vi.waitFor(cb, { interval: 200, timeout: 5000 }); - function collectMessagesContaining( ws: WebSocket, value: string, diff --git a/packages/wrangler/e2e/unenv-preset/preset.test.ts b/packages/wrangler/e2e/unenv-preset/preset.test.ts index deef4d4eb4..84e36158bb 100644 --- a/packages/wrangler/e2e/unenv-preset/preset.test.ts +++ b/packages/wrangler/e2e/unenv-preset/preset.test.ts @@ -1,11 +1,11 @@ import { join } from "node:path"; import { fetch } from "undici"; -// eslint-disable-next-line no-restricted-imports -import { beforeAll, describe, expect, test, vi } from "vitest"; +import { beforeAll, describe, expect, test } from "vitest"; import { CLOUDFLARE_ACCOUNT_ID } from "../helpers/account-id"; import { WranglerE2ETestHelper } from "../helpers/e2e-wrangler-test"; import { generateResourceName } from "../helpers/generate-resource-name"; import { retry } from "../helpers/retry"; +import { waitForLong } from "../helpers/wait-for"; import { WorkerdTests } from "./worker/index"; type TestConfig = { @@ -868,7 +868,7 @@ describe.each(groupedLocalConfigs)( // Wait for the Worker to be actually responding. const readyResp = await retry( (resp) => !resp.ok, - async () => await fetch(`${url}/ping`) + async () => await fetch(`${url}/ping`), ); await expect(readyResp.text()).resolves.toEqual("pong"); @@ -888,17 +888,17 @@ describe.each(groupedLocalConfigs)( async (testName, { expect }) => { // Retries the callback until it succeeds or times out. // Useful for the i.e. DNS tests where underlying requests might error/timeout. - await vi.waitFor( + await waitForLong( async () => { const response = await fetch(`${url}/${testName}`); const body = await response.text(); expect(body).toMatch("passed"); }, - { timeout: 19_000, interval: 200 } + { timeout: 19_000 }, ); - } + }, ); - } + }, ); // Run the remote tests only if the user is logged in (e.g. not for a PR from a forked repo) @@ -926,7 +926,7 @@ describe.runIf(Boolean(CLOUDFLARE_ACCOUNT_ID))( // Wait for the Worker to be actually responding. const readyResp = await retry( (resp) => !resp.ok, - async () => await fetch(`${url}/ping`) + async () => await fetch(`${url}/ping`), ); await expect(readyResp.text()).resolves.toEqual("pong"); @@ -946,17 +946,17 @@ describe.runIf(Boolean(CLOUDFLARE_ACCOUNT_ID))( async (testName, { expect }) => { // Retries the callback until it succeeds or times out. // Useful for the i.e. DNS tests where underlying requests might error/timeout. - await vi.waitFor( + await waitForLong( async () => { const response = await fetch(`${url}/${testName}`); const body = await response.text(); expect(body).toMatch("passed"); }, - { timeout: 19_000, interval: 200 } + { timeout: 19_000 }, ); - } + }, ); - } + }, ); // Run the remote tests only if the user is logged in (e.g. not for a PR from a forked repo) @@ -985,7 +985,7 @@ describe.runIf(Boolean(CLOUDFLARE_ACCOUNT_ID))( // Wait for the Worker to be actually responding. const readyResp = await retry( (resp) => !resp.ok, - async () => await fetch(`${url}/ping`) + async () => await fetch(`${url}/ping`), ); await expect(readyResp.text()).resolves.toEqual("pong"); @@ -1005,17 +1005,17 @@ describe.runIf(Boolean(CLOUDFLARE_ACCOUNT_ID))( async (testName, { expect }) => { // Retries the callback until it succeeds or times out. // Useful for the i.e. DNS tests where underlying requests might error/timeout. - await vi.waitFor( + await waitForLong( async () => { const response = await fetch(`${url}/${testName}`); const body = await response.text(); expect(body).toMatch("passed"); }, - { timeout: 19_000, interval: 200 } + { timeout: 19_000 }, ); - } + }, ); - } + }, ); type ConfigGroup = { @@ -1045,7 +1045,7 @@ function groupByWranglerConfig(configs: TestConfig[]): ConfigGroup[] { if (existing) { existing.name += `, ${config.name}`; for (const [flag, value] of Object.entries( - config.expectRuntimeFlags ?? {} + config.expectRuntimeFlags ?? {}, )) { if ( flag in existing.expectRuntimeFlags && @@ -1053,7 +1053,7 @@ function groupByWranglerConfig(configs: TestConfig[]): ConfigGroup[] { ) { throw new Error( `Conflicting expectRuntimeFlags for "${flag}" in group "${existing.name}": ` + - `existing=${existing.expectRuntimeFlags[flag]}, new=${value} (from "${config.name}")` + `existing=${existing.expectRuntimeFlags[flag]}, new=${value} (from "${config.name}")`, ); } existing.expectRuntimeFlags[flag] = value; @@ -1107,7 +1107,7 @@ function collectEnabledFlags(configs: TestConfig[]): string[] { enableFlags.add(flag); } else if (!flag.startsWith("disable_")) { throw new Error( - `Only enable_... and disable_... flags are handled, got "${flag}"` + `Only enable_... and disable_... flags are handled, got "${flag}"`, ); } } From b92d00404ae4bc9d352777751166f285bc681872 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Tue, 17 Mar 2026 12:30:54 +0000 Subject: [PATCH 02/18] Add retries to realish preview session and worker upload API calls Wrap createPreviewSession() and createWorkerPreview() in retryOnAPIFailure so transient 5xx errors are retried automatically (up to 3 attempts with linear backoff). Also add an optional abortSignal parameter to retryOnAPIFailure so backoff delays can be cancelled immediately when a new bundle arrives. --- .../src/__tests__/utils/retry.test.ts | 49 +++++++++++++++++++ .../startDevWorker/RemoteRuntimeController.ts | 44 +++++++++++------ packages/wrangler/src/utils/retry.ts | 8 +-- 3 files changed, 84 insertions(+), 17 deletions(-) diff --git a/packages/wrangler/src/__tests__/utils/retry.test.ts b/packages/wrangler/src/__tests__/utils/retry.test.ts index 6e7394af63..00a6869c40 100644 --- a/packages/wrangler/src/__tests__/utils/retry.test.ts +++ b/packages/wrangler/src/__tests__/utils/retry.test.ts @@ -102,6 +102,55 @@ describe("retryOnAPIFailure", () => { expect(getRetryAndErrorLogs(std.debug)).toMatchInlineSnapshot(`[]`); }); + it("should cancel retry backoff when abort signal fires", async ({ + expect, + }) => { + const controller = new AbortController(); + let attempts = 0; + + const promise = retryOnAPIFailure( + () => { + attempts++; + if (attempts === 1) { + // After the first failure, abort before the backoff completes + queueMicrotask(() => controller.abort()); + } + throw new APIError({ status: 500, text: "500 error" }); + }, + // Use a very long backoff so the test would hang without abort + 10_000, + 3, + controller.signal + ); + + await expect(promise).rejects.toThrow(); + // Only the first attempt should have been made before the abort + // cancelled the backoff delay + expect(attempts).toBe(1); + }); + + it("should propagate abort error from action without retrying", async ({ + expect, + }) => { + const controller = new AbortController(); + controller.abort(); + let attempts = 0; + + await expect(() => + retryOnAPIFailure( + () => { + attempts++; + throw controller.signal.reason; + }, + undefined, + undefined, + controller.signal + ) + ).rejects.toThrow(); + // AbortError is not an APIError or TypeError, so it should not be retried + expect(attempts).toBe(1); + }); + it("should retry custom APIError implementation with non-5xx error", async ({ expect, }) => { diff --git a/packages/wrangler/src/api/startDevWorker/RemoteRuntimeController.ts b/packages/wrangler/src/api/startDevWorker/RemoteRuntimeController.ts index 11ce2d03df..585106aa60 100644 --- a/packages/wrangler/src/api/startDevWorker/RemoteRuntimeController.ts +++ b/packages/wrangler/src/api/startDevWorker/RemoteRuntimeController.ts @@ -17,6 +17,7 @@ import { logger } from "../../logger"; import { TRACE_VERSION } from "../../tail/createTail"; import { realishPrintLogs } from "../../tail/printing"; import { getAccessToken } from "../../user/access"; +import { retryOnAPIFailure } from "../../utils/retry"; import { RuntimeController } from "./BaseController"; import { castErrorCause } from "./events"; import { unwrapHook } from "./utils"; @@ -60,12 +61,18 @@ export class RemoteRuntimeController extends RuntimeController { const { workerAccount, workerContext } = await getWorkerAccountAndContext(props); - return await createPreviewSession( - props.complianceConfig, - workerAccount, - workerContext, - this.#abortController.signal, - props.name + return await retryOnAPIFailure( + () => + createPreviewSession( + props.complianceConfig, + workerAccount, + workerContext, + this.#abortController.signal, + props.name + ), + undefined, + undefined, + this.#abortController.signal ); } catch (err: unknown) { if (err instanceof Error && err.name == "AbortError") { @@ -94,6 +101,9 @@ export class RemoteRuntimeController extends RuntimeController { if (!this.#session) { return; } + // Capture session in a local variable so TypeScript can narrow + // the type inside the retryOnAPIFailure closure below. + const session = this.#session; try { /* @@ -156,14 +166,20 @@ export class RemoteRuntimeController extends RuntimeController { if (props.bundleId !== this.#currentBundleId) { return; } - const workerPreviewToken = await createWorkerPreview( - props.complianceConfig, - init, - workerAccount, - workerContext, - this.#session, - this.#abortController.signal, - props.minimal_mode + const workerPreviewToken = await retryOnAPIFailure( + () => + createWorkerPreview( + props.complianceConfig, + init, + workerAccount, + workerContext, + session, + this.#abortController.signal, + props.minimal_mode + ), + undefined, + undefined, + this.#abortController.signal ); if (workerPreviewToken.tailUrl) { diff --git a/packages/wrangler/src/utils/retry.ts b/packages/wrangler/src/utils/retry.ts index a035ac8f8f..5b449d8687 100644 --- a/packages/wrangler/src/utils/retry.ts +++ b/packages/wrangler/src/utils/retry.ts @@ -16,7 +16,8 @@ const MAX_ATTEMPTS = 3; export async function retryOnAPIFailure( action: () => T | Promise, backoff = 0, - attempts = MAX_ATTEMPTS + attempts = MAX_ATTEMPTS, + abortSignal?: AbortSignal ): Promise { try { return await action(); @@ -36,11 +37,12 @@ export async function retryOnAPIFailure( throw err; } - await setTimeout(backoff); + await setTimeout(backoff, undefined, { signal: abortSignal }); return retryOnAPIFailure( action, backoff + (MAX_ATTEMPTS - attempts) * 1000, - attempts - 1 + attempts - 1, + abortSignal ); } } From 89ccae8e3a02bbbfbde53f12ec9bfd3e2f842513 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Tue, 17 Mar 2026 16:00:35 +0000 Subject: [PATCH 03/18] Increase e2e timeouts for remote preview and add per-request API timeout - startWorker: use waitForLong (10s) instead of waitFor (5s) for remote reload polling, matching the convention for HTTP endpoint polling - start-worker-remote-bindings: increase beforeAll deploy timeout from 35s to 60s for slow Windows CI - dev.test Workers+Assets: increase waitForReady/waitForReload from 15s to 30s since remote mode involves session creation + asset upload + bundle upload to edge-preview - create-worker-preview: add 30s per-request timeout via AbortSignal.any so a hung Cloudflare API response doesn't block the reload indefinitely --- packages/wrangler/e2e/dev.test.ts | 10 +++--- .../start-worker-remote-bindings.test.ts | 2 +- packages/wrangler/e2e/startWorker.test.ts | 4 +-- .../wrangler/src/dev/create-worker-preview.ts | 31 ++++++++++++++++--- 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/packages/wrangler/e2e/dev.test.ts b/packages/wrangler/e2e/dev.test.ts index da050bc4a6..0511447cc3 100644 --- a/packages/wrangler/e2e/dev.test.ts +++ b/packages/wrangler/e2e/dev.test.ts @@ -374,7 +374,9 @@ describe.each([ }); const worker = helper.runLongLived(cmd); - const { url } = await worker.waitForReady(); + // 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(); @@ -387,7 +389,7 @@ describe.each([ }`, }); - await worker.waitForReload(); + await worker.waitForReload(30_000); await expect(fetchText(url)).resolves.toMatchSnapshot(); }); @@ -422,7 +424,7 @@ describe.each([ }); const worker = helper.runLongLived(cmd); - const { url } = await worker.waitForReady(); + const { url } = await worker.waitForReady(30_000); await expect(fetch(url).then((r) => r.text())).resolves.toMatchSnapshot(); @@ -431,7 +433,7 @@ describe.each([ Welcome to updated Workers + Assets readme!`, }); - await worker.waitForReload(); + await worker.waitForReload(30_000); await expect(fetchText(url)).resolves.toMatchSnapshot(); }); diff --git a/packages/wrangler/e2e/remote-binding/start-worker-remote-bindings.test.ts b/packages/wrangler/e2e/remote-binding/start-worker-remote-bindings.test.ts index 9439850c3c..13cef6067b 100644 --- a/packages/wrangler/e2e/remote-binding/start-worker-remote-bindings.test.ts +++ b/packages/wrangler/e2e/remote-binding/start-worker-remote-bindings.test.ts @@ -23,7 +23,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)("startWorker - remote bindings", () => { cleanOnTestFinished: false, }); return cleanup; - }, 35_000); + }, 60_000); it("allows connecting to a remote worker", async ({ expect }) => { await helper.seed({ diff --git a/packages/wrangler/e2e/startWorker.test.ts b/packages/wrangler/e2e/startWorker.test.ts index 7a5bc46cbd..2b5264acda 100644 --- a/packages/wrangler/e2e/startWorker.test.ts +++ b/packages/wrangler/e2e/startWorker.test.ts @@ -10,7 +10,7 @@ import { importWrangler, WranglerE2ETestHelper, } from "./helpers/e2e-wrangler-test"; -import { waitFor } from "./helpers/wait-for"; +import { waitFor, waitForLong } from "./helpers/wait-for"; import type { DevToolsEvent } from "../src/api"; const OPTIONS = [ @@ -87,7 +87,7 @@ describe("DevEnv", { sequential: true }, () => { "src/index.ts": script.replace("body:1", "body:2"), }); - await waitFor(async () => { + await waitForLong(async () => { res = await worker.fetch("http://dummy"); expect(await res.text()).toBe("body:2"); }); diff --git a/packages/wrangler/src/dev/create-worker-preview.ts b/packages/wrangler/src/dev/create-worker-preview.ts index 0698a5c1df..939ecaa794 100644 --- a/packages/wrangler/src/dev/create-worker-preview.ts +++ b/packages/wrangler/src/dev/create-worker-preview.ts @@ -15,6 +15,21 @@ import type { } from "@cloudflare/workers-utils"; import type { HeadersInit } from "undici"; +/** + * Maximum time (ms) to wait for an individual preview API request before + * treating it as a timeout. Without this, a hung API response blocks the + * entire dev-session reload indefinitely. + */ +const PREVIEW_API_TIMEOUT_MS = 30_000; + +/** + * Combine the caller's abort signal with a per-request timeout so that a + * hung Cloudflare API response doesn't block forever. + */ +function withTimeout(signal: AbortSignal): AbortSignal { + return AbortSignal.any([signal, AbortSignal.timeout(PREVIEW_API_TIMEOUT_MS)]); +} + /** * A Cloudflare account. */ @@ -106,7 +121,7 @@ function switchHost( zonePreview: boolean ): URL { const url = new URL(originalUrl); - url.hostname = zonePreview ? (host ?? url.hostname) : url.hostname; + url.hostname = zonePreview ? host ?? url.hostname : url.hostname; return url; } @@ -187,10 +202,18 @@ export async function createPreviewSession( const { token, exchange_url } = await fetchResult<{ token: string; exchange_url?: string; - }>(complianceConfig, initUrl, undefined, undefined, abortSignal, apiToken); + }>( + complianceConfig, + initUrl, + undefined, + undefined, + withTimeout(abortSignal), + apiToken + ); const previewSessionToken = exchange_url - ? ((await tryExpandToken(exchange_url, ctx, abortSignal)) ?? token) + ? (await tryExpandToken(exchange_url, ctx, withTimeout(abortSignal))) ?? + token : token; try { @@ -280,7 +303,7 @@ async function createPreviewToken( }, }, undefined, - abortSignal + withTimeout(abortSignal) ); return { From 808e35365dad7b84f1d70a3b4f24b3486dee8ceb Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Wed, 18 Mar 2026 13:00:02 +0000 Subject: [PATCH 04/18] fixup --- packages/wrangler/src/dev/create-worker-preview.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/wrangler/src/dev/create-worker-preview.ts b/packages/wrangler/src/dev/create-worker-preview.ts index 939ecaa794..b70d687538 100644 --- a/packages/wrangler/src/dev/create-worker-preview.ts +++ b/packages/wrangler/src/dev/create-worker-preview.ts @@ -121,7 +121,7 @@ function switchHost( zonePreview: boolean ): URL { const url = new URL(originalUrl); - url.hostname = zonePreview ? host ?? url.hostname : url.hostname; + url.hostname = zonePreview ? (host ?? url.hostname) : url.hostname; return url; } @@ -212,8 +212,8 @@ export async function createPreviewSession( ); const previewSessionToken = exchange_url - ? (await tryExpandToken(exchange_url, ctx, withTimeout(abortSignal))) ?? - token + ? ((await tryExpandToken(exchange_url, ctx, withTimeout(abortSignal))) ?? + token) : token; try { From f8eb06cbb06830b8908c46688851705371dfb5a0 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Wed, 18 Mar 2026 15:09:37 +0000 Subject: [PATCH 05/18] Use shared preserve-e2e workers for remote-binding e2e tests Reuse pre-deployed workers instead of deploying fresh ones on every test run. The new ensureWorkerDeployed() helper checks whether a worker is already live before deploying, and the preserve-e2e- prefix keeps the workers excluded from periodic cleanup. --- .../wrangler/e2e/helpers/e2e-wrangler-test.ts | 40 +++++++++++++++++++ .../dev-remote-bindings.test.ts | 31 ++++++-------- .../remote-bindings-api.test.ts | 7 +--- .../start-worker-remote-bindings.test.ts | 7 +--- 4 files changed, 56 insertions(+), 29 deletions(-) diff --git a/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts b/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts index ec77c79237..9f96f85b2f 100644 --- a/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts +++ b/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts @@ -266,6 +266,46 @@ export class WranglerE2ETestHelper { return certificateId; } + /** + * Ensure a worker with a well-known `preserve-e2e-*` name is deployed. + * + * Checks whether the worker is already live by fetching its + * `devprod-testing7928.workers.dev` URL. If it responds with a non-404 + * status the deploy is skipped; otherwise `wrangler deploy` is run and + * the helper waits for the worker to become available. + * + * No cleanup is registered — the worker is expected to persist across + * test runs and is excluded from the periodic e2e cleanup job by its + * `preserve-e2e-` prefix. + */ + async ensureWorkerDeployed({ + workerName, + entryPoint = "", + configPath, + }: { + workerName: string; + entryPoint?: string; + configPath?: string; + }): Promise { + const deployedUrl = `https://${workerName}.devprod-testing7928.workers.dev/`; + try { + const response = await fetch(deployedUrl); + if (response.status !== 404) { + return; // Worker already exists + } + } catch { + // Worker doesn't exist or is not reachable — fall through to deploy + } + const configOption = configPath ? `-c ${configPath}` : ""; + await this.run( + `wrangler deploy ${entryPoint} --name ${workerName} ${configOption} --compatibility-date 2025-01-01` + ); + await waitForLong(async () => { + const response = await fetch(deployedUrl); + expect(response.status).toBe(200); + }); + } + /** * Create a worker for the test and attempt to delete it after the test has finished. * diff --git a/packages/wrangler/e2e/remote-binding/dev-remote-bindings.test.ts b/packages/wrangler/e2e/remote-binding/dev-remote-bindings.test.ts index 75e2e0efa2..f7fc8f95f5 100644 --- a/packages/wrangler/e2e/remote-binding/dev-remote-bindings.test.ts +++ b/packages/wrangler/e2e/remote-binding/dev-remote-bindings.test.ts @@ -7,7 +7,6 @@ import { beforeAll, describe, it } from "vitest"; import { CLOUDFLARE_ACCOUNT_ID } from "../helpers/account-id"; import { WranglerE2ETestHelper } from "../helpers/e2e-wrangler-test"; import { fetchText } from "../helpers/fetch-text"; -import { generateResourceName } from "../helpers/generate-resource-name"; import { normalizeOutput } from "../helpers/normalize"; import { makeRoot, seed } from "../helpers/setup"; import { waitFor } from "../helpers/wait-for"; @@ -15,29 +14,23 @@ import { waitFor } from "../helpers/wait-for"; describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( "wrangler dev - remote bindings", () => { - const remoteWorkerName = generateResourceName(); - const alternativeRemoteWorkerName = generateResourceName(); + const remoteWorkerName = "preserve-e2e-wrangler-remote-worker"; + const alternativeRemoteWorkerName = + "preserve-e2e-wrangler-remote-worker-alt"; const helper = new WranglerE2ETestHelper(); beforeAll(async () => { await helper.seed(resolve(__dirname, "./workers")); - const cleanups = await Promise.all([ - helper - .worker({ - entryPoint: "remote-worker.js", - workerName: remoteWorkerName, - cleanOnTestFinished: false, - }) - .then(({ cleanup }) => cleanup), - helper - .worker({ - entryPoint: "alt-remote-worker.js", - workerName: alternativeRemoteWorkerName, - cleanOnTestFinished: false, - }) - .then(({ cleanup }) => cleanup), + await Promise.all([ + helper.ensureWorkerDeployed({ + entryPoint: "remote-worker.js", + workerName: remoteWorkerName, + }), + helper.ensureWorkerDeployed({ + entryPoint: "alt-remote-worker.js", + workerName: alternativeRemoteWorkerName, + }), ]); - return () => Promise.allSettled(cleanups.map((cleanup) => cleanup())); }, 35_000); it("handles both remote and local service bindings at the same time", async ({ diff --git a/packages/wrangler/e2e/remote-binding/remote-bindings-api.test.ts b/packages/wrangler/e2e/remote-binding/remote-bindings-api.test.ts index 4871b499fd..42d74aa331 100644 --- a/packages/wrangler/e2e/remote-binding/remote-bindings-api.test.ts +++ b/packages/wrangler/e2e/remote-binding/remote-bindings-api.test.ts @@ -6,7 +6,6 @@ import { importWrangler, WranglerE2ETestHelper, } from "../helpers/e2e-wrangler-test"; -import { generateResourceName } from "../helpers/generate-resource-name"; import type { MiniflareOptions, Miniflare as MiniflareType, @@ -25,17 +24,15 @@ const { startRemoteProxySession, maybeStartOrUpdateRemoteProxySession } = describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( "wrangler dev - remote bindings - programmatic API", async () => { - const remoteWorkerName = generateResourceName(); + const remoteWorkerName = "preserve-e2e-wrangler-remote-worker"; const helper = new WranglerE2ETestHelper(); beforeAll(async () => { await helper.seed(resolve(__dirname, "./workers")); - const { cleanup } = await helper.worker({ + await helper.ensureWorkerDeployed({ workerName: remoteWorkerName, entryPoint: "remote-worker.js", - cleanOnTestFinished: false, }); - return cleanup; }, 35_000); function getMfOptions( diff --git a/packages/wrangler/e2e/remote-binding/start-worker-remote-bindings.test.ts b/packages/wrangler/e2e/remote-binding/start-worker-remote-bindings.test.ts index 13cef6067b..dcc554fb4b 100644 --- a/packages/wrangler/e2e/remote-binding/start-worker-remote-bindings.test.ts +++ b/packages/wrangler/e2e/remote-binding/start-worker-remote-bindings.test.ts @@ -7,22 +7,19 @@ import { importWrangler, WranglerE2ETestHelper, } from "../helpers/e2e-wrangler-test"; -import { generateResourceName } from "../helpers/generate-resource-name"; const { unstable_startWorker: startWorker } = await importWrangler(); describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)("startWorker - remote bindings", () => { - const remoteWorkerName = generateResourceName(); + const remoteWorkerName = "preserve-e2e-wrangler-remote-worker"; const helper = new WranglerE2ETestHelper(); beforeAll(async () => { await helper.seed(resolve(__dirname, "./workers")); - const { cleanup } = await helper.worker({ + await helper.ensureWorkerDeployed({ entryPoint: "remote-worker.js", workerName: remoteWorkerName, - cleanOnTestFinished: false, }); - return cleanup; }, 60_000); it("allows connecting to a remote worker", async ({ expect }) => { From 943161dc6f4350f7f66e6e746577254115605289 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Wed, 18 Mar 2026 15:29:47 +0000 Subject: [PATCH 06/18] Add timeout protection to getWorkersDevSubdomain in createPreviewSession Thread the abort signal through to the fetchResult call inside getWorkersDevSubdomain so it gets the same withTimeout protection as the other API calls in createPreviewSession. --- packages/wrangler/src/dev/create-worker-preview.ts | 3 ++- packages/wrangler/src/routes.ts | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/wrangler/src/dev/create-worker-preview.ts b/packages/wrangler/src/dev/create-worker-preview.ts index b70d687538..d566cc0072 100644 --- a/packages/wrangler/src/dev/create-worker-preview.ts +++ b/packages/wrangler/src/dev/create-worker-preview.ts @@ -223,7 +223,8 @@ export async function createPreviewSession( complianceConfig, account.accountId, undefined, - apiToken + apiToken, + withTimeout(abortSignal) ); host = `${name ?? crypto.randomUUID()}.${subdomain}`; } diff --git a/packages/wrangler/src/routes.ts b/packages/wrangler/src/routes.ts index a09ff0189c..3c77022c20 100644 --- a/packages/wrangler/src/routes.ts +++ b/packages/wrangler/src/routes.ts @@ -17,7 +17,8 @@ export async function getWorkersDevSubdomain( complianceConfig: ComplianceConfig, accountId: string, configPath: string | undefined, - apiToken?: ApiCredentials + apiToken?: ApiCredentials, + abortSignal?: AbortSignal ): Promise { try { // note: API docs say that this field is "name", but they're lying. @@ -26,7 +27,7 @@ export async function getWorkersDevSubdomain( `/accounts/${accountId}/workers/subdomain`, undefined, undefined, - undefined, + abortSignal, apiToken ); return `${subdomain}${getComplianceRegionSubdomain(complianceConfig)}.workers.dev`; From 18f9232018f4d00afd7839f114417a0403348c5f Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Wed, 18 Mar 2026 15:38:24 +0000 Subject: [PATCH 07/18] Add changeset for remote preview retry and timeout changes --- .changeset/calm-pens-reply.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/calm-pens-reply.md diff --git a/.changeset/calm-pens-reply.md b/.changeset/calm-pens-reply.md new file mode 100644 index 0000000000..768acfd59f --- /dev/null +++ b/.changeset/calm-pens-reply.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +fix: Add retry and timeout protection to remote preview API calls + +Remote preview sessions (`wrangler dev --remote`) now automatically retry transient 5xx API errors (up to 3 attempts with linear backoff) and enforce a 30-second per-request timeout. Previously, a single hung or failed API response during session creation or worker upload could block the dev session reload indefinitely. From e7adf6a66fb050afca19112f615dcd8c8f14a7d7 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Wed, 18 Mar 2026 16:25:05 +0000 Subject: [PATCH 08/18] =?UTF-8?q?Fix=20retry=20backoff=20formula=20?= =?UTF-8?q?=E2=80=94=20retries=20were=20always=20firing=20with=200ms=20del?= =?UTF-8?q?ay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backoff computation backoff + (MAX_ATTEMPTS - attempts) * 1000 was off-by-one: the computed delay was passed to the next recursive call but that call would throw before ever sleeping on it. Replace with a simple backoff + 1000 so the first retry is immediate (0ms) and the second waits 1000ms, matching the documented intent. --- packages/wrangler/src/utils/retry.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/wrangler/src/utils/retry.ts b/packages/wrangler/src/utils/retry.ts index 5b449d8687..55f6455eff 100644 --- a/packages/wrangler/src/utils/retry.ts +++ b/packages/wrangler/src/utils/retry.ts @@ -38,11 +38,6 @@ export async function retryOnAPIFailure( } await setTimeout(backoff, undefined, { signal: abortSignal }); - return retryOnAPIFailure( - action, - backoff + (MAX_ATTEMPTS - attempts) * 1000, - attempts - 1, - abortSignal - ); + return retryOnAPIFailure(action, backoff + 1000, attempts - 1, abortSignal); } } From 2ff5e3779893278c8c2ee54e470a83331a13d249 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Wed, 18 Mar 2026 16:51:58 +0000 Subject: [PATCH 09/18] fix formatting and linting issues --- .../wrangler/e2e/assets-multiworker.test.ts | 112 +++++++------- packages/wrangler/e2e/deployments.test.ts | 70 ++++----- packages/wrangler/e2e/dev-registry.test.ts | 54 +++---- packages/wrangler/e2e/dev.test.ts | 142 +++++++++--------- .../wrangler/e2e/helpers/e2e-wrangler-test.ts | 44 +++--- packages/wrangler/e2e/multiworker-dev.test.ts | 72 ++++----- .../dev-remote-bindings.test.ts | 32 ++-- .../wrangler/e2e/unenv-preset/preset.test.ts | 75 ++++++--- 8 files changed, 316 insertions(+), 285 deletions(-) diff --git a/packages/wrangler/e2e/assets-multiworker.test.ts b/packages/wrangler/e2e/assets-multiworker.test.ts index d9ce3ac15d..14f0dbd946 100644 --- a/packages/wrangler/e2e/assets-multiworker.test.ts +++ b/packages/wrangler/e2e/assets-multiworker.test.ts @@ -13,7 +13,7 @@ async function startWorkersDevRegistry( helper: WranglerE2ETestHelper, assetWorker: string, regularWorker: string, - regularWorkerFirst = true, + regularWorkerFirst = true ) { const workerA = helper.runLongLived(wranglerDev, { cwd: regularWorkerFirst ? assetWorker : regularWorker, @@ -33,11 +33,11 @@ async function startWorkersMultiworker( helper: WranglerE2ETestHelper, assetWorker: string, regularWorker: string, - regularWorkerFirst = true, + regularWorkerFirst = true ) { const worker = helper.runLongLived( `${wranglerDev} -c wrangler.toml -c ${regularWorkerFirst ? assetWorker : regularWorker}/wrangler.toml`, - { cwd: regularWorkerFirst ? regularWorker : assetWorker }, + { cwd: regularWorkerFirst ? regularWorker : assetWorker } ); const { url } = await worker.waitForReady(5_000); return url; @@ -69,7 +69,7 @@ describe.each( style: MultiworkerStyle; start: typeof startWorkersDevRegistry | typeof startWorkersMultiworker; wranglerDev: string; - }[], + }[] )( "workers with assets ($style, $wranglerDev) ", async ({ start, style, wranglerDev }) => { @@ -208,17 +208,17 @@ describe.each( helper, assetWorker, regularWorker, - false, + false ); await expect(fetchText(`${url}/asset`)).resolves.toBe( - "

have an asset directly

", + "

have an asset directly

" ); await expect(fetchText(`${url}/asset-via-binding`)).resolves.toBe( - "

have an asset via a binding

", + "

have an asset via a binding

" ); await expect( - fetch(`${url}/worker`).then((r) => r.text()), + fetch(`${url}/worker`).then((r) => r.text()) ).resolves.toBe("hello world from a worker with assets"); }); @@ -230,17 +230,17 @@ describe.each( helper, assetWorker, regularWorker, - false, + false ); await waitForLong(() => expect( - fetch(`${url}/hello-from-dee`).then((r) => r.text()), - ).resolves.toBe("hello world from dee"), + fetch(`${url}/hello-from-dee`).then((r) => r.text()) + ).resolves.toBe("hello world from dee") ); await waitForLong(() => - expect(fetchText(`${url}/count`)).resolves.toBe("6"), + expect(fetchText(`${url}/count`)).resolves.toBe("6") ); }); @@ -252,22 +252,22 @@ describe.each( helper, assetWorker, regularWorker, - false, + false ); await waitForLong(() => expect(fetchText(`${url}/do-rpc`)).resolves.toBe( - "Hello through DO RPC", - ), + "Hello through DO RPC" + ) ); await expect( fetchJson(`${url}/do`, { headers: { "X-Reset-Count": "true", }, - }), + }) ).resolves.toMatchObject({ count: 1 }); - }, + } ); }); @@ -309,12 +309,12 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker, + regularWorker ); await waitForLong(() => expect(fetchText(`${url}/asset`)).resolves.toBe( - "

have an asset directly

", - ), + "

have an asset directly

" + ) ); }); @@ -323,12 +323,12 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker, + regularWorker ); await waitForLong(() => expect(fetch(`${url}/not-an-asset`)).resolves.toMatchObject({ status: 404, - }), + }) ); }); @@ -337,13 +337,13 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker, + regularWorker ); await waitForLong(() => expect(fetchText(`${url}/rpc`)).resolves.toContain( // Cannot call RPC methods on assets-only workers - "The RPC receiver does not implement the method", - ), + "The RPC receiver does not implement the method" + ) ); }); }); @@ -433,12 +433,12 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker, + regularWorker ); await waitForLong(() => expect(fetchText(`${url}/asset`)).resolves.toBe( - "

have an asset directly

", - ), + "

have an asset directly

" + ) ); }); @@ -447,12 +447,12 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker, + regularWorker ); await waitForLong(() => expect(fetchText(`${url}/not-an-asset`)).resolves.toBe( - "hello world from a worker with assets", - ), + "hello world from a worker with assets" + ) ); }); @@ -461,12 +461,12 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker, + regularWorker ); await waitForLong(() => expect(fetchText(`${url}/asset-via-binding`)).resolves.toBe( - "

have an asset via a binding

", - ), + "

have an asset via a binding

" + ) ); }); @@ -475,10 +475,10 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker, + regularWorker ); await waitForLong(() => - expect(fetchText(`${url}/rpc`)).resolves.toBe("2"), + expect(fetchText(`${url}/rpc`)).resolves.toBe("2") ); }); }); @@ -507,12 +507,12 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker, + regularWorker ); await waitForLong(() => expect(fetchText(`${url}/asset`)).resolves.toBe( - "

have an asset directly

", - ), + "

have an asset directly

" + ) ); }); @@ -521,12 +521,12 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker, + regularWorker ); await waitForLong(() => expect(fetchText(`${url}/not-an-asset`)).resolves.toBe( - "hello world from a worker with assets", - ), + "hello world from a worker with assets" + ) ); }); @@ -535,12 +535,12 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker, + regularWorker ); await waitForLong(() => expect(fetchText(`${url}/asset-via-binding`)).resolves.toBe( - "

have an asset via a binding

", - ), + "

have an asset via a binding

" + ) ); }); @@ -549,10 +549,10 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker, + regularWorker ); await waitForLong(() => - expect(fetchText(`${url}/rpc`)).resolves.toBe("2"), + expect(fetchText(`${url}/rpc`)).resolves.toBe("2") ); }); }); @@ -595,12 +595,12 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker, + regularWorker ); await waitForLong(() => expect(fetchText(`${url}/asset`)).resolves.toBe( - "hello world from a worker with assets", - ), + "hello world from a worker with assets" + ) ); }); @@ -609,12 +609,12 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker, + regularWorker ); await waitForLong(() => expect(fetchText(`${url}/asset-via-binding`)).resolves.toBe( - "

have an asset via a binding

", - ), + "

have an asset via a binding

" + ) ); }); @@ -623,13 +623,13 @@ describe.each( wranglerDev, helper, assetWorker, - regularWorker, + regularWorker ); await waitForLong(() => - expect(fetchText(`${url}/rpc`)).resolves.toBe("2"), + expect(fetchText(`${url}/rpc`)).resolves.toBe("2") ); }); }); }); - }, + } ); diff --git a/packages/wrangler/e2e/deployments.test.ts b/packages/wrangler/e2e/deployments.test.ts index 3ebbd528a1..c02930a620 100644 --- a/packages/wrangler/e2e/deployments.test.ts +++ b/packages/wrangler/e2e/deployments.test.ts @@ -65,7 +65,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( const response = await retry( (resp) => !resp.ok, - async () => await fetch(deployedUrl), + async () => await fetch(deployedUrl) ); await expect(response.text()).resolves.toEqual("Hello World!"); }); @@ -100,7 +100,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( const response = await retry( (resp) => !resp.ok, - async () => await fetch(deployedUrl), + async () => await fetch(deployedUrl) ); await expect(response.text()).resolves.toEqual("Updated Worker!"); }); @@ -129,7 +129,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( it("rolls back", async ({ expect }) => { const output = await helper.run( - `wrangler rollback --message "A test message"`, + `wrangler rollback --message "A test message"` ); expect(normalizeOutput(output.stdout)).toMatchInlineSnapshot(` "├ Fetching latest deployment @@ -195,7 +195,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( Message: -" `); }); - }, + } ); type AssetTestCase = { @@ -232,22 +232,22 @@ async function checkAssets(testCases: AssetTestCase[], deployedUrl: string) { if (testCase.content) { expect( text, - `expected content for ${testCase.path} to be ${testCase.content}`, + `expected content for ${testCase.path} to be ${testCase.content}` ).toContain(testCase.content); } if (testCase.redirect) { expect( new URL(url).pathname, - `expected redirect for ${testCase.path} to be ${testCase.redirect}`, + `expected redirect for ${testCase.path} to be ${testCase.redirect}` ).toEqual(new URL(testCase.redirect, deployedUrl).pathname); } else { expect( new URL(url).pathname, - `unexpected pathname for ${testCase.path}`, + `unexpected pathname for ${testCase.path}` ).toEqual(new URL(testCase.path, deployedUrl).pathname); } }, - { timeout: 40_000 }, + { timeout: 40_000 } ); } } @@ -320,7 +320,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)("Workers + Assets deployment", () => { const r = await fetch(new URL("/try-404", deployedUrl)); const temp = { text: await r.text(), status: r.status }; return temp; - }, + } ); expect(text).toBeFalsy(); }); @@ -393,7 +393,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)("Workers + Assets deployment", () => { const r = await fetch(new URL("/try-404", deployedUrl)); const temp = { text: await r.text(), status: r.status }; return temp; - }, + } ); expect(text).toContain("

404.html

"); }); @@ -419,7 +419,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)("Workers + Assets deployment", () => { validateAssetUploadLogs( output, ["/404.html", "/index.html", "/[boop].html"], - { includeDebug: true }, + { includeDebug: true } ); const deployedUrl = getDeployedUrl(output); @@ -452,7 +452,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)("Workers + Assets deployment", () => { const r = await fetch(new URL("/try-404", deployedUrl)); const temp = { text: await r.text(), status: r.status }; return temp; - }, + } ); expect(text).toBeFalsy(); }); @@ -593,10 +593,10 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)("Workers + Assets deployment", () => { afterEach(async () => { // clean up dispatch Worker await helper.bestEffortRun( - `wrangler delete -c dispatch-worker/wrangler.toml`, + `wrangler delete -c dispatch-worker/wrangler.toml` ); await helper.bestEffortRun( - `wrangler dispatch-namespace delete ${dispatchNamespaceName}`, + `wrangler dispatch-namespace delete ${dispatchNamespaceName}` ); }); @@ -615,16 +615,16 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)("Workers + Assets deployment", () => { // create a dispatch namespace && verify output let output = await helper.run( - `wrangler dispatch-namespace create ${dispatchNamespaceName}`, + `wrangler dispatch-namespace create ${dispatchNamespaceName}` ); let normalizedStdout = normalizeOutput(output.stdout); expect(normalizedStdout).toContain( - `Created dispatch namespace "tmp-e2e-dispatch-00000000-0000-0000-0000-000000000000" with ID "00000000-0000-0000-0000-000000000000"`, + `Created dispatch namespace "tmp-e2e-dispatch-00000000-0000-0000-0000-000000000000" with ID "00000000-0000-0000-0000-000000000000"` ); // upload user Worker to the dispatch namespace && verify output output = await helper.run( - `wrangler deploy --dispatch-namespace ${dispatchNamespaceName}`, + `wrangler deploy --dispatch-namespace ${dispatchNamespaceName}` ); validateAssetUploadLogs(output, [ "/404.html", @@ -634,7 +634,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)("Workers + Assets deployment", () => { // deploy dispatch Worker && verify output output = await helper.run( - `wrangler deploy -c dispatch-worker/wrangler.toml`, + `wrangler deploy -c dispatch-worker/wrangler.toml` ); normalizedStdout = normalizeOutput(output.stdout); expect(normalizedStdout).toEqual(`Total Upload: xx KiB / gzip: xx KiB @@ -676,7 +676,7 @@ Current Version ID: 00000000-0000-0000-0000-000000000000`); const r = await fetch(new URL("/try-404", deployedUrl)); const temp = { text: await r.text(), status: r.status }; return temp; - }, + } ); expect(text).toBeFalsy(); }); @@ -712,16 +712,16 @@ Current Version ID: 00000000-0000-0000-0000-000000000000`); // create a dispatch namespace && verify output let output = await helper.run( - `wrangler dispatch-namespace create ${dispatchNamespaceName}`, + `wrangler dispatch-namespace create ${dispatchNamespaceName}` ); let normalizedStdout = normalizeOutput(output.stdout); expect(normalizedStdout).toContain( - `Created dispatch namespace "tmp-e2e-dispatch-00000000-0000-0000-0000-000000000000" with ID "00000000-0000-0000-0000-000000000000"`, + `Created dispatch namespace "tmp-e2e-dispatch-00000000-0000-0000-0000-000000000000" with ID "00000000-0000-0000-0000-000000000000"` ); // upload user Worker to the dispatch namespace && verify output output = await helper.run( - `wrangler deploy --dispatch-namespace ${dispatchNamespaceName}`, + `wrangler deploy --dispatch-namespace ${dispatchNamespaceName}` ); validateAssetUploadLogs(output, [ "/404.html", @@ -731,7 +731,7 @@ Current Version ID: 00000000-0000-0000-0000-000000000000`); // deploy dispatch Worker && verify output output = await helper.run( - `wrangler deploy -c dispatch-worker/wrangler.toml`, + `wrangler deploy -c dispatch-worker/wrangler.toml` ); normalizedStdout = normalizeOutput(output.stdout); expect(normalizedStdout).toEqual(`Total Upload: xx KiB / gzip: xx KiB @@ -776,7 +776,7 @@ Current Version ID: 00000000-0000-0000-0000-000000000000`); const r = await fetch(new URL("/try-404", deployedUrl)); const temp = { text: await r.text(), status: r.status }; return temp; - }, + } ); expect(text).toContain("

404.html

"); }); @@ -807,16 +807,16 @@ Current Version ID: 00000000-0000-0000-0000-000000000000`); // create a dispatch namespace && verify output let output = await helper.run( - `wrangler dispatch-namespace create ${dispatchNamespaceName}`, + `wrangler dispatch-namespace create ${dispatchNamespaceName}` ); let normalizedStdout = normalizeOutput(output.stdout); expect(normalizedStdout).toContain( - `Created dispatch namespace "tmp-e2e-dispatch-00000000-0000-0000-0000-000000000000" with ID "00000000-0000-0000-0000-000000000000"`, + `Created dispatch namespace "tmp-e2e-dispatch-00000000-0000-0000-0000-000000000000" with ID "00000000-0000-0000-0000-000000000000"` ); // upload user Worker to the dispatch namespace && verify output output = await helper.run( - `wrangler deploy --dispatch-namespace ${dispatchNamespaceName}`, + `wrangler deploy --dispatch-namespace ${dispatchNamespaceName}` ); validateAssetUploadLogs(output, [ "/404.html", @@ -826,7 +826,7 @@ Current Version ID: 00000000-0000-0000-0000-000000000000`); // deploy dispatch Worker && verify output output = await helper.run( - `wrangler deploy -c dispatch-worker/wrangler.toml`, + `wrangler deploy -c dispatch-worker/wrangler.toml` ); normalizedStdout = normalizeOutput(output.stdout); expect(normalizedStdout).toEqual(`Total Upload: xx KiB / gzip: xx KiB @@ -956,7 +956,7 @@ describe.skipIf(skipContainersTest)("containers", () => { // clean up user Worker after each test const deleteWorker = helper.bestEffortRun(`wrangler delete`); const deleteContainer = helper.bestEffortRun( - `wrangler containers delete ${applicationId}`, + `wrangler containers delete ${applicationId}` ); await Promise.allSettled([deleteWorker, deleteContainer]); }); @@ -970,14 +970,14 @@ describe.skipIf(skipContainersTest)("containers", () => { deployedUrl = getDeployedUrl(outputOne); const matchApplicationId = outputOne.stdout.match( - /([(]Application ID: (?.+?)[)])/, + /([(]Application ID: (?.+?)[)])/ ); applicationId = matchApplicationId?.groups?.applicationId; assert(matchApplicationId?.groups); const outputTwo = await helper.run(`wrangler deploy`); expect(outputTwo.stdout).toContain(`No changes to be made`); - }, + } ); it( @@ -991,16 +991,16 @@ describe.skipIf(skipContainersTest)("containers", () => { }); if (!response.ok) { throw new Error( - "Durable object transient error: " + (await response.text()), + "Durable object transient error: " + (await response.text()) ); } expect(await response.text()).toEqual("hello from container"); }, // big timeout for containers (2m) - { timeout: 60 * 2 * 1000, interval: 1000 }, + { timeout: 60 * 2 * 1000, interval: 1000 } ); - }, + } ); }); @@ -1009,7 +1009,7 @@ describe.skipIf(skipContainersTest)("containers", () => { */ function getDeployedUrl(output: { stdout: string }) { const match = output.stdout.match( - /(?https:\/\/tmp-e2e-.+?\..+?\.workers\.dev)/, + /(?https:\/\/tmp-e2e-.+?\..+?\.workers\.dev)/ ); assert(match?.groups); return match.groups.url; diff --git a/packages/wrangler/e2e/dev-registry.test.ts b/packages/wrangler/e2e/dev-registry.test.ts index 7590fb66c3..80215549c5 100644 --- a/packages/wrangler/e2e/dev-registry.test.ts +++ b/packages/wrangler/e2e/dev-registry.test.ts @@ -1,6 +1,6 @@ import getPort from "get-port"; import dedent from "ts-dedent"; -import { fetch, Request } from "undici"; +import { fetch } from "undici"; import { beforeEach, describe, it } from "vitest"; import { WranglerE2ETestHelper } from "./helpers/e2e-wrangler-test"; import { fetchJson } from "./helpers/fetch-json"; @@ -145,7 +145,7 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { const { url } = await worker.waitForReady(5_000); await expect(fetch(url).then((r) => r.text())).resolves.toBe( - "hello world", + "hello world" ); }); @@ -158,13 +158,13 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { const { url } = await workerA.waitForReady(5_000); await waitForLong(() => - expect(fetchText(url)).resolves.toBe("hello world"), + expect(fetchText(url)).resolves.toBe("hello world") ); await waitFor(async () => expect(normalizeOutput(workerA.currentOutput)).toContain( - "connect to other Wrangler or Vite dev processes running locally", - ), + "connect to other Wrangler or Vite dev processes running locally" + ) ); }); @@ -176,7 +176,7 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { await workerB.waitForReady(5_000); await waitForLong(() => - expect(fetchText(url)).resolves.toBe("hello world"), + expect(fetchText(url)).resolves.toBe("hello world") ); }); }); @@ -209,8 +209,8 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { await waitForLong(() => expect(fetchText(`${url}/service`)).resolves.toBe( - "Hello from service worker", - ), + "Hello from service worker" + ) ); }); @@ -227,10 +227,10 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { await waitForLong(() => expect(fetchText(`${url}/service`)).resolves.toBe( - "Hello from service worker", - ), + "Hello from service worker" + ) ); - }, + } ); }); @@ -329,7 +329,7 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { headers: { "X-Reset-Count": "true", }, - }), + }) ).resolves.toMatchObject({ count: 1 }); }); @@ -348,9 +348,9 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { headers: { "X-Reset-Count": "true", }, - }), + }) ).resolves.toMatchObject({ count: 1 }); - }, + } ); it("can fetch remote DO attached to a through b (start a, start b)", async ({ @@ -369,8 +369,8 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { headers: { "X-Reset-Count": "true", }, - }).then((r) => r.json()), - ).resolves.toMatchObject({ count: 1 }), + }).then((r) => r.json()) + ).resolves.toMatchObject({ count: 1 }) ); }); }); @@ -404,7 +404,7 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { const { url } = await worker.waitForReady(5_000); await expect(fetch(url).then((r) => r.text())).resolves.toBe( - "hello world", + "hello world" ); }); @@ -412,13 +412,13 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { const port = await getPort(); const worker = helper.runLongLived( `${cmd.replace("wrangler dev", "wrangler pages dev")} --port ${port}`, - { cwd: a }, + { cwd: a } ); const { url } = await worker.waitForReady(5_000); await expect(fetch(url).then((r) => r.text())).resolves.toBe( - "Hello from Pages", + "Hello from Pages" ); }); @@ -430,18 +430,18 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { const port = await getPort(); const workerA = helper.runLongLived( `${cmd.replace("wrangler dev", "wrangler pages dev")} --port ${port}`, - { cwd: a }, + { cwd: a } ); const { url } = await workerA.waitForReady(5_000); await waitForLong(() => - expect(fetchText(`${url}/service`)).resolves.toBe("hello world"), + expect(fetchText(`${url}/service`)).resolves.toBe("hello world") ); await waitFor(async () => expect(normalizeOutput(workerA.currentOutput)).toContain( - "connect to other Wrangler or Vite dev processes running locally", - ), + "connect to other Wrangler or Vite dev processes running locally" + ) ); }); @@ -449,7 +449,7 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { const port = await getPort(); const workerA = helper.runLongLived( `${cmd.replace("wrangler dev", "wrangler pages dev")} --port ${port}`, - { cwd: a }, + { cwd: a } ); const { url } = await workerA.waitForReady(5_000); @@ -457,7 +457,7 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { await workerB.waitForReady(5_000); await waitForLong(() => - expect(fetchText(`${url}/service`)).resolves.toBe("hello world"), + expect(fetchText(`${url}/service`)).resolves.toBe("hello world") ); }); @@ -471,7 +471,7 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { const port = await getPort(); const workerA = helper.runLongLived( `${cmd.replace("wrangler dev", "wrangler pages dev")} dist --service BEE=${workerName2} --port ${port}`, - { cwd: a }, + { cwd: a } ); const { url } = await workerA.waitForReady(5_000); @@ -479,7 +479,7 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { await workerB.waitForReady(5_000); await waitForLong(() => - expect(fetchText(`${url}/service`)).resolves.toBe("hello world"), + expect(fetchText(`${url}/service`)).resolves.toBe("hello world") ); }); }); diff --git a/packages/wrangler/e2e/dev.test.ts b/packages/wrangler/e2e/dev.test.ts index 0511447cc3..729b7d76c9 100644 --- a/packages/wrangler/e2e/dev.test.ts +++ b/packages/wrangler/e2e/dev.test.ts @@ -172,13 +172,13 @@ describe.each([ `, }); const worker = helper.runLongLived( - `${cmd} --show-interactive-dev-session=false`, + `${cmd} --show-interactive-dev-session=false` ); const { url } = await worker.waitForReady(); await waitForLong(() => - expect(fetch(url).then((r) => r.text())).resolves.toMatchSnapshot(), + expect(fetch(url).then((r) => r.text())).resolves.toMatchSnapshot() ); expect(worker.currentOutput).not.toContain("[b] open a browser"); @@ -214,7 +214,7 @@ describe.each([ const { url } = await worker.waitForReady(); await expect( - fetch(`${url}/__scheduled`).then((r) => r.text()), + fetch(`${url}/__scheduled`).then((r) => r.text()) ).resolves.toMatchSnapshot(); await worker.readUntil(/Event triggered/); @@ -247,7 +247,7 @@ describe.each([ const { url } = await worker.waitForReady(); await expect( - fetch(`${url}/__scheduled`).then((r) => r.text()), + fetch(`${url}/__scheduled`).then((r) => r.text()) ).resolves.toMatchSnapshot(); await worker.readUntil(/Event triggered/); @@ -293,10 +293,10 @@ describe.each([ // The warning should contain the actual port, not "undefined" await waitFor(() => { expect(worker.currentOutput).toContain( - "Scheduled Workers are not automatically triggered", + "Scheduled Workers are not automatically triggered" ); expect(worker.currentOutput).toContain( - `curl "http://${hostname}:${port}/cdn-cgi/handler/scheduled"`, + `curl "http://${hostname}:${port}/cdn-cgi/handler/scheduled"` ); expect(worker.currentOutput).not.toContain("undefined"); }); @@ -338,7 +338,7 @@ describe.each([ // The warning should NOT appear when testScheduled is enabled expect(worker.currentOutput).not.toContain( - "Scheduled Workers are not automatically triggered", + "Scheduled Workers are not automatically triggered" ); }); }); @@ -504,7 +504,7 @@ it.runIf(process.platform !== "win32")( expect(beginProcesses.length).toBe(0); expect(endProcesses.length).toBe(0); - }, + } ); // Skipping remote python tests because they consistently flake with timeouts @@ -560,7 +560,7 @@ describe.each([{ cmd: "wrangler dev" }])( async () => { const r = await fetch(url); return { text: await r.text(), status: r.status }; - }, + } ); expect(text).toBe("Updated Python Worker value"); @@ -612,7 +612,7 @@ describe.each([{ cmd: "wrangler dev" }])( async () => { const r = await fetch(url); return { text: await r.text(), status: r.status }; - }, + } ); expect(text).toBe("py hello world 5"); @@ -746,7 +746,7 @@ describe.each([{ cmd: "wrangler dev" }])( await worker.readUntil(/excluded_module not found/); await worker.readUntil(/end/); }); - }, + } ); describe.each(HYPERDRIVE_DATABASES)( @@ -992,7 +992,7 @@ describe.each(HYPERDRIVE_DATABASES)( const { url } = await worker.waitForReady(); await fetch(`${url}/connect`); - }, + } ); afterEach(() => { @@ -1000,7 +1000,7 @@ describe.each(HYPERDRIVE_DATABASES)( server.close(); } }); - }, + } ); describe("queue dev tests", () => { @@ -1067,7 +1067,7 @@ describe("writes debug logs to hidden file", () => { const worker = helper.runLongLived("wrangler dev --log-level debug"); const match = await worker.readUntil( - /🪵 {2}Writing logs to "(?.+\.log)"/, + /🪵 {2}Writing logs to "(?.+\.log)"/ ); const filepath = match.groups?.filepath; @@ -1156,7 +1156,7 @@ describe("analytics engine", () => { const text = await fetchText(url); expect(text).toContain( - `successfully wrote datapoint from module worker`, + `successfully wrote datapoint from module worker` ); }); }); @@ -1199,11 +1199,11 @@ describe("analytics engine", () => { const worker = helper.runLongLived(cmd); await worker.readUntil( - /Analytics Engine is not supported locally when using the service-worker format/, + /Analytics Engine is not supported locally when using the service-worker format/ ); }); }); - }, + } ); }); @@ -1273,7 +1273,7 @@ describe.skipIf(CLOUDFLARE_ACCOUNT_ID !== "8d783f274e1f82dc46744c297b015a2f")( const text = await fetchText(url); expect(text).toMatchInlineSnapshot( - `"https://wrangler-testing.testing.devprod.cloudflare.dev/"`, + `"https://wrangler-testing.testing.devprod.cloudflare.dev/"` ); }); @@ -1311,7 +1311,7 @@ describe.skipIf(CLOUDFLARE_ACCOUNT_ID !== "8d783f274e1f82dc46744c297b015a2f")( const text = await fetchText(url); expect(text).toMatchInlineSnapshot( - `"https://wrangler-testing.testing.devprod.cloudflare.dev/"`, + `"https://wrangler-testing.testing.devprod.cloudflare.dev/"` ); }); @@ -1350,7 +1350,7 @@ describe.skipIf(CLOUDFLARE_ACCOUNT_ID !== "8d783f274e1f82dc46744c297b015a2f")( await fetchText(url); await worker.readUntil(/ERROR/); }); - }, + } ); describe("custom builds", () => { @@ -1431,7 +1431,7 @@ describe("custom builds", () => { // assert no more custom builds happen // regression: https://github.com/cloudflare/workers-sdk/issues/6876 await expect( - worker.readUntil(/\[custom build\] Running/, 5_000), + worker.readUntil(/\[custom build\] Running/, 5_000) ).rejects.toThrowError(); // now check assets are still fetchable, even after updates @@ -1448,7 +1448,7 @@ describe("custom builds", () => { async () => { const res2 = await fetch(url); return res2.text(); - }, + } ); await expect(resText).toBe("world"); }); @@ -1501,11 +1501,11 @@ describe("watch mode", () => { (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" }])( @@ -1532,7 +1532,7 @@ describe("watch mode", () => { let { response, cachedETags } = await fetchWithETag( `${url}/index.html`, - {}, + {} ); const originalETag = response.headers.get("etag"); expect(await response.text()).toBe("

Hello Workers + Assets

"); @@ -1547,10 +1547,10 @@ describe("watch mode", () => { (s) => s.response.status !== 200, async () => { return await fetchWithETag(`${url}/index.html`, cachedETags); - }, + } )); expect(await response.text()).toBe( - "

Hello Updated Workers + Assets

", + "

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); @@ -1576,7 +1576,7 @@ describe("watch mode", () => { const { url } = await worker.waitForReady(); let { response, cachedETags } = await fetchWithETag( `${url}/index.html`, - {}, + {} ); expect(await response.text()).toBe("

Hello Workers + Assets

"); @@ -1594,20 +1594,20 @@ describe("watch mode", () => { (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, + 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, + cachedETags )); expect(response.status).toBe(304); }); @@ -1634,18 +1634,18 @@ describe("watch mode", () => { 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, + cachedETags )); expect(await response.text()).toBe("About Workers + Assets"); ({ response, cachedETags } = await fetchWithETag( `${url}/workers/index.html`, - cachedETags, + cachedETags )); expect(await response.text()).toBe("Cloudflare Workers!"); @@ -1659,7 +1659,7 @@ describe("watch mode", () => { (s) => s.response.status !== 404, async () => { return await fetchWithETag(`${url}/index.html`, cachedETags); - }, + } )); expect(response.status).toBe(404); }); @@ -1709,7 +1709,7 @@ describe("watch mode", () => { (r) => r.status !== 302, async () => { return await fetch(`${url}/foo`, { redirect: "manual" }); - }, + } ); expect(response.status).toBe(302); expect(response.headers.get("Location")).toBe("/bar"); @@ -1744,7 +1744,7 @@ describe("watch mode", () => { let { response, cachedETags } = await fetchWithETag( `${url}/index.html`, - {}, + {} ); expect(await response.text()).toBe("

Hello Workers + Assets

"); @@ -1764,15 +1764,15 @@ describe("watch mode", () => { (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

", + "

Read more about Workers + Assets

" ); }); @@ -1828,7 +1828,7 @@ describe("watch mode", () => { status: fetchResponse.status, text: await fetchResponse.text(), }; - }, + } ); expect(status).toBe(200); expect(text).toBe("

Hello Workers + Assets

"); @@ -1895,7 +1895,7 @@ describe("watch mode", () => { status: fetchResponse.status, text: await fetchResponse.text(), }; - }, + } ); expect(status).toBe(200); expect(text).toBe("

Hello Workers + Assets

"); @@ -2108,10 +2108,10 @@ describe("watch mode", () => { `, }); await worker.readUntil( - /Warning: The following routes will attempt to serve Assets on a configured path:/, + /Warning: The following routes will attempt to serve Assets on a configured path:/ ); }); - }, + } ); describe.each([{ cmd: "wrangler dev --assets=dist" }])( @@ -2135,17 +2135,17 @@ describe("watch mode", () => { 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, + cachedETags )); expect(await response.text()).toBe( - "

Read more about Workers + Assets

", + "

Read more about Workers + Assets

" ); // change + add @@ -2164,23 +2164,23 @@ describe("watch mode", () => { (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, + cachedETags )); expect(await response.text()).toBe( - "

Hello Updated Workers + Assets

", + "

Hello Updated Workers + Assets

" ); expect(response.headers.get("etag")).not.toBe(originalETag); // unchanged -> expect 304 ({ response, cachedETags } = await fetchWithETag( `${url}/about.html`, - cachedETags, + cachedETags )); expect(response.status).toBe(304); @@ -2195,7 +2195,7 @@ describe("watch mode", () => { (s) => s.response.status !== 404, async () => { return await fetchWithETag(`${url}/about.html`, cachedETags); - }, + } )); expect(response.status).toBe(404); }); @@ -2330,10 +2330,10 @@ describe("watch mode", () => { `, }); await worker.readUntil( - /Warning: The following routes will attempt to serve Assets on a configured path:/, + /Warning: The following routes will attempt to serve Assets on a configured path:/ ); }); - }, + } ); }); @@ -2387,24 +2387,24 @@ This is a random email body. 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:", + "Email handler replied to sender with the following message:" ); }); const pathRegexp = new RegExp( - "Email handler replied to sender with the following message:\\s*(\\S*)", + "Email handler replied to sender with the following message:\\s*(\\S*)" ); const maybeReplyPath = await vi.waitUntil( () => pathRegexp.exec(worker.currentOutput)?.[1], - { interval: 100, timeout: 5000 }, + { interval: 100, timeout: 5000 } ); expect(await readFile(maybeReplyPath, "utf-8")).toMatchInlineSnapshot(` @@ -2455,11 +2455,11 @@ 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"`, + `"Worker rejected email with the following reason: I dont like this email"` ); expect(response.status).toBe(400); @@ -2499,14 +2499,14 @@ 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`, + `Email handler forwarded message with` ); expect(worker.currentOutput).toContain(`rcptTo: mark.s@example.com`); }); @@ -2555,24 +2555,24 @@ 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", - ), + "send_email binding called with the following message" + ) ); const pathRegexp = new RegExp( - "send_email binding called with the following message:\\s*(\\S*)", + "send_email binding called with the following message:\\s*(\\S*)" ); const maybeReplyPath = await vi.waitUntil( () => pathRegexp.exec(worker.currentOutput)?.[1], - { interval: 100, timeout: 5000 }, + { interval: 100, timeout: 5000 } ); expect(await readFile(maybeReplyPath, "utf-8")).toMatchInlineSnapshot(` @@ -2865,13 +2865,13 @@ describe(".env support in local dev", () => { // 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"', + '"CLOUDFLARE_INCLUDE_PROCESS_ENV": "true"' ); expect(await (await fetch(url)).text()).contains( - '"WRANGLER_ENV_VAR_0": "default-0"', + '"WRANGLER_ENV_VAR_0": "default-0"' ); expect(await (await fetch(url)).text()).contains( - '"WRANGLER_ENV_VAR_1": "env-1"', + '"WRANGLER_ENV_VAR_1": "env-1"' ); }); @@ -2906,7 +2906,7 @@ describe(".env support in local dev", () => { }); const worker = helper.runLongLived( - "wrangler dev --env-file=other/.env --env-file=other/.env.local", + "wrangler dev --env-file=other/.env --env-file=other/.env.local" ); const { url } = await worker.waitForReady(); expect(await (await fetch(url)).text()).toMatchInlineSnapshot(` diff --git a/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts b/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts index 9f96f85b2f..ea8faadb29 100644 --- a/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts +++ b/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts @@ -3,7 +3,7 @@ import crypto from "node:crypto"; import { cp } from "node:fs/promises"; import { setTimeout } from "node:timers/promises"; import { fetch } from "undici"; -import { expect, onTestFinished } from "vitest"; +import { onTestFinished } from "vitest"; import { generateLeafCertificate, generateMtlsCertName, @@ -40,8 +40,8 @@ export class WranglerE2ETestHelper { /** Provide an alternative to `onTestFinished` to handle tearing down resources. */ public readonly onTeardown: ( fn: () => Awaitable, - timeoutMs?: number, - ) => void = onTestFinished, + timeoutMs?: number + ) => void = onTestFinished ) {} /** A temporary directory where files will be seeded and commands will be run. */ @@ -51,7 +51,7 @@ export class WranglerE2ETestHelper { async seed(files: Record): Promise; async seed(sourceDir: string): Promise; async seed( - filesOrSourceDir: Record | string, + filesOrSourceDir: Record | string ): Promise { if (typeof filesOrSourceDir === "string") { await cp(filesOrSourceDir, this.tmpPath, { recursive: true }); @@ -72,7 +72,7 @@ export class WranglerE2ETestHelper { cwd = this.tmpPath, stopOnTestFinished = true, ...options - }: WranglerCommandOptions & { stopOnTestFinished?: boolean } = {}, + }: WranglerCommandOptions & { stopOnTestFinished?: boolean } = {} ): WranglerLongLivedCommand { const wrangler = new WranglerLongLivedCommand(wranglerCommand, { cwd, @@ -89,7 +89,7 @@ export class WranglerE2ETestHelper { /** Run a Wrangler command that will execute and exit, such as `wrangler whoami` */ async run( wranglerCommand: string, - { cwd = this.tmpPath, ...options }: WranglerCommandOptions = {}, + { cwd = this.tmpPath, ...options }: WranglerCommandOptions = {} ) { return runWrangler(wranglerCommand, { cwd, ...options }); } @@ -103,14 +103,14 @@ export class WranglerE2ETestHelper { */ async bestEffortRun( wranglerCommand: string, - { timeout = 5_000, ...options }: WranglerCommandOptions = {}, + { timeout = 5_000, ...options }: WranglerCommandOptions = {} ) { try { return await this.run(wranglerCommand, { timeout, ...options }); } catch (e) { console.warn( `Best-effort cleanup "${wranglerCommand}" failed:`, - e instanceof Error ? e.message : e, + e instanceof Error ? e.message : e ); return undefined; } @@ -139,7 +139,7 @@ export class WranglerE2ETestHelper { const name = generateResourceName("dispatch"); if (isLocal) { throw new Error( - "Dispatch namespaces are not supported in local mode (yet)", + "Dispatch namespaces are not supported in local mode (yet)" ); } await this.run(`wrangler dispatch-namespace create ${name}`); @@ -192,7 +192,7 @@ export class WranglerE2ETestHelper { const name = resourceName ?? generateResourceName("vectorize"); if (!resourceName) { await this.run( - `wrangler vectorize create ${name} --dimensions ${dimensions} --metric ${metric}`, + `wrangler vectorize create ${name} --dimensions ${dimensions} --metric ${metric}` ); } this.onTeardown(async () => { @@ -207,7 +207,7 @@ export class WranglerE2ETestHelper { /** Create a Hyperdrive connection and clean it up during tear-down. */ async hyperdrive( isLocal: boolean, - scheme: "postgresql" | "mysql" = "postgresql", + scheme: "postgresql" | "mysql" = "postgresql" ): Promise<{ id: string; name: string }> { const name = generateResourceName("hyperdrive"); @@ -223,11 +223,11 @@ export class WranglerE2ETestHelper { assert( connectionString, - `${envVar} must be set in order to create a Hyperdrive resource for this test`, + `${envVar} must be set in order to create a Hyperdrive resource for this test` ); const result = await this.run( - `wrangler hyperdrive create ${name} --connection-string="${connectionString}"`, + `wrangler hyperdrive create ${name} --connection-string="${connectionString}"` ); const tomlMatch = /id = "([0-9a-f]{32})"/.exec(result.stdout); const jsonMatch = /"id": "([0-9a-f]{32})"/.exec(result.stdout); @@ -255,7 +255,7 @@ export class WranglerE2ETestHelper { const name = generateMtlsCertName(); const output = await this.run( - `wrangler cert upload mtls-certificate --name ${name} --cert "mtls_client_cert_file.pem" --key "mtls_client_private_key_file.pem"`, + `wrangler cert upload mtls-certificate --name ${name} --cert "mtls_client_cert_file.pem" --key "mtls_client_private_key_file.pem"` ); const match = output.stdout.match(/ID:\s+(?.*)$/m); const certificateId = match?.groups?.certId; @@ -302,7 +302,10 @@ export class WranglerE2ETestHelper { ); await waitForLong(async () => { const response = await fetch(deployedUrl); - expect(response.status).toBe(200); + assert( + response.status === 200, + `Expected status 200 but got ${response.status}` + ); }); } @@ -350,11 +353,11 @@ export class WranglerE2ETestHelper { const configOption = configPath ? `-c ${configPath}` : ""; const workerNameOption = `--name ${workerName}`; const { stdout } = await this.run( - `wrangler deploy ${entryPoint} ${workerNameOption} ${configOption} --compatibility-date 2025-01-01 ${extraFlags.join(" ")}`, + `wrangler deploy ${entryPoint} ${workerNameOption} ${configOption} --compatibility-date 2025-01-01 ${extraFlags.join(" ")}` ); const urlMatcher = new RegExp( - `(?https:\\/\\/${workerName}\\..+?\\.workers\\.dev)`, + `(?https:\\/\\/${workerName}\\..+?\\.workers\\.dev)` ); const deployedUrl = stdout.match(urlMatcher)?.groups?.url; @@ -367,7 +370,10 @@ export class WranglerE2ETestHelper { // Wait for the worker to become available await waitForLong(async () => { const response = await fetch(deployedUrl); - expect(response.status).toBe(200); + assert( + response.status === 200, + `Expected status 200 but got ${response.status}` + ); }); const cleanup = async () => { @@ -380,7 +386,7 @@ export class WranglerE2ETestHelper { } catch (e) { throw new Error( "Failed to register cleanup for worker.\nPerhaps you called this outside an `it` block?\nIf so, pass `cleanOnTestFinished: false` and then use the returned `cleanup` helper yourself", - { cause: e }, + { cause: e } ); } return { deployedUrl, stdout }; diff --git a/packages/wrangler/e2e/multiworker-dev.test.ts b/packages/wrangler/e2e/multiworker-dev.test.ts index c9ea074676..015e1c936f 100644 --- a/packages/wrangler/e2e/multiworker-dev.test.ts +++ b/packages/wrangler/e2e/multiworker-dev.test.ts @@ -192,19 +192,19 @@ describe("multiworker", () => { const { url } = await worker.waitForReady(5_000); await expect(fetch(url).then((r) => r.text())).resolves.toBe( - "hello world", + "hello world" ); }); it("can fetch b through a", async ({ expect }) => { const workerA = helper.runLongLived( `wrangler dev -c wrangler.toml -c ${b}/wrangler.toml`, - { cwd: a }, + { cwd: a } ); const { url } = await workerA.waitForReady(5_000); await waitForLong( - async () => await expect(fetchText(url)).resolves.toBe("hello world"), + async () => await expect(fetchText(url)).resolves.toBe("hello world") ); }); @@ -213,19 +213,19 @@ describe("multiworker", () => { }) => { const workerA = helper.runLongLived( `wrangler dev -c wrangler.toml -c ${b}/wrangler.toml`, - { cwd: a }, + { cwd: a } ); const { url } = await workerA.waitForReady(5_000); await waitForLong( - async () => await expect(fetchText(`${url}/count`)).resolves.toBe("6"), + async () => await expect(fetchText(`${url}/count`)).resolves.toBe("6") ); }); it("can access service props through a binding", async ({ expect }) => { const workerA = helper.runLongLived( `wrangler dev -c wrangler.toml -c ${b}/wrangler.toml`, - { cwd: a }, + { cwd: a } ); const { url } = await workerA.waitForReady(5_000); @@ -257,7 +257,7 @@ describe("multiworker", () => { const workerA = helper.runLongLived( `wrangler dev -c wrangler.toml -c ${b}/wrangler.toml`, - { cwd: a }, + { cwd: a } ); const { url } = await workerA.waitForReady(5_000); @@ -265,8 +265,8 @@ describe("multiworker", () => { await waitForLong( async () => await expect(fetchText(url)).resolves.toBe( - `Couldn't find a local dev session for the "default" entrypoint of service "${service}" to proxy to`, - ), + `Couldn't find a local dev session for the "default" entrypoint of service "${service}" to proxy to` + ) ); }); }); @@ -289,15 +289,15 @@ describe("multiworker", () => { it("can fetch service worker c through a", async ({ expect }) => { const workerA = helper.runLongLived( `wrangler dev -c wrangler.toml -c ${c}/wrangler.toml`, - { cwd: a }, + { cwd: a } ); const { url } = await workerA.waitForReady(5_000); await waitForLong( async () => await expect(fetchText(`${url}/service`)).resolves.toBe( - "Hello from service worker", - ), + "Hello from service worker" + ) ); }); }); @@ -335,14 +335,14 @@ describe("multiworker", () => { headers: { "X-Reset-Count": "true", }, - }), + }) ).resolves.toMatchObject({ count: 1 }); }); it("can fetch remote DO attached to a through b", async ({ expect }) => { const workerB = helper.runLongLived( `wrangler dev -c wrangler.toml -c ${a}/wrangler.toml`, - { cwd: b }, + { cwd: b } ); const { url } = await workerB.waitForReady(5_000); @@ -351,7 +351,7 @@ describe("multiworker", () => { headers: { "X-Reset-Count": "true", }, - }), + }) ).resolves.toMatchObject({ count: 1 }); }); @@ -360,15 +360,15 @@ describe("multiworker", () => { }) => { const workerB = helper.runLongLived( `wrangler dev -c wrangler.toml -c ${a}/wrangler.toml`, - { cwd: b }, + { cwd: b } ); const { url } = await workerB.waitForReady(5_000); await waitForLong( async () => await expect(fetchText(`${url}/do-rpc`)).resolves.toBe( - "Hello through DO RPC", - ), + "Hello through DO RPC" + ) ); }); }); @@ -422,14 +422,14 @@ describe("multiworker", () => { it("tail event sent to b", async ({ expect }) => { const worker = helper.runLongLived( `wrangler dev -c wrangler.toml -c ${b}/wrangler.toml`, - { cwd: a }, + { cwd: a } ); const { url } = await worker.waitForReady(5_000); await expect(fetchText(`${url}`)).resolves.toBe("hello from a"); await waitFor(() => - expect(worker.currentOutput).includes("received tail event"), + expect(worker.currentOutput).includes("received tail event") ); }); }); @@ -482,15 +482,15 @@ describe("multiworker", () => { it("logs tail event sent to b", async ({ expect }) => { const worker = helper.runLongLived( `wrangler dev -c wrangler.toml -c ${b}/wrangler.toml`, - { cwd: a }, + { cwd: a } ); const { url } = await worker.waitForReady(5_000); await waitForLong(() => - expect(fetchText(`${url}`)).resolves.toBe("hello from a"), + expect(fetchText(`${url}`)).resolves.toBe("hello from a") ); await waitFor(() => - expect(worker.currentOutput).includes("received tail stream event"), + expect(worker.currentOutput).includes("received tail stream event") ); }); }); @@ -526,43 +526,43 @@ describe("multiworker", () => { it("pages project assets", async ({ expect }) => { const pages = helper.runLongLived( `wrangler pages dev -c wrangler.toml -c ${b}/wrangler.toml -c ${c}/wrangler.toml`, - { cwd: a }, + { cwd: a } ); const { url } = await pages.waitForReady(5_000); await waitForLong( async () => await expect(fetchText(`${url}`)).resolves.toBe( - "

hello pages assets

", - ), + "

hello pages assets

" + ) ); }); it("pages project fetching service worker", async ({ expect }) => { const pages = helper.runLongLived( `wrangler pages dev -c wrangler.toml -c ${b}/wrangler.toml -c ${c}/wrangler.toml`, - { cwd: a }, + { cwd: a } ); const { url } = await pages.waitForReady(5_000); await waitForLong( async () => await expect(fetchText(`${url}/cee`)).resolves.toBe( - "Hello from service worker", - ), + "Hello from service worker" + ) ); }); it("pages project fetching module worker", async ({ expect }) => { const pages = helper.runLongLived( `wrangler pages dev -c wrangler.toml -c ${b}/wrangler.toml -c ${c}/wrangler.toml`, - { cwd: a }, + { cwd: a } ); const { url } = await pages.waitForReady(5_000); await waitForLong( async () => - await expect(fetchText(`${url}/bee`)).resolves.toBe("hello world"), + await expect(fetchText(`${url}/bee`)).resolves.toBe("hello world") ); }); @@ -571,10 +571,10 @@ describe("multiworker", () => { }) => { const pages = helper.runLongLived( `wrangler pages dev -c wrangler.toml -c wrangler.toml`, - { cwd: a }, + { cwd: a } ); await pages.readUntil( - /You cannot use a Pages project as a service binding target/, + /You cannot use a Pages project as a service binding target/ ); }); }); @@ -633,7 +633,7 @@ describe("multiworker", () => { }) => { const worker = helper.runLongLived( `wrangler dev -c wrangler.toml -c ${b}/wrangler.toml`, - { cwd: a }, + { cwd: a } ); const { url } = await worker.waitForReady(5_000); @@ -642,10 +642,10 @@ describe("multiworker", () => { await waitFor(() => { // The warning should contain the actual port, not "undefined" expect(worker.currentOutput).toContain( - "Scheduled Workers are not automatically triggered", + "Scheduled Workers are not automatically triggered" ); expect(worker.currentOutput).toContain( - `curl "http://${hostname}:${port}/cdn-cgi/handler/scheduled"`, + `curl "http://${hostname}:${port}/cdn-cgi/handler/scheduled"` ); expect(worker.currentOutput).not.toContain("undefined"); }); diff --git a/packages/wrangler/e2e/remote-binding/dev-remote-bindings.test.ts b/packages/wrangler/e2e/remote-binding/dev-remote-bindings.test.ts index f7fc8f95f5..42d5f325db 100644 --- a/packages/wrangler/e2e/remote-binding/dev-remote-bindings.test.ts +++ b/packages/wrangler/e2e/remote-binding/dev-remote-bindings.test.ts @@ -86,38 +86,38 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( const { url } = await worker.waitForReady(); await expect(fetchText(url)).resolves.toMatchInlineSnapshot( - `"REMOTE: Hello from a remote worker"`, + `"REMOTE: Hello from a remote worker"` ); const indexContent = await readFile( `${helper.tmpPath}/simple-service-binding.js`, - "utf8", + "utf8" ); await writeFile( `${helper.tmpPath}/simple-service-binding.js`, indexContent.replace( "REMOTE:", - "The remote worker responded with:", + "The remote worker responded with:" ), - "utf8", + "utf8" ); await setTimeout(500); await expect(fetchText(url)).resolves.toMatchInlineSnapshot( - `"The remote worker responded with: Hello from a remote worker"`, + `"The remote worker responded with: Hello from a remote worker"` ); await writeFile( `${helper.tmpPath}/simple-service-binding.js`, indexContent, - "utf8", + "utf8" ); await setTimeout(500); await expect(fetchText(url)).resolves.toMatchInlineSnapshot( - `"REMOTE: Hello from a remote worker"`, + `"REMOTE: Hello from a remote worker"` ); }); @@ -170,7 +170,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( const { url } = await worker.waitForReady(); await expect(fetchText(url)).resolves.toContain( - "This is a response from Workers AI.", + "This is a response from Workers AI." ); // This should only include logs from the user Wrangler session (i.e. a single list of attached bindings, and only one ready message) @@ -183,7 +183,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( ⎔ Starting local server... [wrangler:info] Ready on http://: [wrangler:info] GET / 200 OK (TIMINGS) - `), + `) ); }); @@ -210,8 +210,8 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( await waitFor(() => expect(worker.currentOutput).toContain( - "Service binding 'REMOTE_WORKER' references Worker 'non-existent-service-binding' which was not found.", - ), + "Service binding 'REMOTE_WORKER' references Worker 'non-existent-service-binding' which was not found." + ) ); }); @@ -237,8 +237,8 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( await waitFor(() => expect(worker.currentOutput).toContain( - "KV namespace 'non-existent-kv' is not valid.", - ), + "KV namespace 'non-existent-kv' is not valid." + ) ); }); }); @@ -291,7 +291,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( }); const worker = helper.runLongLived( - `wrangler dev -c wrangler.json -c ${localTest}/wrangler.json`, + `wrangler dev -c wrangler.json -c ${localTest}/wrangler.json` ); const { url } = await worker.waitForReady(); @@ -303,7 +303,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( `); }); }); - }, + } ); async function spawnLocalWorker(helper: WranglerE2ETestHelper): Promise { @@ -325,7 +325,7 @@ async function spawnLocalWorker(helper: WranglerE2ETestHelper): Promise { // Note: we use a random port here otherwise for some reason in CI windows // allows the default port to be overridden by other processes `wrangler dev --port ${await getPort()}`, - { cwd: local }, + { cwd: local } ); await localWorker.waitForReady(); } diff --git a/packages/wrangler/e2e/unenv-preset/preset.test.ts b/packages/wrangler/e2e/unenv-preset/preset.test.ts index 84e36158bb..eb6719a065 100644 --- a/packages/wrangler/e2e/unenv-preset/preset.test.ts +++ b/packages/wrangler/e2e/unenv-preset/preset.test.ts @@ -1,6 +1,7 @@ +import assert from "node:assert"; import { join } from "node:path"; import { fetch } from "undici"; -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, describe, test } from "vitest"; import { CLOUDFLARE_ACCOUNT_ID } from "../helpers/account-id"; import { WranglerE2ETestHelper } from "../helpers/e2e-wrangler-test"; import { generateResourceName } from "../helpers/generate-resource-name"; @@ -868,15 +869,23 @@ describe.each(groupedLocalConfigs)( // Wait for the Worker to be actually responding. const readyResp = await retry( (resp) => !resp.ok, - async () => await fetch(`${url}/ping`), + async () => await fetch(`${url}/ping`) + ); + const responseText = await readyResp.text(); + assert( + responseText === "pong", + `Expected "pong" but got "${responseText}"` ); - await expect(readyResp.text()).resolves.toEqual("pong"); // Assert runtime flag values for await (const [flag, value] of Object.entries(expectRuntimeFlags)) { const flagResp = await fetch(`${url}/flag?name=${flag}`); - expect(flagResp.ok).toEqual(true); - await expect(flagResp.json(), `flag "${flag}"`).resolves.toEqual(value); + assert(flagResp.ok, `Expected flag response to be ok for "${flag}"`); + const flagValue = await flagResp.json(); + assert( + flagValue === value, + `Expected flag "${flag}" to be ${value} but got ${flagValue}` + ); } return async () => await wrangler.stop(); @@ -894,11 +903,11 @@ describe.each(groupedLocalConfigs)( const body = await response.text(); expect(body).toMatch("passed"); }, - { timeout: 19_000 }, + { timeout: 19_000 } ); - }, + } ); - }, + } ); // Run the remote tests only if the user is logged in (e.g. not for a PR from a forked repo) @@ -926,15 +935,23 @@ describe.runIf(Boolean(CLOUDFLARE_ACCOUNT_ID))( // Wait for the Worker to be actually responding. const readyResp = await retry( (resp) => !resp.ok, - async () => await fetch(`${url}/ping`), + async () => await fetch(`${url}/ping`) + ); + const responseText = await readyResp.text(); + assert( + responseText === "pong", + `Expected "pong" but got "${responseText}"` ); - await expect(readyResp.text()).resolves.toEqual("pong"); // Assert runtime flag values for await (const flag of collectEnabledFlags(localTestConfigs)) { const flagResp = await fetch(`${url}/flag?name=${flag}`); - expect(flagResp.ok).toEqual(true); - await expect(flagResp.json(), `flag "${flag}"`).resolves.toEqual(false); + assert(flagResp.ok, `Expected flag response to be ok for "${flag}"`); + const flagValue = await flagResp.json(); + assert( + flagValue === false, + `Expected flag "${flag}" to be false but got ${flagValue}` + ); } return async () => await wrangler.stop(); @@ -952,11 +969,11 @@ describe.runIf(Boolean(CLOUDFLARE_ACCOUNT_ID))( const body = await response.text(); expect(body).toMatch("passed"); }, - { timeout: 19_000 }, + { timeout: 19_000 } ); - }, + } ); - }, + } ); // Run the remote tests only if the user is logged in (e.g. not for a PR from a forked repo) @@ -985,15 +1002,23 @@ describe.runIf(Boolean(CLOUDFLARE_ACCOUNT_ID))( // Wait for the Worker to be actually responding. const readyResp = await retry( (resp) => !resp.ok, - async () => await fetch(`${url}/ping`), + async () => await fetch(`${url}/ping`) + ); + const responseText = await readyResp.text(); + assert( + responseText === "pong", + `Expected "pong" but got "${responseText}"` ); - await expect(readyResp.text()).resolves.toEqual("pong"); // Assert runtime flag values for await (const flag of flags) { const flagResp = await fetch(`${url}/flag?name=${flag}`); - expect(flagResp.ok).toEqual(true); - await expect(flagResp.json(), `flag "${flag}"`).resolves.toEqual(true); + assert(flagResp.ok, `Expected flag response to be ok for "${flag}"`); + const flagValue = await flagResp.json(); + assert( + flagValue === true, + `Expected flag "${flag}" to be true but got ${flagValue}` + ); } return async () => await wrangler.stop(); @@ -1011,11 +1036,11 @@ describe.runIf(Boolean(CLOUDFLARE_ACCOUNT_ID))( const body = await response.text(); expect(body).toMatch("passed"); }, - { timeout: 19_000 }, + { timeout: 19_000 } ); - }, + } ); - }, + } ); type ConfigGroup = { @@ -1045,7 +1070,7 @@ function groupByWranglerConfig(configs: TestConfig[]): ConfigGroup[] { if (existing) { existing.name += `, ${config.name}`; for (const [flag, value] of Object.entries( - config.expectRuntimeFlags ?? {}, + config.expectRuntimeFlags ?? {} )) { if ( flag in existing.expectRuntimeFlags && @@ -1053,7 +1078,7 @@ function groupByWranglerConfig(configs: TestConfig[]): ConfigGroup[] { ) { throw new Error( `Conflicting expectRuntimeFlags for "${flag}" in group "${existing.name}": ` + - `existing=${existing.expectRuntimeFlags[flag]}, new=${value} (from "${config.name}")`, + `existing=${existing.expectRuntimeFlags[flag]}, new=${value} (from "${config.name}")` ); } existing.expectRuntimeFlags[flag] = value; @@ -1107,7 +1132,7 @@ function collectEnabledFlags(configs: TestConfig[]): string[] { enableFlags.add(flag); } else if (!flag.startsWith("disable_")) { throw new Error( - `Only enable_... and disable_... flags are handled, got "${flag}"`, + `Only enable_... and disable_... flags are handled, got "${flag}"` ); } } From 13c0f685e6f1eba673959bc5ec3524ff87872459 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Wed, 18 Mar 2026 16:58:33 +0000 Subject: [PATCH 10/18] fix missing import --- packages/wrangler/e2e/dev.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wrangler/e2e/dev.test.ts b/packages/wrangler/e2e/dev.test.ts index 729b7d76c9..cceda3db89 100644 --- a/packages/wrangler/e2e/dev.test.ts +++ b/packages/wrangler/e2e/dev.test.ts @@ -5,7 +5,7 @@ import * as nodeNet from "node:net"; import { setTimeout } from "node:timers/promises"; import dedent from "ts-dedent"; import { fetch } from "undici"; -import { afterEach, beforeEach, describe, it } from "vitest"; +import { afterEach, beforeEach, describe, it, vi } from "vitest"; import { CLOUDFLARE_ACCOUNT_ID } from "./helpers/account-id"; import { WranglerE2ETestHelper } from "./helpers/e2e-wrangler-test"; import { fetchText } from "./helpers/fetch-text"; From 2acf8e35b06622b85670fd7895ea0b660ca318ef Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Wed, 18 Mar 2026 18:03:21 +0000 Subject: [PATCH 11/18] Retry on TimeoutError from per-request AbortSignal.timeout() A timed-out API request (DOMException with name TimeoutError) is a transient failure and should be retried, just like 5xx errors. User- initiated aborts (AbortError) are still propagated immediately. --- .../src/__tests__/utils/retry.test.ts | 21 +++++++++++++++++++ packages/wrangler/src/utils/retry.ts | 4 ++++ 2 files changed, 25 insertions(+) diff --git a/packages/wrangler/src/__tests__/utils/retry.test.ts b/packages/wrangler/src/__tests__/utils/retry.test.ts index 00a6869c40..2f29ed0714 100644 --- a/packages/wrangler/src/__tests__/utils/retry.test.ts +++ b/packages/wrangler/src/__tests__/utils/retry.test.ts @@ -151,6 +151,27 @@ describe("retryOnAPIFailure", () => { expect(attempts).toBe(1); }); + it("should retry TimeoutError from AbortSignal.timeout()", async ({ + expect, + }) => { + let attempts = 0; + + await expect(() => + retryOnAPIFailure(() => { + attempts++; + throw new DOMException("The operation was aborted.", "TimeoutError"); + }) + ).rejects.toThrow("The operation was aborted."); + expect(attempts).toBe(3); + expect(getRetryAndErrorLogs(std.debug)).toMatchInlineSnapshot(` + [ + "Retrying API call after error...", + "Retrying API call after error...", + "Retrying API call after error...", + ] + `); + }); + it("should retry custom APIError implementation with non-5xx error", async ({ expect, }) => { diff --git a/packages/wrangler/src/utils/retry.ts b/packages/wrangler/src/utils/retry.ts index 55f6455eff..dfa2e1637a 100644 --- a/packages/wrangler/src/utils/retry.ts +++ b/packages/wrangler/src/utils/retry.ts @@ -26,6 +26,10 @@ export async function retryOnAPIFailure( if (!err.isRetryable()) { throw err; } + } else if (err instanceof DOMException && err.name === "TimeoutError") { + // Per-request timeouts (from AbortSignal.timeout()) are transient + // and should be retried, but user-initiated aborts (AbortError) + // should not. } else if (!(err instanceof TypeError)) { throw err; } From b23eb23f8ed0bc6f087fcb7ade69945d81597c6f Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Thu, 19 Mar 2026 14:54:32 +0000 Subject: [PATCH 12/18] Fix EADDRINUSE flakes by eliminating TOCTOU port races in e2e tests - stop() now awaits process exit (not just signal send) so ports are fully released before the next test starts - Remove get-port TOCTOU pattern from spawnLocalWorker and dev-registry pages dev tests; rely on --port 0 (OS-assigned) instead - Extend getWranglerCommand auto --port 0 to wrangler pages dev commands - Increase multi-worker test waitForReady to 30s since two serialised remote proxy sessions must complete before Ready on appears --- packages/wrangler/e2e/dev-registry.test.ts | 13 ++++--------- packages/wrangler/e2e/helpers/command.ts | 6 +++++- packages/wrangler/e2e/helpers/wrangler.ts | 13 +++++++++---- .../e2e/remote-binding/dev-remote-bindings.test.ts | 13 +++++-------- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/packages/wrangler/e2e/dev-registry.test.ts b/packages/wrangler/e2e/dev-registry.test.ts index 80215549c5..143cf2a771 100644 --- a/packages/wrangler/e2e/dev-registry.test.ts +++ b/packages/wrangler/e2e/dev-registry.test.ts @@ -1,4 +1,3 @@ -import getPort from "get-port"; import dedent from "ts-dedent"; import { fetch } from "undici"; import { beforeEach, describe, it } from "vitest"; @@ -409,9 +408,8 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { }); it("can fetch a (pages project)", async ({ expect }) => { - const port = await getPort(); const worker = helper.runLongLived( - `${cmd.replace("wrangler dev", "wrangler pages dev")} --port ${port}`, + `${cmd.replace("wrangler dev", "wrangler pages dev")}`, { cwd: a } ); @@ -427,9 +425,8 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { // We don't need b's URL, but ensure that b starts up before a await workerB.waitForReady(5_000); - const port = await getPort(); const workerA = helper.runLongLived( - `${cmd.replace("wrangler dev", "wrangler pages dev")} --port ${port}`, + `${cmd.replace("wrangler dev", "wrangler pages dev")}`, { cwd: a } ); const { url } = await workerA.waitForReady(5_000); @@ -446,9 +443,8 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { }); it("can fetch b through a (start a, start b)", async ({ expect }) => { - const port = await getPort(); const workerA = helper.runLongLived( - `${cmd.replace("wrangler dev", "wrangler pages dev")} --port ${port}`, + `${cmd.replace("wrangler dev", "wrangler pages dev")}`, { cwd: a } ); const { url } = await workerA.waitForReady(5_000); @@ -468,9 +464,8 @@ describe.each([{ cmd: "wrangler dev" }])("dev registry $cmd", ({ cmd }) => { "wrangler.toml": dedent` `, }); - const port = await getPort(); const workerA = helper.runLongLived( - `${cmd.replace("wrangler dev", "wrangler pages dev")} dist --service BEE=${workerName2} --port ${port}`, + `${cmd.replace("wrangler dev", "wrangler pages dev")} dist --service BEE=${workerName2}`, { cwd: a } ); const { url } = await workerA.waitForReady(5_000); diff --git a/packages/wrangler/e2e/helpers/command.ts b/packages/wrangler/e2e/helpers/command.ts index 959b60133c..966767ae85 100644 --- a/packages/wrangler/e2e/helpers/command.ts +++ b/packages/wrangler/e2e/helpers/command.ts @@ -150,7 +150,7 @@ export class LongLivedCommand { } async stop() { - return new Promise((resolve) => { + await new Promise((resolve) => { assert( this.commandProcess.pid, `Command "${this.command}" had no process id` @@ -169,6 +169,10 @@ export class LongLivedCommand { resolve(); }); }); + // Wait for the process to actually exit so that ports are fully released + // before the next test starts. Without this, the next test may encounter + // EADDRINUSE because the dying process still holds the port. + await this.exitPromise.catch(() => {}); } async signal(signal: NodeJS.Signals) { diff --git a/packages/wrangler/e2e/helpers/wrangler.ts b/packages/wrangler/e2e/helpers/wrangler.ts index 700ad0360d..ead4be84a2 100644 --- a/packages/wrangler/e2e/helpers/wrangler.ts +++ b/packages/wrangler/e2e/helpers/wrangler.ts @@ -51,6 +51,13 @@ export class WranglerLongLivedCommand extends LongLivedCommand { } } +function isDevCommand(command: string) { + return ( + command.startsWith("wrangler dev") || + command.startsWith("wrangler pages dev") + ); +} + function getWranglerCommand(command: string) { // Enforce a `wrangler` prefix to make commands clearer to read assert( @@ -61,15 +68,13 @@ function getWranglerCommand(command: string) { // If the user hasn't specifically set an inspector port, set it to 0 to reduce port conflicts const inspectorPort = - command.includes(`--inspector-port`) || !command.startsWith("wrangler dev") + command.includes(`--inspector-port`) || !isDevCommand(command) ? "" : " --inspector-port 0"; // If the user hasn't specifically set a Worker port, set it to 0 to reduce port conflicts const workerPort = - command.includes(`--port`) || !command.startsWith("wrangler dev") - ? "" - : " --port 0"; + command.includes(`--port`) || !isDevCommand(command) ? "" : " --port 0"; return `${WRANGLER} ${command.slice("wrangler ".length)}${inspectorPort}${workerPort}`; } diff --git a/packages/wrangler/e2e/remote-binding/dev-remote-bindings.test.ts b/packages/wrangler/e2e/remote-binding/dev-remote-bindings.test.ts index 42d5f325db..1a26f54ff6 100644 --- a/packages/wrangler/e2e/remote-binding/dev-remote-bindings.test.ts +++ b/packages/wrangler/e2e/remote-binding/dev-remote-bindings.test.ts @@ -1,7 +1,6 @@ import { readFile, writeFile } from "node:fs/promises"; import { resolve } from "node:path"; import { setTimeout } from "node:timers/promises"; -import getPort from "get-port"; import dedent from "ts-dedent"; import { beforeAll, describe, it } from "vitest"; import { CLOUDFLARE_ACCOUNT_ID } from "../helpers/account-id"; @@ -294,7 +293,10 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( `wrangler dev -c wrangler.json -c ${localTest}/wrangler.json` ); - const { url } = await worker.waitForReady(); + // Multi-worker with remote bindings on both workers requires two + // serialised remote proxy sessions (API calls) before "Ready on" + // can appear, so give it more time than the default 15s. + const { url } = await worker.waitForReady(30_000); await expect(fetchText(url)).resolves.toMatchInlineSnapshot(` "LOCAL: [local-test-worker]REMOTE: Hello from an alternative remote worker @@ -321,11 +323,6 @@ async function spawnLocalWorker(helper: WranglerE2ETestHelper): Promise { } }`, }); - const localWorker = helper.runLongLived( - // Note: we use a random port here otherwise for some reason in CI windows - // allows the default port to be overridden by other processes - `wrangler dev --port ${await getPort()}`, - { cwd: local } - ); + const localWorker = helper.runLongLived("wrangler dev", { cwd: local }); await localWorker.waitForReady(); } From fe9e10aff1a826a8a69e70a142c4faca572a570e Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Thu, 19 Mar 2026 14:59:11 +0000 Subject: [PATCH 13/18] Use waitForLong for error-log tests that depend on remote API validation The 'shows helpful error logs' tests wait for error messages that only appear after the remote proxy session validates bindings against the Cloudflare API. waitFor (5s) is too short; waitForLong (10s) matches the convention for anything involving HTTP round-trips. --- .../e2e/remote-binding/dev-remote-bindings.test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/wrangler/e2e/remote-binding/dev-remote-bindings.test.ts b/packages/wrangler/e2e/remote-binding/dev-remote-bindings.test.ts index 1a26f54ff6..8c3912455a 100644 --- a/packages/wrangler/e2e/remote-binding/dev-remote-bindings.test.ts +++ b/packages/wrangler/e2e/remote-binding/dev-remote-bindings.test.ts @@ -8,7 +8,7 @@ import { WranglerE2ETestHelper } from "../helpers/e2e-wrangler-test"; import { fetchText } from "../helpers/fetch-text"; import { normalizeOutput } from "../helpers/normalize"; import { makeRoot, seed } from "../helpers/setup"; -import { waitFor } from "../helpers/wait-for"; +import { waitFor, waitForLong } from "../helpers/wait-for"; describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( "wrangler dev - remote bindings", @@ -207,7 +207,9 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( const worker = helper.runLongLived("wrangler dev"); - await waitFor(() => + // The error appears only after the remote proxy session validates + // the binding against the Cloudflare API, so use the longer timeout. + await waitForLong(() => expect(worker.currentOutput).toContain( "Service binding 'REMOTE_WORKER' references Worker 'non-existent-service-binding' which was not found." ) @@ -234,7 +236,9 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( const worker = helper.runLongLived("wrangler dev"); - await waitFor(() => + // The error appears only after the remote proxy session validates + // the binding against the Cloudflare API, so use the longer timeout. + await waitForLong(() => expect(worker.currentOutput).toContain( "KV namespace 'non-existent-kv' is not valid." ) From 461d7539db74ef0a88534944d5ff6b025b1b94c2 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Thu, 19 Mar 2026 15:04:25 +0000 Subject: [PATCH 14/18] Fix TOCTOU port race in remote proxy session and use waitForLong for API-dependent error tests - Replace getPort() with port: 0 in start-remote-proxy-session.ts so the OS assigns the port atomically at bind time, eliminating the TOCTOU window that could cause EADDRINUSE during wrangler dev - Switch error-log assertion tests from waitFor (5s) to waitForLong (10s) since the error messages depend on Cloudflare API validation --- .../src/api/remoteBindings/start-remote-proxy-session.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/wrangler/src/api/remoteBindings/start-remote-proxy-session.ts b/packages/wrangler/src/api/remoteBindings/start-remote-proxy-session.ts index ebc580bb96..9fa1dd4f51 100644 --- a/packages/wrangler/src/api/remoteBindings/start-remote-proxy-session.ts +++ b/packages/wrangler/src/api/remoteBindings/start-remote-proxy-session.ts @@ -1,5 +1,4 @@ import path from "node:path"; -import getPort from "get-port"; import { DeferredPromise } from "miniflare"; import remoteBindingsWorkerPath from "worker:remoteBindings/ProxyServerWorker"; import { logger } from "../../logger"; @@ -43,7 +42,7 @@ export async function startRemoteProxySession( remote: "minimal", auth: options?.auth, server: { - port: await getPort(), + port: 0, }, inspector: false, logLevel: getStartWorkerLogLevel(logger.loggerLevel), From 0ab6e7895dcf7abc91409abb7f3622c2659f4003 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Thu, 19 Mar 2026 15:47:24 +0000 Subject: [PATCH 15/18] clarify minimum version of Node for Wrangler This is needed because we (already) use the abort signal API. --- packages/wrangler/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 980a107ab9..a57376aaa2 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -181,7 +181,7 @@ "fsevents": "~2.3.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=20.9.0" }, "volta": { "extends": "../../package.json" From f38b78385c7b3422a033ebd0c548dc9e68b4de2d Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Thu, 19 Mar 2026 15:47:24 +0000 Subject: [PATCH 16/18] clarify minimum version of Node for Wrangler This is needed because we (already) use the abort signal API. --- packages/wrangler/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index a57376aaa2..c4bb8fbc9d 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -181,7 +181,7 @@ "fsevents": "~2.3.2" }, "engines": { - "node": ">=20.9.0" + "node": ">=20.3.0" }, "volta": { "extends": "../../package.json" From 5298aa993d4b817b92d8150b59edf8dfb2f89c54 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Thu, 19 Mar 2026 16:38:02 +0000 Subject: [PATCH 17/18] Make workers.dev domain configurable via E2E_ACCOUNT_WORKERS_DEV_DOMAIN The ensureWorkerDeployed helper and the remote dev URL assertion now use E2E_ACCOUNT_WORKERS_DEV_DOMAIN (defaults to devprod-testing7928.workers.dev). Set this env var alongside CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN to run the e2e tests against a different account. --- .../tests/index.test.ts | 9 ++++---- .../e2e/tests/cli/cli.test.ts | 5 +++- .../e2e/remote-bindings.test.ts | 12 +++++----- packages/wrangler/e2e/README.md | 23 ++++++++++++++++--- packages/wrangler/e2e/dev.test.ts | 7 ++++-- packages/wrangler/e2e/helpers/account-id.ts | 10 ++++++++ .../wrangler/e2e/helpers/e2e-wrangler-test.ts | 11 +++++---- turbo.json | 7 +++++- 8 files changed, 62 insertions(+), 22 deletions(-) diff --git a/fixtures/get-platform-proxy-remote-bindings/tests/index.test.ts b/fixtures/get-platform-proxy-remote-bindings/tests/index.test.ts index dc34395e99..cc5b7e0ab5 100644 --- a/fixtures/get-platform-proxy-remote-bindings/tests/index.test.ts +++ b/fixtures/get-platform-proxy-remote-bindings/tests/index.test.ts @@ -19,6 +19,9 @@ import type { DispatchFetch, Response } from "miniflare"; type Fetcher = { fetch: DispatchFetch }; +const workersDomain = + process.env.E2E_ACCOUNT_WORKERS_DEV_DOMAIN ?? + "devprod-testing7928.workers.dev"; const auth = getAuthenticatedEnv(); const execOptions = { encoding: "utf8", @@ -35,8 +38,7 @@ if (auth) { let remoteKvId: string; beforeAll(async () => { - const deployedUrl = - "https://preserve-e2e-get-platform-proxy-remote.devprod-testing7928.workers.dev/"; + const deployedUrl = `https://preserve-e2e-get-platform-proxy-remote.${workersDomain}/`; try { assert((await fetch(deployedUrl)).status !== 404); @@ -54,8 +56,7 @@ if (auth) { ); } - const stagingDeployedUrl = - "https://preserve-e2e-get-platform-proxy-remote-staging.devprod-testing7928.workers.dev/"; + const stagingDeployedUrl = `https://preserve-e2e-get-platform-proxy-remote-staging.${workersDomain}/`; try { assert((await fetch(stagingDeployedUrl)).status !== 404); } catch { diff --git a/packages/create-cloudflare/e2e/tests/cli/cli.test.ts b/packages/create-cloudflare/e2e/tests/cli/cli.test.ts index 2ecd2136ca..ffe9e34086 100644 --- a/packages/create-cloudflare/e2e/tests/cli/cli.test.ts +++ b/packages/create-cloudflare/e2e/tests/cli/cli.test.ts @@ -15,6 +15,9 @@ import { recreateLogFolder } from "../../helpers/log-stream"; import { runC3 } from "../../helpers/run-c3"; const { name: pm } = detectPackageManager(); +const workersDomain = + process.env.E2E_ACCOUNT_WORKERS_DEV_DOMAIN ?? + "devprod-testing7928.workers.dev"; describe("Create Cloudflare CLI", () => { beforeAll((ctx) => { @@ -497,7 +500,7 @@ describe("Create Cloudflare CLI", () => { if ( ( await fetch( - "https://existing-script-test-do-not-delete.devprod-testing7928.workers.dev/" + `https://existing-script-test-do-not-delete.${workersDomain}/` ) ).status === 404 ) { diff --git a/packages/vite-plugin-cloudflare/e2e/remote-bindings.test.ts b/packages/vite-plugin-cloudflare/e2e/remote-bindings.test.ts index 1e29fde216..c6f88b7e8a 100644 --- a/packages/vite-plugin-cloudflare/e2e/remote-bindings.test.ts +++ b/packages/vite-plugin-cloudflare/e2e/remote-bindings.test.ts @@ -12,6 +12,9 @@ import { const commands = ["dev", "buildAndPreview"] as const; const isWindows = process.platform === "win32"; +const workersDomain = + process.env.E2E_ACCOUNT_WORKERS_DEV_DOMAIN ?? + "devprod-testing7928.workers.dev"; // Remote bindings tests are skipped on Windows due to slow/unreliable remote proxy // session initialization times in CI, which causes intermittent timeout failures. @@ -38,11 +41,8 @@ if (isWindows) { beforeAll(async () => { try { assert( - ( - await fetch( - "https://preserve-e2e-vite-remote.devprod-testing7928.workers.dev/" - ) - ).status !== 404 + (await fetch(`https://preserve-e2e-vite-remote.${workersDomain}/`)) + .status !== 404 ); } catch { runCommand(`npx wrangler deploy`, `${projectPath}/remote-worker`); @@ -51,7 +51,7 @@ if (isWindows) { assert( ( await fetch( - "https://preserve-e2e-vite-remote-alt.devprod-testing7928.workers.dev/" + `https://preserve-e2e-vite-remote-alt.${workersDomain}/` ) ).status !== 404 ); diff --git a/packages/wrangler/e2e/README.md b/packages/wrangler/e2e/README.md index 4debd017b6..2ab23a77a0 100644 --- a/packages/wrangler/e2e/README.md +++ b/packages/wrangler/e2e/README.md @@ -39,16 +39,33 @@ pnpm test:e2e:wrangler -- -u --bail=1 ### Cloudflare Credentials Cloudflare credentials are provided to the tests by setting `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`. +If you don't provide these then only the local e2e tests are executed. -- If you don't provide these then only the local e2e tests are executed. -- If you don't provide the "DevProd Testing" Cloudflare account (as `CLOUDFLARE_ACCOUNT_ID=8d783f274e1f82dc46744c297b015a2f`), tests that require that specific account are not executed. +#### Running against the CI account -To fully run the tests you should generate an API token for the "DevProd Testing" account: +The default configuration targets the "DevProd Testing" account. To fully run the tests, generate an API token for that account: ```zsh CLOUDFLARE_ACCOUNT_ID=8d783f274e1f82dc46744c297b015a2f CLOUDFLARE_API_TOKEN= pnpm test:e2e:wrangler ``` +#### Running against your own account + +You can run the e2e tests against any Cloudflare account. Some tests rely on +pre-deployed `preserve-e2e-*` workers whose URL includes the account's +`workers.dev` subdomain. Set `E2E_ACCOUNT_WORKERS_DEV_DOMAIN` so these tests +can locate (and, on first run, deploy) those workers on your account: + +```zsh +CLOUDFLARE_ACCOUNT_ID= \ +CLOUDFLARE_API_TOKEN= \ +E2E_ACCOUNT_WORKERS_DEV_DOMAIN=.workers.dev \ +pnpm test:e2e:wrangler +``` + +> You can find your subdomain in the Cloudflare dashboard under **Workers & Pages**. +> It defaults to `devprod-testing7928.workers.dev` (the CI account). + ### Focusing on a single e2e test file If you want to run a subset of tests (e.g. just one) while retaining the turborepo cache for the builds of the dependencies, you can provide the list of test files via the `WRANGLER_E2E_TEST_FILE` environment variable. diff --git a/packages/wrangler/e2e/dev.test.ts b/packages/wrangler/e2e/dev.test.ts index cceda3db89..de93375493 100644 --- a/packages/wrangler/e2e/dev.test.ts +++ b/packages/wrangler/e2e/dev.test.ts @@ -6,7 +6,10 @@ import { setTimeout } from "node:timers/promises"; import dedent from "ts-dedent"; import { fetch } from "undici"; import { afterEach, beforeEach, describe, it, vi } from "vitest"; -import { CLOUDFLARE_ACCOUNT_ID } from "./helpers/account-id"; +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"; @@ -1238,7 +1241,7 @@ describe.skipIf(CLOUDFLARE_ACCOUNT_ID !== "8d783f274e1f82dc46744c297b015a2f")( const text = await fetchText(url); - expect(text).toContain(`devprod-testing7928.workers.dev`); + expect(text).toContain(E2E_ACCOUNT_WORKERS_DEV_DOMAIN); }); it("respects dev.host setting", async ({ expect }) => { diff --git a/packages/wrangler/e2e/helpers/account-id.ts b/packages/wrangler/e2e/helpers/account-id.ts index c7f313d581..f7146aa6de 100644 --- a/packages/wrangler/e2e/helpers/account-id.ts +++ b/packages/wrangler/e2e/helpers/account-id.ts @@ -1,2 +1,12 @@ export const CLOUDFLARE_ACCOUNT_ID = process.env .CLOUDFLARE_ACCOUNT_ID as string; + +/** + * The workers.dev subdomain for the account used by e2e tests. + * + * Set the `E2E_ACCOUNT_WORKERS_DEV_DOMAIN` environment variable to run the + * tests against a different account (e.g. your personal account's subdomain). + */ +export const E2E_ACCOUNT_WORKERS_DEV_DOMAIN = + process.env.E2E_ACCOUNT_WORKERS_DEV_DOMAIN ?? + "devprod-testing7928.workers.dev"; diff --git a/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts b/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts index ea8faadb29..6dac7a99d4 100644 --- a/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts +++ b/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts @@ -4,6 +4,7 @@ import { cp } from "node:fs/promises"; import { setTimeout } from "node:timers/promises"; import { fetch } from "undici"; import { onTestFinished } from "vitest"; +import { E2E_ACCOUNT_WORKERS_DEV_DOMAIN } from "./account-id"; import { generateLeafCertificate, generateMtlsCertName, @@ -269,10 +270,10 @@ export class WranglerE2ETestHelper { /** * Ensure a worker with a well-known `preserve-e2e-*` name is deployed. * - * Checks whether the worker is already live by fetching its - * `devprod-testing7928.workers.dev` URL. If it responds with a non-404 - * status the deploy is skipped; otherwise `wrangler deploy` is run and - * the helper waits for the worker to become available. + * Checks whether the worker is already live by fetching its workers.dev + * URL (controlled by `E2E_ACCOUNT_WORKERS_DEV_DOMAIN`). If it responds + * with a non-404 status the deploy is skipped; otherwise `wrangler deploy` + * is run and the helper waits for the worker to become available. * * No cleanup is registered — the worker is expected to persist across * test runs and is excluded from the periodic e2e cleanup job by its @@ -287,7 +288,7 @@ export class WranglerE2ETestHelper { entryPoint?: string; configPath?: string; }): Promise { - const deployedUrl = `https://${workerName}.devprod-testing7928.workers.dev/`; + const deployedUrl = `https://${workerName}.${E2E_ACCOUNT_WORKERS_DEV_DOMAIN}/`; try { const response = await fetch(deployedUrl); if (response.status !== 404) { diff --git a/turbo.json b/turbo.json index c3b8bab6fb..5c8953fefa 100644 --- a/turbo.json +++ b/turbo.json @@ -54,7 +54,12 @@ "test:e2e": { "dependsOn": ["build"], "outputLogs": "new-only", - "env": ["CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_API_TOKEN", "NODE_DEBUG"] + "env": [ + "CLOUDFLARE_ACCOUNT_ID", + "CLOUDFLARE_API_TOKEN", + "E2E_ACCOUNT_WORKERS_DEV_DOMAIN", + "NODE_DEBUG" + ] }, "//#check:format": { "cache": true From 91deef090ab112ba6caf7f123d65fbead623e463 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Thu, 19 Mar 2026 17:42:04 +0000 Subject: [PATCH 18/18] Increase vite-plugin waitForReady timeout from 20s to 30s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The buildAndPreview flow does a full build before starting the preview server, so it needs more headroom than 20s — especially in CI. --- packages/vite-plugin-cloudflare/e2e/helpers.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/vite-plugin-cloudflare/e2e/helpers.ts b/packages/vite-plugin-cloudflare/e2e/helpers.ts index 9cf6ca9736..83c44ba423 100644 --- a/packages/vite-plugin-cloudflare/e2e/helpers.ts +++ b/packages/vite-plugin-cloudflare/e2e/helpers.ts @@ -307,7 +307,9 @@ export async function fetchJson(url: string, info?: RequestInit) { export async function waitForReady(proc: Process) { const match = await vi.waitUntil( () => proc.stdout.match(/Local:\s+(http:\/\/localhost:\d+)/), - { interval: 100, timeout: 20_000 } + // buildAndPreview does a full build before starting the server, + // so allow more time than the default. + { interval: 100, timeout: 30_000 } ); return match[1]; }