Skip to content

Commit ba5a460

Browse files
authored
Fix vitest config (#43)
## Why is this change necessary? Vitest was not behaving as expected. E2E commands were running unit tests. the "setup files" were being executed for each file, not once globally ## How does this change address the issue? switches to using more explicit include/exclude for vitest, instead of the `--dir` flag. switches to using the globalSetup for the E2E tests Switches to using some actual playwright calls in the E2E tests ## What side effects does this change have? none ## How is this change tested? downstream repo with docker backend, and with exe backend ## Other started preparing for Nuxt v4, and disabled devtools and telemetry in test mode
1 parent 2cf7191 commit ba5a460

File tree

7 files changed

+132
-96
lines changed

7 files changed

+132
-96
lines changed

template/frontend/nuxt.config.ts.jinja

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
import { defineNuxtConfig } from "nuxt/config";
33
export default defineNuxtConfig({
44
compatibilityDate: "2024-11-01",
5-
devtools: { enabled: true },
5+
future: {
6+
compatibilityVersion: 4,
7+
},
8+
devtools: { enabled: process.env.NODE_ENV !== "test" },
9+
telemetry: process.env.NODE_ENV !== "test",
610
// the conditional modules added in by the template make it complicated to format consistently...at least with only 3 'always included' modules
711
// prettier-ignore
812
modules: [

template/frontend/package.json.jinja

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,16 @@
1212
"generate": "pnpm run type-check && nuxt generate",
1313
"preview": "nuxt preview",
1414
"postinstall": "nuxt prepare && pnpm exec playwright-core install --only-shell chromium-headless-shell",
15-
"test-unit": "vitest --dir tests/unit --run --coverage",
16-
"test-unit:watch": "vitest --dir tests/unit",
17-
"test-compiled": "vitest --test-timeout=15000 --dir tests/compiled --run",
18-
"test-e2e": "{% endraw %}{% if not deploy_as_executable %}{% raw %}docker compose --file=../docker-compose.yaml build && dotenv -v USE_DOCKER_COMPOSE_FOR_VITEST_E2E=1{% endraw %}{% else %}{% raw %}dotenv -v USE_BUILT_BACKEND_FOR_VITEST_E2E=1{% endraw %}{% endif %}{% raw %} -- vitest --test-timeout=15000 --dir tests/e2e --run",
19-
"test-compiled:watch": "vitest --dir tests/compiled"{% endraw %}{% if frontend_uses_graphql %}{% raw %},
15+
"test-unit": "vitest --exclude=\"tests/e2e/**\" --exclude=\"tests/compiled/**\" --run --coverage",
16+
"test-unit:watch": "vitest --exclude=\"tests/e2e/**\" --exclude=\"tests/compiled/**\"",
17+
"test-compiled": "vitest --exclude=\"tests/e2e/**\" --exclude=\"tests/unit/**\" --test-timeout=15000 --run",
18+
"test-e2e": "{% endraw %}{% if not deploy_as_executable %}{% raw %}docker compose --file=../docker-compose.yaml build && dotenv -v USE_DOCKER_COMPOSE_FOR_VITEST_E2E=1{% endraw %}{% else %}{% raw %}dotenv -v USE_BUILT_BACKEND_FOR_VITEST_E2E=1{% endraw %}{% endif %}{% raw %} -- vitest --exclude=\"tests/unit/**\" --exclude=\"tests/compiled/**\" --test-timeout=15000 --run",
19+
"test-compiled:watch": "vitest --exclude=\"tests/e2e/**\" --exclude=\"tests/unit/**\" --test-timeout=15000"{% endraw %}{% if frontend_uses_graphql %}{% raw %},
2020
"codegen": "graphql-codegen --config codegen.ts"{% endraw %}{% endif %}{% raw %}
2121
},
2222
"dependencies": {
2323
"@nuxt/ui": "{% endraw %}{{ nuxt_ui_version }}{% raw %}",
2424
"nuxt": "{% endraw %}{{ nuxt_version }}{% raw %}",
25-
"typescript": "{% endraw %}{{ typescript_version }}{% raw %}",
2625
"vue": "{% endraw %}{{ vue_version }}{% raw %}",
2726
"vue-router": "{% endraw %}{{ vue_router_version }}{% raw %}"
2827
},
@@ -39,6 +38,7 @@
3938
"@nuxt/test-utils": "^3.17.2",{% endraw %}{% if frontend_uses_graphql %}{% raw %}
4039
"@nuxtjs/apollo": "5.0.0-alpha.14",{% endraw %}{% endif %}{% raw %}
4140
"@nuxtjs/eslint-config-typescript": "^12.1.0",
41+
"@playwright/test": "^1.52.0",
4242
"@vitest/coverage-istanbul": "^3.1.3",{% endraw %}{% if frontend_uses_graphql %}{% raw %}
4343
"@vue/apollo-composable": "^4.2.2",{% endraw %}{% endif %}{% raw %}
4444
"@vue/devtools-api": "^7.7.2",
@@ -57,6 +57,7 @@
5757
"playwright-core": "^1.52.0",
5858
"postcss": "^8.5.3",
5959
"tailwindcss": "^4.0.14",
60+
"typescript": "{% endraw %}{{ typescript_version }}{% raw %}",
6061
"vitest": "^3.1.3",
6162
"vue-eslint-parser": "^10.1.1",
6263
"vue-tsc": "^2.2.8"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { inject } from "vitest";
2+
3+
export function url(path: string): string {
4+
if (!path.startsWith("/")) {
5+
path = `/${path}`;
6+
}
7+
return `${inject("baseUrl")}${path}`;
8+
}

template/frontend/tests/e2e/index.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, test } from "vitest";
2-
import { getPage, url } from "~/tests/setup/app";
2+
import { url } from "~/tests/e2e/helpers/playwright";
3+
import { getPage } from "~/tests/setup/app";
34

45
describe("Index page", async () => {
56
test("Page displays Hello World", async () => {

template/frontend/tests/setup/app.ts

Lines changed: 5 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,20 @@
1-
import { execSync, spawn } from "child_process";
2-
import fs from "fs";
3-
import path from "path";
41
import type { Browser, Page } from "playwright";
52
import { chromium } from "playwright";
6-
import { afterAll, beforeAll } from "vitest";
7-
8-
import { APP_NAME, DEPLOYED_BACKEND_PORT_NUMBER, DEPLOYED_FRONTEND_PORT_NUMBER } from "~/tests/setup/constants";
3+
import { afterAll, beforeAll, beforeEach } from "vitest";
94

105
const isE2E = process.env.USE_DOCKER_COMPOSE_FOR_VITEST_E2E || process.env.USE_BUILT_BACKEND_FOR_VITEST_E2E;
11-
const isDockerE2E = process.env.USE_DOCKER_COMPOSE_FOR_VITEST_E2E;
12-
const isBuiltBackendE2E = process.env.USE_BUILT_BACKEND_FOR_VITEST_E2E;
136
let browser: Browser;
147
let page: Page;
15-
const executableExtension = process.platform === "win32" ? ".exe" : "";
16-
const repoRoot = path.resolve(__dirname, "../../../");
17-
const executablePath = path.resolve(repoRoot, `./backend/dist/${APP_NAME}/${APP_NAME}${executableExtension}`);
18-
if (isBuiltBackendE2E) {
19-
if (!fs.existsSync(executablePath) || !fs.statSync(executablePath).isFile()) {
20-
throw new Error(`File not found: ${executablePath}`);
21-
}
22-
}
23-
export const BASE_URL = `http://127.0.0.1:${
24-
isBuiltBackendE2E ? DEPLOYED_BACKEND_PORT_NUMBER : DEPLOYED_FRONTEND_PORT_NUMBER
25-
}`;
26-
export function url(path: string): string {
27-
if (!path.startsWith("/")) {
28-
path = `/${path}`;
29-
}
30-
return `${BASE_URL}${path}`;
31-
}
32-
const healthCheckUrl = `http://127.0.0.1:${
33-
isBuiltBackendE2E ? DEPLOYED_BACKEND_PORT_NUMBER.toString() + "/api/healthcheck" : DEPLOYED_FRONTEND_PORT_NUMBER
34-
}`; // TODO: if there is a backend, check that too, even if it's a docker-compose situation
8+
359
if (isE2E) {
3610
beforeAll(async () => {
37-
if (isBuiltBackendE2E) {
38-
console.log(`Starting app at ${executablePath} ...`);
39-
const child = spawn(executablePath, ["--host", "0.0.0.0"], {
40-
// TODO: figure out why Github CI pipelines fail without setting all allowed hosts
41-
stdio: "inherit",
42-
});
43-
child.on("close", (code) => {
44-
console.log(`Process exited with code ${code}`);
45-
});
46-
}
47-
if (isDockerE2E) {
48-
console.log("Starting docker-compose...");
49-
execSync("docker compose --file=../docker-compose.yaml up --detach --force-recreate --renew-anon-volumes", {
50-
stdio: "inherit",
51-
});
52-
}
5311
browser = await chromium.launch(); // headless by default
54-
page = await browser.newPage();
55-
// Wait for /api/healthcheck to become available
56-
const maxAttempts = 10;
57-
let attempts = 0;
58-
while (attempts < maxAttempts) {
59-
try {
60-
const res = await fetch(healthCheckUrl);
61-
if (res.ok) {
62-
break;
63-
}
64-
} catch {
65-
// ignore errors // TODO: make this more specific (e.g., only ignore network errors)
66-
}
67-
attempts++;
68-
console.log(`Waiting for ${healthCheckUrl} to become available... Attempt ${attempts}`);
69-
await new Promise((resolve) => setTimeout(resolve, 1000));
70-
}
71-
if (attempts === maxAttempts) {
72-
throw new Error(`Timeout waiting for ${healthCheckUrl}`);
73-
}
7412
}, 40 * 1000); // increase timeout to allow application to start
75-
13+
beforeEach(async () => {
14+
page = await browser.newPage();
15+
});
7616
afterAll(async () => {
7717
await browser.close();
78-
if (isBuiltBackendE2E) {
79-
console.log("Stopping application...");
80-
try {
81-
const res = await fetch(`${BASE_URL}/api/shutdown`);
82-
if (!res.ok) {
83-
throw new Error(`Failed to stop the application: ${res.statusText}`);
84-
}
85-
} catch (error) {
86-
const logFilePath = path.resolve(repoRoot, `./frontend/logs/${APP_NAME}-backend.log`);
87-
// sometimes it takes a second for the log file to be fully written to disk
88-
await new Promise((resolve) => setTimeout(resolve, 1000));
89-
if (!fs.existsSync(logFilePath) || !fs.statSync(logFilePath).isFile()) {
90-
throw new Error(`Log file not found: ${logFilePath}`, { cause: error });
91-
}
92-
const logData = fs.readFileSync(logFilePath, "utf-8");
93-
console.log("Application logs:\n", logData);
94-
throw error;
95-
}
96-
}
97-
if (isDockerE2E) {
98-
console.log("Stopping docker-compose...");
99-
execSync("docker compose --file=../docker-compose.yaml down", { stdio: "inherit" });
100-
}
10118
}, 40 * 1000); // increase timeout to allow application to stop
10219
}
10320

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { execSync, spawn } from "child_process";
2+
import fs from "fs";
3+
import path from "path";
4+
import type { Browser } from "playwright";
5+
import { chromium } from "playwright";
6+
import type { TestProject } from "vitest/node";
7+
import { APP_NAME, DEPLOYED_BACKEND_PORT_NUMBER, DEPLOYED_FRONTEND_PORT_NUMBER } from "~/tests/setup/constants";
8+
9+
const isE2E = process.env.USE_DOCKER_COMPOSE_FOR_VITEST_E2E || process.env.USE_BUILT_BACKEND_FOR_VITEST_E2E;
10+
const isDockerE2E = process.env.USE_DOCKER_COMPOSE_FOR_VITEST_E2E;
11+
const isBuiltBackendE2E = process.env.USE_BUILT_BACKEND_FOR_VITEST_E2E;
12+
let browser: Browser;
13+
14+
const executableExtension = process.platform === "win32" ? ".exe" : "";
15+
const repoRoot = path.resolve(__dirname, "../../../");
16+
const executablePath = path.resolve(repoRoot, `./backend/dist/${APP_NAME}/${APP_NAME}${executableExtension}`);
17+
if (isBuiltBackendE2E) {
18+
if (!fs.existsSync(executablePath) || !fs.statSync(executablePath).isFile()) {
19+
throw new Error(`File not found: ${executablePath}`);
20+
}
21+
}
22+
export const BASE_URL = `http://127.0.0.1:${
23+
isBuiltBackendE2E ? DEPLOYED_BACKEND_PORT_NUMBER : DEPLOYED_FRONTEND_PORT_NUMBER
24+
}`;
25+
const healthCheckUrl = `http://127.0.0.1:${
26+
isBuiltBackendE2E ? DEPLOYED_BACKEND_PORT_NUMBER.toString() + "/api/healthcheck" : DEPLOYED_FRONTEND_PORT_NUMBER
27+
}`; // TODO: if there is a backend, check that too, even if it's a docker-compose situation
28+
29+
export async function setup(project: TestProject) {
30+
project.provide("baseUrl", BASE_URL);
31+
if (isE2E) {
32+
if (isBuiltBackendE2E) {
33+
console.log(`Starting app at ${executablePath} ...`);
34+
const child = spawn(executablePath, ["--host", "0.0.0.0"], {
35+
// TODO: figure out why Github CI pipelines fail without setting all allowed hosts
36+
stdio: "inherit",
37+
});
38+
child.on("close", (code) => {
39+
console.log(`Process exited with code ${code}`);
40+
});
41+
}
42+
if (isDockerE2E) {
43+
console.log("Starting docker-compose...");
44+
execSync("docker compose --file=../docker-compose.yaml up --detach --force-recreate --renew-anon-volumes", {
45+
stdio: "inherit",
46+
});
47+
}
48+
browser = await chromium.launch(); // headless by default
49+
// Wait for /api/healthcheck to become available
50+
const maxAttempts = 10;
51+
let attempts = 0;
52+
while (attempts < maxAttempts) {
53+
try {
54+
const res = await fetch(healthCheckUrl);
55+
if (res.ok) {
56+
break;
57+
}
58+
} catch {
59+
// ignore errors // TODO: make this more specific (e.g., only ignore network errors)
60+
}
61+
attempts++;
62+
console.log(`Waiting for ${healthCheckUrl} to become available... Attempt ${attempts}`);
63+
await new Promise((resolve) => setTimeout(resolve, 1000));
64+
}
65+
if (attempts === maxAttempts) {
66+
throw new Error(`Timeout waiting for ${healthCheckUrl}`);
67+
}
68+
}
69+
}
70+
export async function teardown() {
71+
if (isE2E) {
72+
await browser.close();
73+
if (isBuiltBackendE2E) {
74+
console.log("Stopping application...");
75+
try {
76+
const res = await fetch(`${BASE_URL}/api/shutdown`);
77+
if (!res.ok) {
78+
throw new Error(`Failed to stop the application: ${res.statusText}`);
79+
}
80+
} catch (error) {
81+
const logFilePath = path.resolve(repoRoot, `./frontend/logs/${APP_NAME}-backend.log`);
82+
// sometimes it takes a second for the log file to be fully written to disk
83+
await new Promise((resolve) => setTimeout(resolve, 1000));
84+
if (!fs.existsSync(logFilePath) || !fs.statSync(logFilePath).isFile()) {
85+
throw new Error(`Log file not found: ${logFilePath}`, { cause: error });
86+
}
87+
const logData = fs.readFileSync(logFilePath, "utf-8");
88+
console.log("Application logs:\n", logData);
89+
throw error;
90+
}
91+
}
92+
if (isDockerE2E) {
93+
console.log("Stopping docker-compose...");
94+
execSync("docker compose --file=../docker-compose.yaml down", { stdio: "inherit" });
95+
}
96+
}
97+
}
98+
99+
declare module "vitest" {
100+
export interface ProvidedContext {
101+
baseUrl: string;
102+
}
103+
}

template/frontend/vitest.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export default defineVitestConfig({
1111
sequence: {
1212
shuffle: true,
1313
},
14+
include: ["tests/**/*.spec.ts"],
1415
coverage: {
1516
provider: "istanbul",
1617
reporter: ["text", "json", "html"],
@@ -21,5 +22,6 @@ export default defineVitestConfig({
2122
exclude: ["**/generated/graphql.ts", "**/codegen.ts", "**/nuxt.config.ts", ...coverageConfigDefaults.exclude],
2223
},
2324
setupFiles: ["./tests/setup/faker.ts", "./tests/setup/app.ts"],
25+
globalSetup: "./tests/setup/globalSetup.ts",
2426
},
2527
});

0 commit comments

Comments
 (0)