Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions .github/workflows/code-checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: Code Checks

on:
pull_request:
push:
branches:
- master

concurrency:
group: code-checks-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
checks:
name: Lint, Format, Unit Tests
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Lint check
run: pnpm run lint:check

- name: Format check
run: pnpm run format:check

- name: Unit tests
run: pnpm run test

e2e-frontend:
name: Frontend E2E Tests
runs-on: ubuntu-latest
needs: checks

services:
mongodb:
image: mongo:7
ports:
- 27017:27017
options: >-
--health-cmd "mongosh --eval 'db.adminCommand({ ping: 1 })' || exit 1"
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Install Playwright browsers
run: pnpm --filter astriarch-frontend exec playwright install --with-deps chromium

- name: Run frontend E2E
env:
TEST_BACKEND_PORT: 8002
MONGODB_CONNECTION_STRING: mongodb://127.0.0.1:27017/astriarch_v2_test
run: pnpm --filter astriarch-frontend run test:e2e

- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: |
apps/astriarch-frontend/playwright-report
apps/astriarch-frontend/test-results
if-no-files-found: ignore
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,29 @@ Run Frontend and Backend by navigating to the proper directory and run:

then visit `http://localhost:5173`

## E2E Testing

Playwright end-to-end tests live in the frontend workspace and run against a real frontend + backend + MongoDB stack.

Prerequisites:

- Node.js 18+
- `pnpm install`
- `npx playwright install chromium`
- MongoDB running locally (`docker-compose up -d mongodb`)

Run the suite:

- `pnpm --filter astriarch-frontend test:e2e`
- `pnpm --filter astriarch-frontend test:e2e:headed`
- `pnpm --filter astriarch-frontend test:e2e:ui`

Notes:

- The Playwright config starts a dedicated test backend on port `8002` and frontend on port `4173`.
- Test cleanup is performed via a test-only backend endpoint (`/api/test/cleanup`) that is enabled only when `NODE_ENV=test`.
- Add new scenarios under `apps/astriarch-frontend/e2e/scenarios` and reuse fixtures/helpers from `apps/astriarch-frontend/e2e/fixtures` and `apps/astriarch-frontend/e2e/helpers`.


## Overview

Expand Down
11 changes: 11 additions & 0 deletions apps/astriarch-backend/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/*.spec.ts'],
clearMocks: true,
transform: {
'^.+\\.tsx?$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.jest.json' }],
},
};
4 changes: 3 additions & 1 deletion apps/astriarch-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
"format": "prettier --write \"src/**/*.ts\"",
"format:check": "prettier --check \"src/**/*.ts\"",
"lint": "eslint src/**/*.ts",
"lint:check": "eslint src/**/*.ts",
"type-check": "tsc --noEmit",
"test": "jest",
"test:watch": "jest --watch"
"test:watch": "jest --watch",
"cleanup:test-data": "node scripts/cleanup-test-data.js"
},
"dependencies": {
"astriarch-engine": "workspace:*",
Expand Down
29 changes: 29 additions & 0 deletions apps/astriarch-backend/scripts/cleanup-test-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env node

const backendPort = process.env.TEST_BACKEND_PORT || '8002';
const prefix = process.env.E2E_PREFIX || '__e2e__';
const cleanupUrl = `http://localhost:${backendPort}/api/test/cleanup`;

async function run() {
try {
const res = await fetch(cleanupUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prefix }),
});

if (!res.ok) {
const body = await res.text();
console.error(`[cleanup-test-data] failed: HTTP ${res.status} ${body}`);
process.exit(1);
}

const body = await res.json();
console.log('[cleanup-test-data] success', body);
} catch (err) {
console.error('[cleanup-test-data] request failed', err);
process.exit(1);
}
}

run();
7 changes: 7 additions & 0 deletions apps/astriarch-backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { connectDatabase } from "./database/connection";
import { WebSocketServer } from "./websocket";
import { healthRoutes } from "./routes/healthRoutes";
import { highScoreRoutes } from "./routes/highScoreRoutes";
import { testRoutes } from "./routes/testRoutes";
import { GameController } from "./controllers/GameControllerWebSocket";
import { logger } from "./utils/logger";

