import ci from "ci-info"; import dedent from "ts-dedent"; import { fetch } from "undici"; import { afterAll, afterEach, assert, beforeAll, beforeEach, describe, type ExpectStatic, it, } 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 { normalizeOutput, validateAssetUploadLogs } from "./helpers/normalize"; import { retry } from "./helpers/retry"; import { waitForLong } from "./helpers/wait-for"; const TIMEOUT = 50_000; describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( "deployments", { timeout: TIMEOUT }, () => { // Note that we are sharing the workerName and helper across all these tests, // which means that these tests are not isolated from each other. // Seeded files will leak between tests. const workerName = generateResourceName(); const helper = new WranglerE2ETestHelper(); afterAll(async () => { // clean up user Worker after all tests await helper.bestEffortRun(`wrangler delete`); }); it("deploys a Worker", async ({ expect }) => { await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" `, "src/index.ts": dedent` export default { fetch(request) { return new Response("Hello World!") } }`, "package.json": dedent` { "name": "${workerName}", "version": "0.0.0", "private": true } `, }); const output = await helper.run(`wrangler deploy`); const deployedUrl = getDeployedUrl(output); await waitForLong( async () => { const response = await fetch(deployedUrl); expect(await response.text()).toEqual("Hello World!"); }, { timeout: 30_000 } ); }); it("lists 1 deployment", async ({ expect }) => { const output = await helper.run(`wrangler deployments list`); expect(normalizeOutput(output.stdout)).toMatchInlineSnapshot(` "Created: TIMESTAMP Author: person@example.com Source: Upload Message: Automatic deployment on upload. Version(s): (100%) 00000000-0000-0000-0000-000000000000 Created: TIMESTAMP Tag: - Message: -" `); }); it("modifies & deploys a Worker", async ({ expect }) => { await helper.seed({ "src/index.ts": dedent` export default { fetch(request) { return new Response("Updated Worker!") } }`, }); const output = await helper.run(`wrangler deploy`); const deployedUrl = getDeployedUrl(output); await waitForLong( async () => { const response = await fetch(deployedUrl); expect(await response.text()).toEqual("Updated Worker!"); }, { timeout: 30_000 } ); }); it("lists 2 deployments", async ({ expect }) => { const dep = await helper.run(`wrangler deployments list`); expect(normalizeOutput(dep.stdout)).toMatchInlineSnapshot(` "Created: TIMESTAMP Author: person@example.com Source: Upload Message: Automatic deployment on upload. Version(s): (100%) 00000000-0000-0000-0000-000000000000 Created: TIMESTAMP Tag: - Message: - Created: TIMESTAMP Author: person@example.com Source: Unknown (deployment) Message: - Version(s): (100%) 00000000-0000-0000-0000-000000000000 Created: TIMESTAMP Tag: - Message: -" `); }); it("rolls back", async ({ expect }) => { const output = await helper.run( `wrangler rollback --message "A test message"` ); expect(normalizeOutput(output.stdout)).toMatchInlineSnapshot(` "├ Fetching latest deployment │ ├ Your current deployment has 1 version(s): │ │ (100%) 00000000-0000-0000-0000-000000000000 │ Created: TIMESTAMP │ Tag: - │ Message: - │ ├ Finding latest stable Worker Version to rollback to │ │ ? Please provide an optional message for this rollback (120 characters max) 🤖 Using default value in non-interactive context: A test message │ ├ WARNING You are about to rollback to Worker Version 00000000-0000-0000-0000-000000000000. │ This will immediately replace the current deployment and become the active deployment across all your deployed triggers. │ However, your local development environment will not be affected by this rollback. │ Rolling back to a previous deployment will not rollback any of the bound resources (Durable Object, D1, R2, KV, etc). │ │ (100%) 00000000-0000-0000-0000-000000000000 │ Created: TIMESTAMP │ Tag: - │ Message: - │ ? Are you sure you want to deploy this Worker Version to 100% of traffic? 🤖 Using fallback value in non-interactive context: yes Performing rollback... │ ╰ SUCCESS Worker Version 00000000-0000-0000-0000-000000000000 has been deployed to 100% of traffic. Current Version ID: 00000000-0000-0000-0000-000000000000" `); }); it("lists deployments", async ({ expect }) => { const dep = await helper.run(`wrangler deployments list`); expect(normalizeOutput(dep.stdout)).toMatchInlineSnapshot(` "Created: TIMESTAMP Author: person@example.com Source: Upload Message: Automatic deployment on upload. Version(s): (100%) 00000000-0000-0000-0000-000000000000 Created: TIMESTAMP Tag: - Message: - Created: TIMESTAMP Author: person@example.com Source: Unknown (deployment) Message: - Version(s): (100%) 00000000-0000-0000-0000-000000000000 Created: TIMESTAMP Tag: - Message: - Created: TIMESTAMP Author: person@example.com Source: Unknown (deployment) Message: A test message Version(s): (100%) 00000000-0000-0000-0000-000000000000 Created: TIMESTAMP Tag: - Message: -" `); }); } ); type AssetTestCase = { path: string; content?: string; redirect?: string; }; function generateInitialAssets(workerName: string) { return { "public/index.html": dedent`

index.html

`, "public/[boop].html": dedent`

[boop].html

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

404.html

`, "package.json": dedent` { "name": "${workerName}", "version": "0.0.0", "private": true }`, }; } async function checkAssets( expect: ExpectStatic, testCases: AssetTestCase[], deployedUrl: string ) { for (const testCase of testCases) { await waitForLong( async () => { const r = await fetch(new URL(testCase.path, deployedUrl)); const text = await r.text(); const url = r.url; if (testCase.content) { expect( text, `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}` ).toEqual(new URL(testCase.redirect, deployedUrl).pathname); } else { expect( new URL(url).pathname, `unexpected pathname for ${testCase.path}` ).toEqual(new URL(testCase.path, deployedUrl).pathname); } }, { timeout: 40_000 } ); } } describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)("Workers + Assets deployment", () => { let helper: WranglerE2ETestHelper; let workerName: string; beforeEach(() => { // We are recreating the helper on each test to ensure they are isolated from each other. helper = new WranglerE2ETestHelper(); // Use a new user Worker in each test workerName = generateResourceName(); }); describe("Workers", () => { afterEach(async () => { // clean up user Worker after each test await helper.bestEffortRun(`wrangler delete`); }); it("deploys a Workers + Assets project with assets only", async ({ expect, }) => { await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" compatibility_date = "2023-01-01" [assets] directory = "public" `, ...generateInitialAssets(workerName), }); // deploy user Worker && verify output const output = await helper.run(`wrangler deploy`); validateAssetUploadLogs(expect, output, [ "/404.html", "/index.html", "/[boop].html", ]); const deployedUrl = getDeployedUrl(output); const testCases: AssetTestCase[] = [ // Tests html_handling = "auto_trailing_slash" (default): { path: "/", content: "

