Skip to content

Commit b0571fd

Browse files
test: add e2e tests of the build+preview use case (#8905)
* test: add e2e tests of the build+preview use case * fixups * add npm install retries * clarify mock registry usage * fix retry logic * ensure seeded directory is cleaned between tests * don't rely upon pre/post npm hooks * faster e2e tests * skip AI e2e tests if there is no account id or api token * fix up README * allow package install beforeAll hook to run for up to 200 secs * don't crash if killing test process fails * skip e2e build tests on Windows
1 parent fc47c79 commit b0571fd

File tree

9 files changed

+203
-148
lines changed

9 files changed

+203
-148
lines changed
Lines changed: 21 additions & 16 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,44 +9,49 @@ 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
1719

1820
These tests use a mock npm registry where the built plugin has been published.
1921

20-
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.
22+
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. The mock npm registry will delegate to the standard public npm registry for non-local dependency requests (e.g. vite, typescript, etc).
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+
There is a `seed()` helper to setup a clean copy of a fixture outside of the monorepo so that it can be isolated from any other dependencies in the project.
2325

2426
The simplest test looks like:
2527

2628
```ts
27-
test("can serve a Worker request", async ({ expect, seed, viteDev }) => {
28-
const projectPath = await seed("basic", "pnpm");
29+
const projectPath = await seed("basic", "pnpm");
2930

30-
const proc = await viteDev(projectPath);
31+
test("can serve a Worker request", async ({ expect, seed, runLongLived }) => {
32+
const proc = await runLongLived("npm", "dev", projectPath);
3133
const url = await waitForReady(proc);
3234
expect(await fetchJson(url + "/api/")).toEqual({ name: "Cloudflare" });
3335
});
3436
```
3537

36-
- The `seed()` helper does the following:
37-
- makes a copy of the named fixture into a temporary directory,
38+
- The `seed()` helper creates a `beforeAll()` block:
39+
- make a copy of the named fixture into a temporary directory,
3840
- updates the vite-plugin dependency in the package.json to match the local version
39-
- runs `npm install` (or equivalent package manager command) in the temporary project
41+
- runs `npm install` (or equivalent package manager command) in the temporary project - the dependencies are installed from the mock npm registry mentioned above.
4042
- returns the path to the directory containing the copy (`projectPath` above)
43+
- The `seed()` helper also creates an `afterAll()` block:
4144
- the temporary directory will be deleted at the end of the test.
4245
- 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.
43-
- 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.
44-
- 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.
46+
- The `runLongLived()` helper runs an npm script (from the package.json scripts section) and returns an object that can be used to monitor its output. The process will be killed at the end of the test.
47+
- The `waitForReady()` helper will resolve when a `vite dev` long lived process has output a ready message, from which it will parse the url that can be fetched in the test.
4548
- 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.
4649

47-
## Debugging the tests
50+
## Debugging e2e tests
4851

49-
You can provide the following environment variables to get access to the logs and the actual files being tested:
52+
You can control the logging and cleanup via environment variables:
5053

51-
- `NODE_DEBUG=vite-plugin:test` - this will display debugging log messages as well as the streamed output from the commands being run.
52-
- `CLOUDFLARE_VITE_E2E_KEEP_TEMP_DIRS=1` - this will prevent the temporary directory containing the test project from being deleted, so that you can go and play with it manually.
54+
- Keep the temporary directory after the tests have completed: `CLOUDFLARE_VITE_E2E_KEEP_TEMP_DIRS=true`
55+
- See debug logs for the tests: `NODE_DEBUG=vite-plugin:test`
56+
- See debug logs for the mock npm registry: `NODE_DEBUG=mock-npm-registry`
57+
- See debug logs for Vite: `DEBUG="vite:*"`
Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,45 @@
1-
import { describe } from "vitest";
2-
import { fetchJson, test, waitForReady } from "./helpers.js";
1+
import { describe, test } from "vitest";
2+
import { fetchJson, runLongLived, seed, waitForReady } from "./helpers.js";
33

4-
describe("node compatibility", () => {
5-
describe.each(["pnpm", "npm", "yarn"])("using %s", (pm) => {
6-
test("can serve a Worker request", async ({ expect, seed, viteDev }) => {
7-
const projectPath = await seed("basic", pm);
4+
const isWindows = process.platform === "win32";
5+
const packageManagers = ["pnpm", , "npm", "yarn"] as const;
6+
const commands = ["dev", "buildAndPreview"] as const;
87

9-
const proc = await viteDev(projectPath);
10-
const url = await waitForReady(proc);
11-
expect(await fetchJson(url + "/api/")).toEqual({ name: "Cloudflare" });
12-
});
13-
});
14-
});
8+
describe("basic e2e tests", () => {
9+
describe.each(packageManagers)('with "%s" package manager', async (pm) => {
10+
const projectPath = seed("basic", pm);
1511

16-
// This test checks that wrapped bindings which rely on additional workers with an authed connection to the CF API work
17-
describe("Workers AI", () => {
18-
test("can serve a Worker request", async ({ expect, seed, viteDev }) => {
19-
const projectPath = await seed("basic", "npm");
12+
describe.each(commands)('with "%s" command', (command) => {
13+
describe("node compatibility", () => {
14+
test.skipIf(isWindows && command === "buildAndPreview")(
15+
"can serve a Worker request",
16+
async ({ expect }) => {
17+
const proc = await runLongLived(pm, command, projectPath);
18+
const url = await waitForReady(proc);
19+
expect(await fetchJson(url + "/api/")).toEqual({
20+
name: "Cloudflare",
21+
});
22+
}
23+
);
24+
});
2025

21-
const proc = await viteDev(projectPath);
22-
const url = await waitForReady(proc);
26+
// This test checks that wrapped bindings which rely on additional workers with an authed connection to the CF API work
27+
// They are skipped if you have not provided the necessary account id and api token.
28+
describe.skipIf(
29+
!process.env.CLOUDFLARE_ACCOUNT_ID || !process.env.CLOUDFLARE_API_TOKEN
30+
)("Workers AI", () => {
31+
test.skipIf(isWindows && command === "buildAndPreview")(
32+
"can serve a Worker request",
33+
async ({ expect }) => {
34+
const proc = await runLongLived(pm, command, projectPath);
35+
const url = await waitForReady(proc);
2336

24-
expect(await fetchJson(url + "/ai/")).toEqual({
25-
response: expect.stringContaining("Workers AI"),
37+
expect(await fetchJson(url + "/ai/")).toEqual({
38+
response: expect.stringContaining("Workers AI"),
39+
});
40+
}
41+
);
42+
});
2643
});
2744
});
2845
});

packages/vite-plugin-cloudflare/e2e/dynamic.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import { describe } from "vitest";
2-
import { fetchJson, test, waitForReady } from "./helpers.js";
1+
import { describe, test } from "vitest";
2+
import { fetchJson, runLongLived, seed, waitForReady } from "./helpers.js";
3+
4+
const packageManagers = ["pnpm", "npm", "yarn"] as const;
35

46
describe("prebundling Node.js compatibility", () => {
5-
describe.each(["pnpm", "npm", "yarn"])("using %s", (pm) => {
7+
describe.each(packageManagers)('with "%s" package manager', (pm) => {
8+
const projectPath = seed("dynamic", pm);
9+
610
test("will not cause a reload on a dynamic import of a Node.js module", async ({
711
expect,
8-
seed,
9-
viteDev,
1012
}) => {
11-
const projectPath = await seed("dynamic", pm);
12-
13-
const proc = await viteDev(projectPath);
13+
const proc = await runLongLived(pm, "dev", projectPath);
1414
const url = await waitForReady(proc);
1515
expect(await fetchJson(url)).toEqual("OK!");
1616
expect(proc.stdout).not.toContain(

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"private": true,
55
"type": "module",
66
"scripts": {
7-
"build": "tsc -b && vite build",
7+
"build": "vite build",
8+
"buildAndPreview": "vite build && vite preview",
89
"dev": "vite",
910
"lint": "eslint .",
1011
"preview": "vite preview"
@@ -26,7 +27,6 @@
2627
"globals": "^15.14.0",
2728
"typescript": "~5.7.2",
2829
"typescript-eslint": "^8.22.0",
29-
"vite": "^6.1.0",
30-
"wrangler": "^3.108.1"
30+
"vite": "^6.1.0"
3131
}
3232
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"type": "module",
66
"scripts": {
77
"build": "vite build",
8+
"buildAndPreview": "vite build && vite preview",
89
"dev": "vite",
910
"lint": "eslint .",
1011
"preview": "vite preview"
@@ -19,7 +20,6 @@
1920
"globals": "^15.14.0",
2021
"typescript": "~5.7.2",
2122
"typescript-eslint": "^8.22.0",
22-
"vite": "^6.1.0",
23-
"wrangler": "^3.108.1"
23+
"vite": "^6.1.0"
2424
}
2525
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import util from "node:util";
55
import { startMockNpmRegistry } from "@cloudflare/mock-npm-registry";
66
import type { TestProject } from "vitest/node";
77

8+
const debuglog = util.debuglog("vite-plugin:test");
9+
810
declare module "vitest" {
911
export interface ProvidedContext {
1012
root: string;
@@ -20,15 +22,19 @@ export default async function ({ provide }: TestProject) {
2022

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

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

2630
// Cleanup temporary directory on teardown
2731
return async () => {
2832
await stopMockNpmRegistry();
2933

30-
if (!process.env.CLOUDFLARE_VITE_E2E_KEEP_TEMP_DIRS) {
31-
console.log("Cleaning up temporary directory...", root);
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...");
3238
await fs.rm(root, { recursive: true, maxRetries: 10 });
3339
}
3440
};

0 commit comments

Comments
 (0)