Expand Down Expand Up @@ -82,6 +83,12 @@ app.use(
app.use("/api/health", healthRoutes);
app.use("/api/highscores", highScoreRoutes);

// Test-only routes — only registered in test environment.
if (process.env.NODE_ENV === "test") {
app.use("/api/test", testRoutes);
logger.info("Test routes registered (NODE_ENV=test)");
}

// 404 handler
app.use("*", (req, res) => {
res.status(404).json({ error: "Not found" });
Expand Down
52 changes: 52 additions & 0 deletions apps/astriarch-backend/src/routes/healthRoutes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { Request, Response } from "express";

import { healthRoutes } from "./healthRoutes";

function getHealthRouteHandler() {
const routeLayer = (healthRoutes as any).stack.find(
(layer: any) => layer.route?.path === "/" && layer.route?.methods?.get,
);

if (!routeLayer) {
throw new Error("Health route GET / handler not found");
}

return routeLayer.route.stack[0].handle as (req: Request, res: Response) => void;
}

describe("healthRoutes", () => {
beforeEach(() => {
jest.spyOn(console, "log").mockImplementation(() => {});
});

afterEach(() => {
jest.restoreAllMocks();
});

it("returns the expected health payload and initializes session state", () => {
const handler = getHealthRouteHandler();
const json = jest.fn();

const req = {
session: {},
sessionID: "test-session-id",
} as unknown as Request;
const res = {
json,
} as unknown as Response;

handler(req, res);

expect((req.session as any).created).toEqual(expect.any(String));
expect(json).toHaveBeenCalledTimes(1);

const payload = json.mock.calls[0][0];
expect(payload).toMatchObject({
status: "OK",
version: "2.0.0",
});
expect(typeof payload.timestamp).toBe("string");
expect(Number.isNaN(Date.parse(payload.timestamp))).toBe(false);
expect(typeof payload.uptime).toBe("number");
});
});
65 changes: 65 additions & 0 deletions apps/astriarch-backend/src/routes/testRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* Test-only cleanup routes.
*
* These routes are ONLY registered when NODE_ENV=test and should NEVER
* be reachable in production or staging environments.
*
* POST /api/test/cleanup
* Removes games and sessions whose name starts with the given prefix
* (default "__e2e__") so that E2E test runs start from a clean state.
*/

import { Router, Request, Response } from "express";
import mongoose from "mongoose";
import { ServerGameModel } from "../models/Game";
import { logger } from "../utils/logger";

const router: Router = Router();

router.post("/cleanup", async (req: Request, res: Response) => {
const prefix: string = (req.body?.prefix as string) || "__e2e__";

// Extra safety guard — only allow the e2e prefix pattern.
if (!prefix.startsWith("__e2e__")) {
res.status(400).json({ error: "Invalid prefix: must start with '__e2e__'" });
return;
}

try {
// Remove games whose name starts with the prefix.
const gameResult = await ServerGameModel.deleteMany({
name: { $regex: `^${escapeRegex(prefix)}` },
});

// Remove all sessions — safe because this route only exists in NODE_ENV=test.
const db = mongoose.connection.db;
let sessionCount = 0;
if (db) {
try {
const sessionResult = await db.collection("sessions").deleteMany({});
sessionCount = sessionResult.deletedCount ?? 0;
} catch {
// Sessions collection may not exist yet — not a fatal error.
}
}

logger.info(
`[test/cleanup] Deleted ${gameResult.deletedCount} game(s) and ${sessionCount} session(s) with prefix "${prefix}"`,
);

res.json({
ok: true,
gamesDeleted: gameResult.deletedCount,
sessionsDeleted: sessionCount,
});
} catch (error) {
logger.error("[test/cleanup] Cleanup failed:", error);
res.status(500).json({ error: "Cleanup failed" });
}
});

function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

export { router as testRoutes };
13 changes: 13 additions & 0 deletions apps/astriarch-backend/tsconfig.jest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["node", "jest"]
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}
3 changes: 2 additions & 1 deletion apps/astriarch-backend/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"exclude": [
"node_modules",
"dist",
"src/tests/**/*"
"src/tests/**/*",
"src/**/*.spec.ts"
]
}
4 changes: 4 additions & 0 deletions apps/astriarch-frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@ vite.config.ts.timestamp-*

*storybook.log
storybook-static

# Playwright artifacts
playwright-report/
test-results/
Loading
Loading