index.html

", }, { path: "/index.html", content: "

index.html

", redirect: "/", }, { path: "/[boop]", content: "

[boop].html

", redirect: "/%5Bboop%5D", }, ]; await checkAssets(expect, testCases, deployedUrl); // Test 404 handling: // even though 404.html has been uploaded, because not_found_handling is set to "none" // we expect to get an empty response const { text } = await retry( (s) => s.status !== 404, async () => { const r = await fetch(new URL("/try-404", deployedUrl)); const temp = { text: await r.text(), status: r.status }; return temp; } ); expect(text).toBeFalsy(); }); it("deploys a Worker with static assets and user Worker", async ({ expect, }) => { await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" [assets] directory = "public" binding = "ASSETS" html_handling = "none" not_found_handling = "404-page" `, "src/index.ts": dedent` export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname === "/binding") { return await env.ASSETS.fetch(new URL("index.html", request.url)); } else if (url.pathname === "/try-404") { return await env.ASSETS.fetch(request.url); } return new Response("Hello World!") } }`, ...generateInitialAssets(workerName), }); // deploy user Worker && verify output const output = await helper.run(`wrangler deploy`); validateAssetUploadLogs(expect, output, [ "/404.html", "/index.html", "/[boop].html", ]); const deployedUrl = getDeployedUrl(output); const testCases: AssetTestCase[] = [ // because html handling has now been set to "none", only exact matches will be served { path: "/index.html", content: "

index.html

", }, // 404s should fall through to the user worker, and "/" is not an exact match // so we should expect the UW response { path: "/", content: "Hello World!" }, { path: "/binding", content: "

index.html

", }, { path: "/worker", content: "Hello World!", }, ]; await checkAssets(expect, testCases, deployedUrl); // unlike before, not_found_handling has been set to "404-page" instead of the default "none" // note that with a user worker, the request must be passed back to the asset worker via the ASSET binding // in order to return the 404 page const { text } = await retry( // Retry while the status isn't 404 OR the content hasn't propagated yet. // Before assets are live the platform may return its own 404 page (status 200) // instead of the user's 404.html asset. (s) => s.status !== 404 || !s.text.includes("

404.html

"), async () => { 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

"); }); it("deploys a Workers + Assets project with helpful debug logs", async ({ expect, }) => { await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" compatibility_date = "2023-01-01" [assets] directory = "public" `, ...generateInitialAssets(workerName), }); // deploy user Worker && verify output const output = await helper.run(`wrangler deploy`, { debug: true, }); validateAssetUploadLogs( expect, output, ["/404.html", "/index.html", "/[boop].html"], { includeDebug: true } ); const deployedUrl = getDeployedUrl(output); const testCases: AssetTestCase[] = [ // Tests html_handling = "auto_trailing_slash" (default): { path: "/", content: "

