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
132 changes: 132 additions & 0 deletions .codevalid/ui/mock/mock-api-server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* Lightweight mock API server for CodeValid seed tests.
*
* Listens on port 5436 (the default VITE_API_BASE_URL fallback).
* Handles all endpoints the app polls at startup and during tests:
* GET /health → 200 { status: 'ok' }
* POST /api/users/identify → 200 { userId:1, userName, authorized:true }
* POST /api/users/signup → 200 { userId:1, userName, createdAtUtc, eventStatus }
* GET /api/users/me/settings → 200 { hasSettings:false, settings:{…} }
* GET /api/rides* → 200 []
* GET /api/dashboard* → 200 {}
* * everything else → 200 {}
*
* Started by playwright.config.js webServer block.
*/

import http from "http";

const PORT = parseInt(process.env.MOCK_API_PORT || "5436", 10);

const EMPTY_SETTINGS = {
averageCarMpg: null,
yearlyGoalMiles: null,
oilChangePrice: null,
mileageRateCents: null,
locationLabel: null,
latitude: null,
longitude: null,
dashboardGallonsAvoidedEnabled: false,
dashboardGoalProgressEnabled: false,
updatedAtUtc: null,
weatherApiKey: null,
eiaGasApiKey: null,
};

function json(res, body, status = 200) {
const payload = JSON.stringify(body);
res.writeHead(status, {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(payload),
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,POST,PUT,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type,X-User-Id",
});
res.end(payload);
}

function readBody(req) {
return new Promise((resolve) => {
const chunks = [];
req.on("data", (c) => chunks.push(c));
req.on("end", () => {
try {
resolve(JSON.parse(Buffer.concat(chunks).toString() || "{}"));
} catch {
resolve({});
}
});
});
}

const server = http.createServer(async (req, res) => {
const url = req.url.split("?")[0];
const method = req.method;

// CORS preflight
if (method === "OPTIONS") {
res.writeHead(204, {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,POST,PUT,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type,X-User-Id",
});
return res.end();
}

// Health check (ApiStartupGuard polls this)
if (url === "/health" || url === "/api/health") {
return json(res, { status: "ok" });
}

// Login / identify
if (url === "/api/users/identify" && method === "POST") {
const body = await readBody(req);
if (body.name && body.pin) {
return json(res, { userId: 1, userName: body.name, authorized: true });
}
return json(res, { code: "unauthorized", message: "Name or PIN is incorrect." }, 401);
}

// Signup
if (url === "/api/users/signup" && method === "POST") {
const body = await readBody(req);
return json(res, {
userId: 1,
userName: body.name || "test-user",
createdAtUtc: new Date().toISOString(),
eventStatus: "queued",
});
}

// User settings
if (url === "/api/users/me/settings") {
if (method === "GET") {
return json(res, { hasSettings: false, settings: EMPTY_SETTINGS });
}
if (method === "PUT") {
return json(res, { hasSettings: true, settings: EMPTY_SETTINGS });
}
}

// Rides
if (url.startsWith("/api/rides")) {
if (method === "GET") return json(res, []);
return json(res, {});
}

// Dashboard
if (url.startsWith("/api/dashboard") || url.startsWith("/api/stats")) {
return json(res, {});
}

// Catch-all
return json(res, {});
});

server.listen(PORT, "0.0.0.0", () => {
console.log(`Mock API server listening on http://0.0.0.0:${PORT}`);
});

// Keep alive
process.on("SIGTERM", () => server.close());
process.on("SIGINT", () => server.close());
62 changes: 62 additions & 0 deletions .codevalid/ui/mock/mock-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Playwright route helpers for CodeValid task tests.
*
* The mock API server (mock-api-server.js) already handles all HTTP calls
* at the network level (started by playwright.config.js webServer).
* These helpers are available for task tests that need to override specific
* endpoint responses within a single test (e.g. simulate a 401 or 429).
*
* Usage in a task test:
* import { overrideLogin } from "../../mock/mock-api.js";
* test.beforeEach(async ({ page }) => { await overrideLogin(page, { fail: true }); });
*/

/**
* Override the login endpoint for a single test.
* @param {import('@playwright/test').Page} page
* @param {{ fail?: boolean, status?: number }} opts
*/
export async function overrideLogin(page, { fail = false, status = 200 } = {}) {
await page.route("**/api/users/identify", async (route) => {
if (fail) {
await route.fulfill({
status: status || 401,
contentType: "application/json",
body: JSON.stringify({ code: "unauthorized", message: "Name or PIN is incorrect." }),
});
} else {
const body = route.request().postDataJSON() ?? {};
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ userId: 1, userName: body.name ?? "test-user", authorized: true }),
});
}
});
}

