Skip to content

Commit c018844

Browse files
test: add e2e tests of the build+preview use case (un-revert of #8384) (#8436)
* test: add e2e tests of the build+preview use case (#8384) * test: add e2e tests of the build+preview use case * fixups: refactor tests to avoid boilerplate and make it easier to test all the combinations * Apply suggestions from code review Co-authored-by: James Opstad <[email protected]> --------- Co-authored-by: James Opstad <[email protected]> * avoid running binaries directly, which doesn't seem to work with yarn on Windows --------- Co-authored-by: James Opstad <[email protected]>
1 parent 383dc0a commit c018844

File tree

5 files changed

+115
-44
lines changed

5 files changed

+115
-44
lines changed
Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# vite-plugin e2e tests
22

3-
This directory contains e2e test that give more confidence that the plugin will work in real world scenarios outside the comfort of this monorepo.
3+
This directory contains e2e tests that give more confidence that the plugin will work in real world scenarios outside the comfort of this monorepo.
44

55
In general, these tests create test projects by copying a fixture from the `fixtures` directory into a temporary directory and then installing the local builds of the plugin along with its dependencies.
66

@@ -9,8 +9,10 @@ In general, these tests create test projects by copying a fixture from the `fixt
99
Simply use turbo to run the tests from the root of the monorepo.
1010
This will also ensure that the required dependencies have all been built before running the tests.
1111

12+
You will need to provide CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN for the Workers AI tests to pass.
13+
1214
```sh
13-
pnpm test:e2e -F @cloudflare/vite-plugin
15+
CLOUDFLARE_ACCOUNT_ID=xxxx CLOUDFLARE_API_TOKEN=yyyy pnpm test:e2e -F @cloudflare/vite-plugin
1416
```
1517

1618
## Developing e2e tests
@@ -19,7 +21,7 @@ These tests use a mock npm registry where the built plugin has been published.
1921

2022
The registry is booted up and loaded with the local build of the plugin and its local dependencies in the global-setup.ts file that runs once at the start of the e2e test run, and the server is killed and its caches removed at the end of the test run.
2123

22-
The Vite `test` function is an extended with additional helpers to setup clean copies of fixtures outside of the monorepo so that they can be isolated from any other dependencies in the project.
24+
The Vitest `test` function is extended with additional helpers to setup clean copies of fixtures outside of the monorepo so that they can be isolated from any other dependencies in the project.
2325

2426
The simplest test looks like:
2527

@@ -28,14 +30,23 @@ test("can serve a Worker request", async ({ expect, seed, viteDev }) => {
2830
const projectPath = await seed("basic");
2931
runCommand(`pnpm install`, projectPath);
3032

31-
const proc = await viteDev(projectPath);
33+
const proc = await viteDev("pnpm", "dev", projectPath);
3234
const url = await waitForReady(proc);
3335
expect(await fetchJson(url + "/api/")).toEqual({ name: "Cloudflare" });
3436
});
3537
```
3638

3739
- The `seed()` helper makes a copy of the named fixture into a temporary directory. It returns the path to the directory containing the copy (`projectPath` above). This directory will be deleted at the end of the test.
3840
- The `runCommand()` helper simply executes a one-shot command and resolves when it has exited. You can use this to install the dependencies of the fixture from the mock npm registry, as in the example above.
39-
- The `viteDev()` helper boots up the `vite dev` command and returns an object that can be used to monitor its output. The process will be killed at the end of the test.
40-
- The `waitForReady()` helper will resolve when the `vite dev` process has output its ready message, from which it will parse the url that can be fetched in the test.
41+
- The `viteCommand()` helper boots up the given long-lived command and returns an object that can be used to monitor its output. The process will be killed at the end of the test.
42+
- The `waitForReady()` helper will resolve when the `proc` process has output its ready message, from which it will parse the url that can be fetched in the test.
4143
- The `fetchJson()` helper makes an Undici fetch to the url parsing the response into JSON. It will retry every 250ms for up to 10 secs to minimize flakes.
44+
45+
## Debugging e2e tests
46+
47+
You can control the logging and cleanup via environment variables:
48+
49+
- Keep the temporary directory after the tests have completed: `CLOUDFLARE_VITE_E2E_KEEP_TEMP_DIRS=true`
50+
- See debug logs for the tests: `NODE_DEBUG=vite-plugin:test`
51+
- See debug logs for the mock npm registry: `NODE_DEBUG=mock-npm-registry`
52+
- See debug logs for Vite: `DEBUG="vite:*"`
Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,53 @@
11
import { describe } from "vitest";
22
import { fetchJson, runCommand, test, waitForReady } from "./helpers.js";
33

4-
describe("node compatibility", () => {
5-
describe.each(["pnpm --no-store", "npm", "yarn"])("using %s", (pm) => {
6-
test("can serve a Worker request", async ({ expect, seed, viteDev }) => {
7-
const projectPath = await seed("basic");
8-
runCommand(`${pm} install`, projectPath);
4+
const isWindows = process.platform === "win32";
95

10-
const proc = await viteDev(projectPath);
11-
const url = await waitForReady(proc);
12-
expect(await fetchJson(url + "/api/")).toEqual({ name: "Cloudflare" });
13-
});
14-
});
15-
});
6+
const packageManagers = ["pnpm", "npm", "yarn"] as const;
7+
const commands = ["dev", "preview"] as const;
8+
9+
describe("basic e2e tests", () => {
10+
describe.each(commands)('with "%s" command', (command) => {
11+
// TODO: re-enable `vite preview` tests on Windows (DEVX-1748)
12+
describe.skipIf(command === "preview" && isWindows).each(packageManagers)(
13+
'with "%s" package manager',
14+
(pm) => {
15+
describe("node compatibility", () => {
16+
test("can serve a Worker request", async ({
17+
expect,
18+
seed,
19+
viteCommand,
20+
}) => {
21+
const projectPath = await seed("basic");
22+
runCommand(`${pm} install`, projectPath);
23+
24+
const proc = await viteCommand(pm, command, projectPath);
25+
const url = await waitForReady(proc);
26+
expect(await fetchJson(url + "/api/")).toEqual({
27+
name: "Cloudflare",
28+
});
29+
});
30+
});
1631

17-
// This test checks that wrapped bindings which rely on additional workers with an authed connection to the CF API work
18-
describe("Workers AI", () => {
19-
test("can serve a Worker request", async ({ expect, seed, viteDev }) => {
20-
const projectPath = await seed("basic");
21-
runCommand(`npm install`, projectPath);
32+
// This test checks that wrapped bindings which rely on additional workers with an authed connection to the CF API work
33+
describe("Workers AI", () => {
34+
test("can serve a Worker request", async ({
35+
expect,
36+
seed,
37+
viteCommand,
38+
}) => {
39+
const projectPath = await seed("basic");
40+
runCommand(`${pm} install`, projectPath);
2241

23-
const proc = await viteDev(projectPath);
24-
const url = await waitForReady(proc);
42+
const proc = await viteCommand(pm, command, projectPath);
43+
const url = await waitForReady(proc);
2544

26-
expect(await fetchJson(url + "/ai/")).toEqual({
27-
response: expect.stringContaining("Workers AI"),
28-
});
45+
expect(await fetchJson(url + "/ai/")).toEqual({
46+
response: expect.stringContaining("Workers AI"),
47+
});
48+
});
49+
});
50+
}
51+
);
2952
});
3053
});

packages/vite-plugin-cloudflare/e2e/fixtures/basic/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"type": "module",
66
"scripts": {
7-
"build": "tsc -b && vite build",
7+
"build": "vite build",
88
"dev": "vite",
99
"lint": "eslint .",
1010
"preview": "vite preview"

packages/vite-plugin-cloudflare/e2e/global-setup.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import os from "node:os";
33
import path from "node:path";
44
import util from "node:util";
55
import { startMockNpmRegistry } from "@cloudflare/mock-npm-registry";
6-
import type { GlobalSetupContext } from "vitest/node";
6+
import type { TestProject } from "vitest/node";
7+
8+
const debuglog = util.debuglog("vite-plugin:test");
79

810
declare module "vitest" {
911
export interface ProvidedContext {
@@ -13,22 +15,27 @@ declare module "vitest" {
1315

1416
// Using a global setup means we can modify tests without having to re-install
1517
// packages into our temporary directory
16-
// Typings for the GlobalSetupContext are augmented in `global-setup.d.ts`.
17-
export default async function ({ provide }: GlobalSetupContext) {
18+
export default async function ({ provide }: TestProject) {
1819
const stopMockNpmRegistry = await startMockNpmRegistry(
1920
"@cloudflare/vite-plugin"
2021
);
2122

2223
// Create temporary directory to host projects used for testing
2324
const root = await fs.mkdtemp(path.join(os.tmpdir(), "vite-plugin-"));
25+
debuglog("Created temporary directory at " + root);
2426

27+
// The type of the provided `root` is defined in the `ProvidedContent` type above.
2528
provide("root", root);
2629

2730
// Cleanup temporary directory on teardown
2831
return async () => {
2932
await stopMockNpmRegistry();
3033

31-
console.log("Cleaning up temporary directory...");
32-
await fs.rm(root, { recursive: true, maxRetries: 10 });
34+
if (process.env.CLOUDFLARE_VITE_E2E_KEEP_TEMP_DIRS) {
35+
debuglog("Temporary directory left in-place at " + root);
36+
} else {
37+
debuglog("Cleaning up temporary directory...");
38+
await fs.rm(root, { recursive: true, maxRetries: 10 });
39+
}
3340
};
3441
}

packages/vite-plugin-cloudflare/e2e/helpers.ts

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,32 @@ import path from "node:path";
55
import util from "node:util";
66
import { stripAnsi } from "miniflare";
77
import kill from "tree-kill";
8-
import { test as baseTest, inject, vi } from "vitest";
8+
import { test as baseTest, inject, onTestFailed, vi } from "vitest";
99

1010
const debuglog = util.debuglog("vite-plugin:test");
1111

12+
const testEnv = {
13+
...process.env,
14+
// The following env vars are set to ensure that package managers
15+
// do not use the same global cache and accidentally hit race conditions.
16+
YARN_CACHE_FOLDER: "./.yarn/cache",
17+
YARN_ENABLE_GLOBAL_CACHE: "false",
18+
PNPM_HOME: "./.pnpm",
19+
npm_config_cache: "./.npm/cache",
20+
};
21+
1222
/**
1323
* Extends the Vitest `test()` function to support running vite in
1424
* well defined environments that represent real-world usage.
1525
*/
1626
export const test = baseTest.extend<{
1727
seed: (fixture: string) => Promise<string>;
18-
viteDev: (
28+
viteCommand: (
29+
pm: "pnpm" | "npm" | "yarn",
30+
command: "dev" | "preview",
1931
projectPath: string,
2032
options?: { flags?: string[]; maxBuffer?: number }
21-
) => Process;
33+
) => Promise<Process>;
2234
}>({
2335
/** Seed a test project from a fixture. */
2436
async seed({}, use) {
@@ -45,18 +57,24 @@ export const test = baseTest.extend<{
4557
}
4658
}
4759
},
48-
/** Start a `vite dev` command and wraps its outputs. */
49-
async viteDev({}, use) {
60+
/** Starts a command and wraps its outputs. */
61+
async viteCommand({}, use) {
5062
const processes: ChildProcess[] = [];
51-
await use((projectPath) => {
52-
debuglog("starting vite for " + projectPath);
53-
const proc = childProcess.exec(`pnpm exec vite dev`, {
63+
await use(async (pm, command, projectPath) => {
64+
if (command === "preview") {
65+
// We must first run the build command to generate the Worker that is to be previewed.
66+
await runCommand(`${pm} run build`, projectPath);
67+
}
68+
69+
debuglog(`starting "${command}" with ${pm} in ${projectPath}`);
70+
const proc = childProcess.exec(`${pm} run ${command}`, {
5471
cwd: projectPath,
72+
env: testEnv,
5573
});
5674
processes.push(proc);
5775
return wrap(proc);
5876
});
59-
debuglog("Closing down vite dev processes", processes.length);
77+
debuglog("Closing down command processes", processes.length);
6078
processes.forEach((proc) => proc.pid && kill(proc.pid));
6179
},
6280
});
@@ -68,7 +86,7 @@ export interface Process {
6886
}
6987

7088
/**
71-
* Wrap a long running child process to capture its stdio and make it available programmatically.
89+
* Wraps a long running child process to capture its stdio and make it available programmatically.
7290
*/
7391
function wrap(proc: childProcess.ChildProcess): Process {
7492
let stdout = "";
@@ -88,7 +106,7 @@ function wrap(proc: childProcess.ChildProcess): Process {
88106
stderr += chunk;
89107
});
90108
const closePromise = events.once(proc, "close");
91-
return {
109+
const wrappedProc = {
92110
get stdout() {
93111
return stripAnsi(stdout);
94112
},
@@ -99,12 +117,24 @@ function wrap(proc: childProcess.ChildProcess): Process {
99117
return closePromise.then(([exitCode]) => exitCode ?? -1);
100118
},
101119
};
120+
121+
onTestFailed(() => {
122+
console.log(
123+
`Wrapped process logs (${proc.spawnfile} ${proc.spawnargs.join(" ")}):`
124+
);
125+
console.log(wrappedProc.stdout);
126+
console.error(wrappedProc.stderr);
127+
});
128+
129+
return wrappedProc;
102130
}
103131

104132
export function runCommand(command: string, cwd: string) {
133+
debuglog(`Running "${command}"`);
105134
childProcess.execSync(command, {
106135
cwd,
107136
stdio: debuglog.enabled ? "inherit" : "ignore",
137+
env: testEnv,
108138
});
109139
}
110140

0 commit comments

Comments
 (0)