index.html

", }, { path: "/index.html", content: "

index.html

", redirect: "/", }, { path: "/[boop]", content: "

[boop].html

", redirect: "/%5Bboop%5D", }, ]; await checkAssets(expect, testCases, deployedUrl); // Test 404 handling: // even though 404.html has been uploaded, because not_found_handling is set to "none" // we expect to get an empty response const { text } = await retry( (s) => s.status !== 404, async () => { const r = await fetch(new URL("/try-404", deployedUrl)); const temp = { text: await r.text(), status: r.status }; return temp; } ); expect(text).toBeFalsy(); }); it("runs the user Worker ahead of matching assets when run_worker_first = true", async ({ expect, }) => { await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" [assets] directory = "public" binding = "ASSETS" html_handling = "none" not_found_handling = "404-page" run_worker_first = true `, "src/index.ts": dedent` export default { async fetch(request, env) { return new Response("Hello World from User Worker!") } }`, ...generateInitialAssets(workerName), }); // deploy user Worker && verify output const output = await helper.run(`wrangler deploy`); validateAssetUploadLogs(expect, output, [ "/404.html", "/index.html", "/[boop].html", ]); const deployedUrl = getDeployedUrl(output); const testCases: AssetTestCase[] = [ { path: "/index.html", content: "Hello World from User Worker!", }, { path: "/", content: "Hello World from User Worker!", }, { path: "/worker", content: "Hello World from User Worker!", }, ]; await checkAssets(expect, testCases, deployedUrl); }); it("runs the user Worker ahead of matching assets for matching run_worker_first routes", async ({ expect, }) => { await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" [assets] directory = "public" binding = "ASSETS" html_handling = "none" not_found_handling = "404-page" run_worker_first = ["/api/*", "!/api/assets/*"] `, "src/index.ts": dedent` export default { async fetch(request, env) { return new Response("Hello World from User Worker!") } }`, ...generateInitialAssets(workerName), "public/api/index.html": "

api/index.html

", "public/api/assets/test.html": "

api/assets/test.html

", }); // deploy user Worker && verify output const output = await helper.run(`wrangler deploy`); validateAssetUploadLogs(expect, output, [ "/404.html", "/api/index.html", "/index.html", "/api/assets/test.html", "/[boop].html", ]); const deployedUrl = getDeployedUrl(output); const testCases: AssetTestCase[] = [ { path: "/index.html", content: "

