Skip to content

Commit 468f44a

Browse files
committed
Enhance testing setup with Docker integration and Vitest configuration
- Added testing instructions to README.md for Docker usage. - Updated package.json to include new test and coverage scripts for containerized testing. - Modified test files to utilize environment variables for API host configuration. - Introduced vite-env.d.ts to define environment variables for TypeScript. - Created run_test_container.mjs to manage test execution in Docker. - Updated Dockerfile.test and docker-compose.yml for test service configuration. - Added entrypoint script for test container setup. - Refactored CI scripts to use Docker for backend services.
1 parent 897e6ab commit 468f44a

File tree

15 files changed

+421
-14
lines changed

15 files changed

+421
-14
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,25 @@ running:
150150
docker compose -f docker-compose.build.yml build
151151
```
152152

153+
## Testing (Docker)
154+
155+
Frontend tests run in Vitest browser mode (Playwright). To keep the environment consistent, use the dedicated test runner compose file instead of the dev container.
156+
157+
Prereqs:
158+
- Ensure `./build-secrets/.npmrc` exists (same requirement as Docker builds).
159+
160+
Run tests:
161+
162+
```bash
163+
yarn test:container
164+
```
165+
166+
Run coverage:
167+
168+
```bash
169+
yarn coverage:container
170+
```
171+
153172
## pre-commit
154173

155174
[Pre-commit](https://pre-commit.com/) enforces code style practices in this

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
"test": "vitest run",
1414
"test:watch": "vitest watch",
1515
"coverage": "vitest run --coverage",
16-
"tsc": "tsc -b"
16+
"tsc": "tsc -b",
17+
"test:container": "node tools/scripts/run_test_container.mjs test",
18+
"coverage:container": "node tools/scripts/run_test_container.mjs coverage"
1719
},
1820
"engines": {
1921
"node": ">=20.17.0"

packages/dapper-ts/src/__tests__/index.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ let dapper: DapperTs;
1010

1111
beforeAll(() => {
1212
dapper = new DapperTs(() => {
13-
return { apiHost: "http://127.0.0.1:8000" };
13+
return {
14+
apiHost:
15+
import.meta.env.VITE_THUNDERSTORE_TEST_API_HOST ??
16+
"http://127.0.0.1:8000",
17+
};
1418
});
1519
});
1620

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
interface ImportMeta {
2+
readonly env: {
3+
readonly VITE_THUNDERSTORE_TEST_API_HOST?: string;
4+
readonly [key: string]: string | undefined;
5+
};
6+
}

packages/dapper-ts/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"outDir": "./dist",
2424
"rootDir": "./src",
2525
"jsx": "react-jsx",
26-
"types": ["@vitest/browser/providers/playwright"]
26+
"types": ["vite/client", "@vitest/browser/providers/playwright"]
2727
},
2828
"include": ["./src/**/*.tsx", "./src/**/*.ts"],
2929
"exclude": ["node_modules"]

packages/thunderstore-api/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
"@vitest/browser": "3.2.4",
3131
"playwright": "1.55.1",
3232
"typescript": "^5.6.2",
33-
"vitest": "3.2.4"
33+
"vite": "7.1.7",
34+
"vitest": "3.2.4",
35+
"zod": "^3.23.8"
3436
},
3537
"preconstruct": {
3638
"entrypoints": [

packages/thunderstore-api/src/__tests__/defaultConfig.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export const config = () => {
22
return {
3-
apiHost: "http://127.0.0.1:8000",
3+
apiHost:
4+
import.meta.env.VITE_THUNDERSTORE_TEST_API_HOST ??
5+
"http://127.0.0.1:8000",
46
};
57
};
68

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
interface ImportMeta {
2+
readonly env: {
3+
readonly VITE_THUNDERSTORE_TEST_API_HOST?: string;
4+
readonly [key: string]: string | undefined;
5+
};
6+
}

packages/thunderstore-api/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"outDir": "./dist",
2525
"rootDir": "./src",
2626
"jsx": "react-jsx",
27-
"types": ["@vitest/browser/providers/playwright"]
27+
"types": ["vite/client", "@vitest/browser/providers/playwright"]
2828
},
2929
"include": ["./src/**/*.tsx", "./src/**/*.ts"],
3030
"exclude": ["node_modules"]
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import { spawn } from "node:child_process";
2+
import process from "node:process";
3+
4+
function usage(exitCode = 1) {
5+
// Keep this short: it shows up in CI logs.
6+
console.error(
7+
"Usage: node tools/scripts/run_test_container.mjs <test|coverage> [-- <vitest args...>]"
8+
);
9+
process.exit(exitCode);
10+
}
11+
12+
function spawnLogged(command, args, options = {}) {
13+
const printable = [command, ...args].join(" ");
14+
console.log(printable);
15+
16+
return new Promise((resolve, reject) => {
17+
const child = spawn(command, args, {
18+
stdio: "inherit",
19+
shell: false,
20+
...options,
21+
});
22+
23+
child.on("error", reject);
24+
child.on("close", (code) => {
25+
if (code === 0) {
26+
resolve();
27+
} else {
28+
const err = new Error(`${printable} failed with exit code ${code}`);
29+
err.exitCode = code;
30+
reject(err);
31+
}
32+
});
33+
});
34+
}
35+
36+
async function waitForDjangoInContainer(
37+
composeFile,
38+
{ timeoutMs = 120_000, pollMs = 1_000 } = {}
39+
) {
40+
const deadline = Date.now() + timeoutMs;
41+
42+
const pythonCheck =
43+
"import http.client, sys; " +
44+
"c=http.client.HTTPConnection('127.0.0.1',8000,timeout=2); " +
45+
"c.request('GET','/'); " +
46+
"r=c.getresponse(); " +
47+
"sys.exit(0 if 200 <= r.status < 400 else 1)";
48+
49+
while (Date.now() < deadline) {
50+
try {
51+
await spawnLogged(
52+
"docker",
53+
[
54+
"compose",
55+
"-f",
56+
composeFile,
57+
"exec",
58+
"-T",
59+
"django",
60+
"python",
61+
"-c",
62+
pythonCheck,
63+
],
64+
{
65+
env: process.env,
66+
stdio: "ignore",
67+
}
68+
);
69+
return;
70+
} catch {
71+
// ignore until timeout
72+
}
73+
74+
const remainingSeconds = Math.max(
75+
0,
76+
Math.round((deadline - Date.now()) / 1000)
77+
);
78+
console.log(
79+
`Waiting for django to be ready inside container (${remainingSeconds}s remaining)`
80+
);
81+
await new Promise((r) => setTimeout(r, pollMs));
82+
}
83+
84+
throw new Error("Timed out waiting for django to be ready inside container");
85+
}
86+
87+
function parseArgs(argv) {
88+
const [subcommand, ...rest] = argv;
89+
if (!subcommand || subcommand === "-h" || subcommand === "--help") {
90+
usage(0);
91+
}
92+
93+
if (subcommand !== "test" && subcommand !== "coverage") {
94+
console.error(`Unknown subcommand: ${subcommand}`);
95+
usage(1);
96+
}
97+
98+
const dashDashIndex = rest.indexOf("--");
99+
// Yarn v1 forwards args without requiring `--`. Accept both forms:
100+
// yarn test:container path/to/test
101+
// yarn test:container -- path/to/test
102+
const vitestArgs =
103+
dashDashIndex === -1 ? rest : rest.slice(dashDashIndex + 1);
104+
105+
return { subcommand, vitestArgs };
106+
}
107+
108+
async function main() {
109+
const { subcommand, vitestArgs } = parseArgs(process.argv.slice(2));
110+
111+
const backendComposeFile =
112+
"tools/thunderstore-test-backend/docker-compose.yml";
113+
114+
// The test runner executes *inside* Docker. That container can resolve
115+
// service names like `django`, but it can't resolve the host-published
116+
// address `127.0.0.1:8000`. Override the backend's canonical host so
117+
// redirects stay usable from within the container network.
118+
const backendComposeEnv = {
119+
...process.env,
120+
PRIMARY_HOST: process.env.PRIMARY_HOST ?? "django:8000",
121+
};
122+
123+
try {
124+
// Start backend in the background.
125+
await spawnLogged(
126+
"docker",
127+
[
128+
"compose",
129+
"-f",
130+
backendComposeFile,
131+
"up",
132+
"-d",
133+
"--remove-orphans",
134+
"--build",
135+
// Only start backend dependencies; the same compose file also contains
136+
// the test runner services.
137+
"db",
138+
"redis",
139+
"rabbitmq",
140+
"minio",
141+
"django",
142+
],
143+
{ env: backendComposeEnv }
144+
);
145+
146+
// Wait until it's actually serving traffic (cold starts can take a while).
147+
// We probe from inside the container network so we don't depend on any
148+
// published host port and we implicitly wait for the backend setup
149+
// commands inside the containers to finish.
150+
await waitForDjangoInContainer(backendComposeFile, { timeoutMs: 300_000 });
151+
152+
const command = subcommand === "test" ? "test" : "coverage";
153+
154+
const args = [
155+
"compose",
156+
"-f",
157+
backendComposeFile,
158+
"run",
159+
"--rm",
160+
"--build",
161+
"cyberstorm-tests",
162+
"yarn",
163+
command,
164+
...vitestArgs,
165+
];
166+
167+
await spawnLogged("docker", args, { env: backendComposeEnv });
168+
} finally {
169+
// Always tear down the test backend so it doesn't conflict with dev.
170+
await spawnLogged(
171+
"docker",
172+
["compose", "-f", backendComposeFile, "down", "--remove-orphans"],
173+
{ env: backendComposeEnv }
174+
).catch(() => {
175+
// Best-effort cleanup.
176+
});
177+
}
178+
}
179+
180+
main().catch((err) => {
181+
console.error(err?.stack || String(err));
182+
process.exit(typeof err?.exitCode === "number" ? err.exitCode : 1);
183+
});

0 commit comments

Comments
 (0)