/**
* Override the user settings endpoint for a single test.
* @param {import('@playwright/test').Page} page
* @param {object} settings - partial settings object to return
*/
export async function overrideSettings(page, settings = {}) {
const merged = {
averageCarMpg: null, yearlyGoalMiles: null, oilChangePrice: null,
mileageRateCents: null, locationLabel: null, latitude: null, longitude: null,
dashboardGallonsAvoidedEnabled: false, dashboardGoalProgressEnabled: false,
updatedAtUtc: null, weatherApiKey: null, eiaGasApiKey: null,
...settings,
};
await page.route("**/api/users/me/settings", async (route) => {
if (route.request().method() === "GET") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ hasSettings: true, settings: merged }),
});
} else {
await route.continue();
}
});
}
61 changes: 61 additions & 0 deletions .codevalid/ui/seed_test/seed_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Seed test — proves the BikeTracking frontend starts, is reachable,
* and the login page renders a visible heading.
*
* Project: codevalid-sample-test
* Stack: React 19 + Vite 8, name+PIN form auth (no SSO/Cognito)
*
* The mock API server (mock/mock-api-server.js) handles:
* GET /health → 200 (satisfies ApiStartupGuard)
* POST /api/users/identify → 200 (login)
* …and other endpoints the app polls after login.
*/

import { test, expect } from "@playwright/test";
import { ExecutionRecorder } from "../helpers/execution-recorder.js";

test.describe("Seed — app reachability", () => {
test("login page loads and shows the app heading", async ({ page }, testInfo) => {
const recorder = new ExecutionRecorder({
testId: "seed-001",
testTitle: "login page loads and shows the app heading",
});

await recorder.step("navigate to app root", async () => {
await page.goto("/");
});

// The app redirects "/" → "/login". ApiStartupGuard polls /health on the
// mock API server (port 5436). Once it gets 200, children render.
await recorder.step("assert login heading is visible", async () => {
await expect(
page.getByRole("heading", { name: /commute bike tracker/i })
).toBeVisible({ timeout: 20000 });
});

await recorder.step("assert login form fields are present", async () => {
await expect(page.getByLabel(/name/i)).toBeVisible();
await expect(page.getByLabel(/pin/i)).toBeVisible();
await expect(page.getByRole("button", { name: /log in/i })).toBeVisible();
});

await recorder.save(testInfo);
});

test("title tag is set correctly", async ({ page }, testInfo) => {
const recorder = new ExecutionRecorder({
testId: "seed-002",
testTitle: "title tag is set correctly",
});

await recorder.step("navigate to app root", async () => {
await page.goto("/");
});

await recorder.step("assert document title", async () => {
await expect(page).toHaveTitle(/BikeTracking/i, { timeout: 15000 });
});

await recorder.save(testInfo);
});
});
37 changes: 37 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# ── .dockerignore ─────────────────────────────────────────────────────────────
# Build context is the repo root.
# Exclude heavy or irrelevant directories so `COPY . .` stays fast.

# Node dependencies (will be installed inside the image)
node_modules/
src/BikeTracking.Frontend/node_modules/

# Framework / Vite build output
dist/
src/BikeTracking.Frontend/dist/

# .NET / IDE artefacts
bin/
obj/
*.user
*.suo
.idea/
.vs/

# Tauri native build output
src-tauri/target/

# Git metadata
.git/

# OS generated
.DS_Store
Thumbs.db

# CodeValid recordings (generated at test runtime, not needed at build time)
.codevalid/ui/recording/
.codevalid/ui/run-results/

# Wrong lockfile: this is an npm repo (package-lock.json)
pnpm-lock.yaml
yarn.lock
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,15 @@ src/BikeTracking.Frontend/playwright-report/

# Tauri: Rust build artifacts (Cargo.lock IS committed — app crate convention)
src/BikeTracking.Frontend/src-tauri/target/

# CodeValid — auto-added
.next/
out/
.turbo/
__pycache__/
.pytest_cache/
.venv/
.codevalid/ui/Dockerfile
.codevalid/ui/playwright.config.js
.codevalid/ui/helper/
.codevalid/ui/helpers/
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"type": "module",
"devDependencies": {
"skills": "^1.5.13"
}
Expand Down
Loading