index.html

" }, { path: "/missing.html", content: "

404.html

" }, { path: "/api/", content: "Hello World from User Worker!" }, { path: "/api/foo.html", content: "Hello World from User Worker!" }, { path: "/api/assets/missing", content: "404.html" }, { path: "/api/assets/test.html", content: "api/assets/test.html" }, ]; await checkAssets(expect, testCases, deployedUrl); }); }); describe("Workers for Platforms", () => { let dispatchNamespaceName: string; let dispatchWorkerName: string; beforeEach(async () => { // set up a new dispatch Worker in each test dispatchNamespaceName = generateResourceName("dispatch"); dispatchWorkerName = generateResourceName(); await helper.seed({ "dispatch-worker/wrangler.toml": dedent` name = "${dispatchWorkerName}" main = "./src/index.js" compatibility_date = "2023-01-01" [[dispatch_namespaces]] binding = "DISPATCH" namespace = "${dispatchNamespaceName}" `, "dispatch-worker/src/index.js": dedent` export default { async fetch(request, env, ctx) { const stub = env.DISPATCH.get("${workerName}"); return stub.fetch(request); } } `, }); }); afterEach(async () => { // clean up dispatch Worker await helper.bestEffortRun( `wrangler delete -c dispatch-worker/wrangler.toml` ); await helper.bestEffortRun( `wrangler dispatch-namespace delete ${dispatchNamespaceName}` ); }); it("deploys a Workers + Assets project with assets only", async ({ expect, }) => { await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" compatibility_date = "2023-01-01" [assets] directory = "public" `, ...generateInitialAssets(workerName), }); // create a dispatch namespace && verify output let output = await helper.run( `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"` ); // upload user Worker to the dispatch namespace && verify output output = await helper.run( `wrangler deploy --dispatch-namespace ${dispatchNamespaceName}` ); validateAssetUploadLogs(expect, output, [ "/404.html", "/index.html", "/[boop].html", ]); // deploy dispatch Worker && verify output output = await helper.run( `wrangler deploy -c dispatch-worker/wrangler.toml` ); normalizedStdout = normalizeOutput(output.stdout); expect(normalizedStdout).toEqual(`Total Upload: xx KiB / gzip: xx KiB Your Worker has access to the following bindings: Binding Resource env.DISPATCH (tmp-e2e-dispatch-00000000-0000-0000-0000-000000000000) Dispatch Namespace Uploaded tmp-e2e-worker-00000000-0000-0000-0000-000000000000 (TIMINGS) Deployed tmp-e2e-worker-00000000-0000-0000-0000-000000000000 triggers (TIMINGS) https://tmp-e2e-worker-00000000-0000-0000-0000-000000000000.SUBDOMAIN.workers.dev Current Version ID: 00000000-0000-0000-0000-000000000000`); const deployedUrl = getDeployedUrl(output); const testCases: AssetTestCase[] = [ // Tests html_handling = "auto_trailing_slash" (default): { path: "/", content: "

index.html

", }, { path: "/index.html", content: "

index.html

", redirect: "/", }, { path: "/[boop]", content: "

[boop].html

