Skip to content

Commit ade600c

Browse files
authored
C3: Add e2e coverage for deployment (#3658)
* C3: Add e2e coverage for deployment * PR feedback * Adding cleanup script for test projects * C3: Run framework e2e tests concurrently * Fixing project cleanup and tweaking test concurrency
1 parent 060ecaa commit ade600c

File tree

8 files changed

+211
-72
lines changed

8 files changed

+211
-72
lines changed

.github/workflows/test-c3.yml

+3
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,6 @@ jobs:
6464

6565
- name: E2E Tests
6666
run: npm run test:e2e -w create-cloudflare
67+
env:
68+
CLOUDFLARE_API_TOKEN: ${{ secrets.TEST_CLOUDFLARE_API_TOKEN }}
69+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.TEST_CLOUDFLARE_ACCOUNT_ID }}

packages/create-cloudflare/e2e-tests/helpers.ts

+30-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { spawn } from "cross-spawn";
2+
import { spinnerFrames } from "helpers/interactive";
23

34
export const keys = {
45
enter: "\x0d",
@@ -37,6 +38,10 @@ export const runC3 = async ({
3738
const currentDialog = promptHandlers[0];
3839

3940
lines.forEach((line) => {
41+
// Uncomment to debug test output
42+
// if (filterLine(line)) {
43+
// console.log(line);
44+
// }
4045
stdout.push(line);
4146

4247
if (currentDialog && currentDialog.matcher.test(line)) {
@@ -64,17 +69,39 @@ export const runC3 = async ({
6469
if (code === 0) {
6570
resolve(null);
6671
} else {
72+
console.log(stderr.join("\n").trim());
6773
rejects(code);
6874
}
6975
});
7076

71-
proc.on("error", (err) => {
72-
rejects(err);
77+
proc.on("error", (exitCode) => {
78+
rejects({
79+
exitCode,
80+
output: condenseOutput(stdout).join("\n").trim(),
81+
errors: stderr.join("\n").trim(),
82+
});
7383
});
7484
});
7585

7686
return {
77-
output: stdout.join("\n").trim(),
87+
output: condenseOutput(stdout).join("\n").trim(),
7888
errors: stderr.join("\n").trim(),
7989
};
8090
};
91+
92+
// Removes lines from the output of c3 that aren't particularly useful for debugging tests
93+
export const condenseOutput = (lines: string[]) => {
94+
return lines.filter(filterLine);
95+
};
96+
97+
const filterLine = (line: string) => {
98+
// Remove all lines with spinners
99+
for (const frame of spinnerFrames) {
100+
if (line.includes(frame)) return false;
101+
}
102+
103+
// Remove empty lines
104+
if (line.replace(/\s/g, "").length == 0) return false;
105+
106+
return true;
107+
};
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,95 @@
11
import { existsSync, mkdtempSync, realpathSync, rmSync } from "fs";
2+
import crypto from "node:crypto";
23
import { tmpdir } from "os";
34
import { join } from "path";
5+
import spawn from "cross-spawn";
46
import { FrameworkMap } from "frameworks/index";
57
import { readJSON } from "helpers/files";
8+
import { fetch } from "undici";
69
import { describe, expect, test, afterEach, beforeEach } from "vitest";
710
import { keys, runC3 } from "./helpers";
811
import type { RunnerConfig } from "./helpers";
912

13+
export const TEST_PREFIX = "c3-e2e-";
14+
1015
/*
1116
Areas for future improvement:
12-
- Make these actually e2e by verifying that deployment works
1317
- Add support for frameworks with global installs (like docusaurus, gatsby, etc)
1418
*/
1519

16-
describe("E2E: Web frameworks", () => {
20+
type FrameworkTestConfig = RunnerConfig & {
21+
expectResponseToContain: string;
22+
};
23+
24+
describe(`E2E: Web frameworks`, () => {
1725
const tmpDirPath = realpathSync(mkdtempSync(join(tmpdir(), "c3-tests")));
18-
const projectPath = join(tmpDirPath, "pages-tests");
26+
const baseProjectName = `c3-e2e-${crypto.randomBytes(3).toString("hex")}`;
1927

20-
beforeEach(() => {
28+
const getProjectName = (framework: string) =>
29+
`${baseProjectName}-${framework}`;
30+
const getProjectPath = (framework: string) =>
31+
join(tmpDirPath, getProjectName(framework));
32+
33+
beforeEach((ctx) => {
34+
const framework = ctx.meta.name;
35+
const projectPath = getProjectPath(framework);
2136
rmSync(projectPath, { recursive: true, force: true });
2237
});
2338

24-
afterEach(() => {
39+
afterEach((ctx) => {
40+
const framework = ctx.meta.name;
41+
const projectPath = getProjectPath(framework);
42+
const projectName = getProjectName(framework);
43+
2544
if (existsSync(projectPath)) {
2645
rmSync(projectPath, { recursive: true });
2746
}
47+
48+
try {
49+
const { output } = spawn.sync("npx", [
50+
"wrangler",
51+
"pages",
52+
"project",
53+
"delete",
54+
"-y",
55+
projectName,
56+
]);
57+
58+
if (!output.toString().includes(`Successfully deleted ${projectName}`)) {
59+
console.error(output.toString());
60+
}
61+
} catch (error) {
62+
console.error(`Failed to cleanup project: ${projectName}`);
63+
console.error(error);
64+
}
2865
});
2966

3067
const runCli = async (
3168
framework: string,
32-
{ argv = [], promptHandlers = [], overrides = {} }: RunnerConfig
69+
{ argv = [], promptHandlers = [], overrides }: RunnerConfig
3370
) => {
71+
const projectPath = getProjectPath(framework);
72+
3473
const args = [
3574
projectPath,
3675
"--type",
3776
"webFramework",
3877
"--framework",
3978
framework,
40-
"--no-deploy",
79+
"--deploy",
80+
"--no-open",
4181
];
4282

43-
if (argv.length > 0) {
44-
args.push(...argv);
45-
} else {
46-
args.push("--no-git");
47-
}
48-
49-
// For debugging purposes, uncomment the following to see the exact
50-
// command the test uses. You can then run this via the command line.
51-
// console.log("COMMAND: ", `node ${["./dist/cli.js", ...args].join(" ")}`);
83+
args.push(...argv);
5284

53-
await runC3({ argv: args, promptHandlers });
85+
const { output } = await runC3({ argv: args, promptHandlers });
5486

5587
// Relevant project files should have been created
5688
expect(projectPath).toExist();
57-
5889
const pkgJsonPath = join(projectPath, "package.json");
5990
expect(pkgJsonPath).toExist();
6091

92+
// Wrangler should be installed
6193
const wranglerPath = join(projectPath, "node_modules/wrangler");
6294
expect(wranglerPath).toExist();
6395

@@ -68,7 +100,7 @@ describe("E2E: Web frameworks", () => {
68100
...frameworkConfig.packageScripts,
69101
} as Record<string, string>;
70102

71-
if (overrides.packageScripts) {
103+
if (overrides && overrides.packageScripts) {
72104
// override packageScripts with testing provided scripts
73105
Object.entries(overrides.packageScripts).forEach(([target, cmd]) => {
74106
frameworkTargetPackageScripts[target] = cmd;
@@ -79,46 +111,77 @@ describe("E2E: Web frameworks", () => {
79111
Object.entries(frameworkTargetPackageScripts).forEach(([target, cmd]) => {
80112
expect(pkgJson.scripts[target]).toEqual(cmd);
81113
});
114+
115+
return { output };
82116
};
83117

84-
test.each(["astro", "hono", "react", "remix", "vue"])("%s", async (name) => {
85-
await runCli(name, {});
86-
});
118+
const runCliWithDeploy = async (framework: string) => {
119+
const projectName = `${baseProjectName}-${framework}`;
87120

88-
test("Nuxt", async () => {
89-
await runCli("nuxt", {
90-
overrides: {
91-
packageScripts: {
92-
build: "NITRO_PRESET=cloudflare-pages nuxt build",
93-
},
94-
},
121+
const { argv, overrides, promptHandlers, expectResponseToContain } =
122+
frameworkTests[framework];
123+
124+
await runCli(framework, {
125+
overrides,
126+
promptHandlers,
127+
argv: [...(argv ?? []), "--deploy", "--no-git"],
95128
});
96-
});
97129

98-
test("next", async () => {
99-
await runCli("next", {
130+
// Verify deployment
131+
const projectUrl = `https://${projectName}.pages.dev/`;
132+
133+
const res = await fetch(projectUrl);
134+
expect(res.status).toBe(200);
135+
136+
const body = await res.text();
137+
expect(
138+
body,
139+
`(${framework}) Deployed page (${projectUrl}) didn't contain expected string: "${expectResponseToContain}"`
140+
).toContain(expectResponseToContain);
141+
};
142+
143+
// These are ordered based on speed and reliability for ease of debugging
144+
const frameworkTests: Record<string, FrameworkTestConfig> = {
145+
astro: {
146+
expectResponseToContain: "Hello, Astronaut!",
147+
},
148+
hono: {
149+
expectResponseToContain: "/api/hello",
150+
},
151+
qwik: {
152+
expectResponseToContain: "Welcome to Qwik",
100153
promptHandlers: [
101154
{
102-
matcher: /Do you want to use the next-on-pages eslint-plugin\?/,
103-
input: ["y"],
155+
matcher: /Yes looks good, finish update/,
156+
input: [keys.enter],
104157
},
105158
],
106-
});
107-
});
108-
109-
test("qwik", async () => {
110-
await runCli("qwik", {
159+
},
160+
remix: {
161+
expectResponseToContain: "Welcome to Remix",
162+
},
163+
next: {
164+
expectResponseToContain: "Create Next App",
111165
promptHandlers: [
112166
{
113-
matcher: /Yes looks good, finish update/,
114-
input: [keys.enter],
167+
matcher: /Do you want to use the next-on-pages eslint-plugin\?/,
168+
input: ["y"],
115169
},
116170
],
117-
});
118-
});
119-
120-
test("solid", async () => {
121-
await runCli("solid", {
171+
},
172+
nuxt: {
173+
expectResponseToContain: "Welcome to Nuxt!",
174+
overrides: {
175+
packageScripts: {
176+
build: "NITRO_PRESET=cloudflare-pages nuxt build",
177+
},
178+
},
179+
},
180+
react: {
181+
expectResponseToContain: "React App",
182+
},
183+
solid: {
184+
expectResponseToContain: "Hello world",
122185
promptHandlers: [
123186
{
124187
matcher: /Which template do you want to use/,
@@ -133,11 +196,9 @@ describe("E2E: Web frameworks", () => {
133196
input: [keys.enter],
134197
},
135198
],
136-
});
137-
});
138-
139-
test("svelte", async () => {
140-
await runCli("svelte", {
199+
},
200+
svelte: {
201+
expectResponseToContain: "SvelteKit app",
141202
promptHandlers: [
142203
{
143204
matcher: /Which Svelte app template/,
@@ -152,19 +213,21 @@ describe("E2E: Web frameworks", () => {
152213
input: [keys.enter],
153214
},
154215
],
155-
});
156-
});
216+
},
217+
vue: {
218+
expectResponseToContain: "Vite App",
219+
},
220+
};
221+
222+
test.concurrent.each(Object.keys(frameworkTests))(
223+
"%s",
224+
async (name) => {
225+
await runCliWithDeploy(name);
226+
},
227+
{ retry: 3 }
228+
);
157229

158-
// This test blows up in CI due to Github providing an unusual git user email address.
159-
// E.g.
160-
// ```
161-
// fatal: empty ident name (for <[email protected].
162-
// internal.cloudapp.net>) not allowed
163-
// ```
164230
test.skip("Hono (wrangler defaults)", async () => {
165231
await runCli("hono", { argv: ["--wrangler-defaults"] });
166-
167-
// verify that wrangler-defaults defaults to `true` for using git
168-
expect(join(projectPath, ".git")).toExist();
169232
});
170233
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { execa } from "execa";
2+
3+
if (!process.env.CLOUDFLARE_API_TOKEN) {
4+
console.error("CLOUDFLARE_API_TOKEN must be set");
5+
process.exit(1);
6+
}
7+
8+
if (!process.env.CLOUDFLARE_ACCOUNT_ID) {
9+
console.error("CLOUDFLARE_ACCOUNT_ID must be set");
10+
process.exit(1);
11+
}
12+
13+
const npx = async (args) => {
14+
const argv = args.split(" ");
15+
return execa("npx", argv);
16+
};
17+
18+
const listProjectsToDelete = async () => {
19+
const toDelete = [];
20+
21+
const { stdout } = await npx("wrangler pages project list");
22+
23+
for (const line of stdout.split("\n")) {
24+
const c3ProjectRe = /(c3-e2e-[\w-]*)\s?/;
25+
const match = line.match(c3ProjectRe);
26+
27+
if (match) {
28+
toDelete.push(match[1]);
29+
}
30+
}
31+
32+
return toDelete;
33+
};
34+
35+
const deleteProjects = async (projects) => {
36+
for (const project of projects) {
37+
try {
38+
console.log(`Deleting project: ${project}`);
39+
await npx(`wrangler pages project delete -y ${project}`);
40+
} catch (error) {
41+
console.error(error);
42+
}
43+
}
44+
};
45+
46+
const projects = await listProjectsToDelete();
47+
deleteProjects(projects);

packages/create-cloudflare/src/common.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export const runDeploy = async (ctx: PagesGeneratorContext) => {
9393
const result = await runCommand(deployCmd, {
9494
silent: true,
9595
cwd: ctx.project.path,
96-
env: { CLOUDFLARE_ACCOUNT_ID: ctx.account.id },
96+
env: { CLOUDFLARE_ACCOUNT_ID: ctx.account.id, NODE_ENV: "production" },
9797
startText: `Deploying your application`,
9898
doneText: `${brandColor("deployed")} ${dim(`via \`${deployCmd}\``)}`,
9999
});

0 commit comments

Comments
 (0)