", redirect: "/%5Bboop%5D", }, ]; await checkAssets(expect, testCases, deployedUrl); // Test 404 handling: // even though 404.html has been uploaded, because not_found_handling is set to "none" // we expect to get an empty response const { text } = await retry( (s) => s.status !== 404, async () => { const r = await fetch(new URL("/try-404", deployedUrl)); const temp = { text: await r.text(), status: r.status }; return temp; } ); expect(text).toBeFalsy(); }); it("deploys a Worker with static assets and user Worker", async ({ expect, }) => { await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" [assets] directory = "public" binding = "ASSETS" html_handling = "none" not_found_handling = "404-page" `, "src/index.ts": dedent` export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname === "/binding") { return await env.ASSETS.fetch(new URL("index.html", request.url)); } else if (url.pathname === "/try-404") { return await env.ASSETS.fetch(request.url); } return new Response("Hello World!") } }`, ...generateInitialAssets(workerName), }); // create a dispatch namespace && verify output let output = await helper.run( `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"` ); // upload user Worker to the dispatch namespace && verify output output = await helper.run( `wrangler deploy --dispatch-namespace ${dispatchNamespaceName}` ); validateAssetUploadLogs(expect, output, [ "/404.html", "/index.html", "/[boop].html", ]); // deploy dispatch Worker && verify output output = await helper.run( `wrangler deploy -c dispatch-worker/wrangler.toml` ); normalizedStdout = normalizeOutput(output.stdout); expect(normalizedStdout).toEqual(`Total Upload: xx KiB / gzip: xx KiB Your Worker has access to the following bindings: Binding Resource env.DISPATCH (tmp-e2e-dispatch-00000000-0000-0000-0000-000000000000) Dispatch Namespace Uploaded tmp-e2e-worker-00000000-0000-0000-0000-000000000000 (TIMINGS) Deployed tmp-e2e-worker-00000000-0000-0000-0000-000000000000 triggers (TIMINGS) https://tmp-e2e-worker-00000000-0000-0000-0000-000000000000.SUBDOMAIN.workers.dev Current Version ID: 00000000-0000-0000-0000-000000000000`); const deployedUrl = getDeployedUrl(output); const testCases: AssetTestCase[] = [ // because html handling has now been set to "none", only exact matches will be served { path: "/index.html", content: "

index.html

", }, // 404s should fall through to the user worker, and "/" is not an exact match // so we should expect the UW response { path: "/", content: "Hello World!" }, { path: "/binding", content: "

index.html

", }, { path: "/worker", content: "Hello World!", }, ]; await checkAssets(expect, testCases, deployedUrl); // unlike before, not_found_handling has been set to "404-page" // instead of the default "none" // note that with a user Worker, the request must be passed back to // the asset worker via the ASSET binding in order to return the 404 // page const { text } = await retry( // Retry while the status isn't 404 OR the content hasn't propagated yet. // Before assets are live the platform may return its own 404 page (status 200) // instead of the user's 404.html asset. (s) => s.status !== 404 || !s.text.includes("

404.html

"), async () => { 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

"); }); it("runs the user Worker ahead of matching assets when run_worker_first = true", async ({ expect, }) => { await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" [assets] directory = "public" binding = "ASSETS" html_handling = "none" not_found_handling = "404-page" run_worker_first = true `, "src/index.ts": dedent` export default { async fetch(request, env) { return new Response("Hello World from User Worker!") } }`, ...generateInitialAssets(workerName), }); // create a dispatch namespace && verify output let output = await helper.run( `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"` ); // upload user Worker to the dispatch namespace && verify output output = await helper.run( `wrangler deploy --dispatch-namespace ${dispatchNamespaceName}` ); validateAssetUploadLogs(expect, output, [ "/404.html", "/index.html", "/[boop].html", ]); // deploy dispatch Worker && verify output output = await helper.run( `wrangler deploy -c dispatch-worker/wrangler.toml` ); normalizedStdout = normalizeOutput(output.stdout); expect(normalizedStdout).toEqual(`Total Upload: xx KiB / gzip: xx KiB Your Worker has access to the following bindings: Binding Resource env.DISPATCH (tmp-e2e-dispatch-00000000-0000-0000-0000-000000000000) Dispatch Namespace Uploaded tmp-e2e-worker-00000000-0000-0000-0000-000000000000 (TIMINGS) Deployed tmp-e2e-worker-00000000-0000-0000-0000-000000000000 triggers (TIMINGS) https://tmp-e2e-worker-00000000-0000-0000-0000-000000000000.SUBDOMAIN.workers.dev Current Version ID: 00000000-0000-0000-0000-000000000000`); const deployedUrl = getDeployedUrl(output); const testCases: AssetTestCase[] = [ { path: "/index.html", content: "Hello World from User Worker!", }, { path: "/", content: "Hello World from User Worker!", }, { path: "/worker", content: "Hello World from User Worker!", }, ]; await checkAssets(expect, testCases, deployedUrl); }); }); }); const skipContainersTest = !CLOUDFLARE_ACCOUNT_ID || (ci.isCI && process.platform !== "linux"); describe.skipIf(skipContainersTest)("containers", () => { let helper: WranglerE2ETestHelper; let workerName: string; let applicationId: string | undefined; let deployedUrl: string; beforeAll(async () => { helper = new WranglerE2ETestHelper(); workerName = generateResourceName(); await helper.seed({ "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2023-01-01" [durable_objects] bindings = [ { name = "MY_DO", class_name = "MyDurableObject" } ] [[containers]] name = "e2e-test-${workerName}" class_name = "MyDurableObject" image = "./Dockerfile" max_instances = 1 unsafe = { configuration = { experimental_flags = ["experimental_enable_snapshots"] } } [[migrations]] tag = "v1" new_sqlite_classes = ["MyDurableObject"] [observability] enabled = true `, "container/index.js": dedent` const { createServer } = require("http"); const server = createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('hello from container'); }); server.listen(80, () => { console.log('Server running on port 80'); }); `, Dockerfile: dedent` FROM node:18-slim WORKDIR /app COPY container/index.js . CMD ["node", "index.js"] EXPOSE 80 `, "src/index.ts": dedent` export default { async fetch(req, env) { const url = new URL(req.url) if (url.pathname === "/do") { const id = env.MY_DO.idFromName(url.pathname); const stub = env.MY_DO.get(id); try { return await stub.fetch(req); } catch (err) { return new Response("Error fetching from stub: " + err.message, { status: 400 }); } } return new Response("not found", { status: 404 }); }, }; export class MyDurableObject implements DurableObject { constructor(ctx) { this.ctx = ctx; } async fetch(_request: Request) { if (!this.ctx.container) { return new Response('this.ctx.container not defined', { status: 500 }); } if (!this.ctx.container.running) { this.ctx.container.start(); this.monitor = this.ctx.container.monitor(); } return this.ctx.container.getTcpPort(80).fetch(new Request("http://foo")); } }`, }); }); afterAll(async () => { // clean up user Worker after each test const deleteWorker = helper.bestEffortRun(`wrangler delete`); const deleteContainer = helper.bestEffortRun( `wrangler containers delete ${applicationId}` ); await Promise.allSettled([deleteWorker, deleteContainer]); }, 30_000); it( "can deploy a snapshot-enabled Dockerfile container and skip unchanged rebuilds", { timeout: 60 * 2 * 1000 }, async ({ expect }) => { const outputOne = await helper.run(`wrangler deploy`); deployedUrl = getDeployedUrl(outputOne); const matchApplicationId = outputOne.stdout.match( /([(]Application ID: (?.+?)[)])/ ); applicationId = matchApplicationId?.groups?.applicationId; assert(matchApplicationId?.groups); assert(applicationId); assert(CLOUDFLARE_ACCOUNT_ID); const containerInfo = await helper.run( `wrangler containers info ${applicationId}` ); const application = JSON.parse(containerInfo.stdout) as { configuration?: { experimental_flags?: unknown; image?: unknown; }; }; expect(application.configuration?.experimental_flags).toContain( "experimental_enable_snapshots" ); const image = application.configuration?.image; assert(typeof image === "string"); const expectedImagePrefix = `registry.cloudflare.com/${CLOUDFLARE_ACCOUNT_ID}/e2e-test-${workerName}@sha256:`; expect(image.startsWith(expectedImagePrefix)).toBe(true); expect(image).toMatch(/@sha256:[a-f0-9]{64}$/); 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 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()) ); } expect(await response.text()).toEqual("hello from container"); }, // big timeout for containers (2m) { timeout: 60 * 2 * 1000, interval: 1000 } ); } ); }); /** * Extracts the deployed URL from the output of a Wrangler command. */ function getDeployedUrl(output: { stdout: string }) { const match = output.stdout.match( /(?https:\/\/tmp-e2e-.+?\..+?\.workers\.dev)/ ); assert(match?.groups); return match.groups.url; }