feat(platform): checkpoint current implementation state

This commit is contained in:
2026-04-01 07:42:03 +02:00
parent 3e53471f05
commit 8c5be51251
125 changed files with 10269 additions and 17808 deletions
+37 -4
View File
@@ -6,19 +6,52 @@ test.describe("Resources", () => {
await page.fill('input[type="email"]', "manager@capakraken.dev");
await page.fill('input[type="password"]', "manager123");
await page.click('button[type="submit"]');
await expect(page).toHaveURL(/\/(dashboard|resources)/);
await page.goto("/resources");
await expect(page).toHaveURL(/\/resources/);
});
test("shows resources list", async ({ page }) => {
await expect(page.locator("h1")).toContainText("Resources");
await expect(page.getByRole("heading", { name: "Resources" })).toBeVisible();
await expect(page.locator("table")).toBeVisible();
});
test("can search resources", async ({ page }) => {
const rows = page.locator("tbody tr");
await expect(rows.first()).toBeVisible();
const firstRowText = (await rows.first().textContent()) ?? "";
const searchTerm = firstRowText
.split(/\s+/)
.map((token) => token.replace(/[^A-Za-z0-9@._-]/g, "").trim())
.find((token) => token.length >= 3) ?? "EMP";
const searchInput = page.locator('input[type="search"]');
await searchInput.fill("EMP-001");
await searchInput.fill(searchTerm);
await page.waitForTimeout(500);
// Should show filtered results
await expect(page.locator("tbody tr")).toHaveCount(1);
await expect(searchInput).toHaveValue(searchTerm);
await expect(page.getByText(`Search: "${searchTerm}"`)).toBeVisible();
});
test("shows a not-found state for a missing resource detail page", async ({ page }) => {
await page.goto("/resources/does-not-exist");
await expect(page.getByText("Resource not found.")).toBeVisible();
await expect(page.getByRole("link", { name: "Back to resources" })).toBeVisible();
});
test("shows a generic load error when the resource detail query fails", async ({ page }) => {
await page.route("**/api/trpc/resource.getById**", async (route) => {
await route.fulfill({
status: 500,
contentType: "application/json",
body: JSON.stringify({ error: { message: "boom" } }),
});
});
await page.goto("/resources/test-resource-id");
await expect(page.getByText("This resource could not be loaded right now.")).toBeVisible();
await expect(page.getByRole("link", { name: "Back to resources" })).toBeVisible();
});
});
+68 -8
View File
@@ -1,4 +1,5 @@
import { spawn } from "node:child_process";
import { randomBytes } from "node:crypto";
import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { createServer } from "node:net";
import { dirname, resolve } from "node:path";
@@ -7,12 +8,16 @@ import { fileURLToPath } from "node:url";
const currentDir = dirname(fileURLToPath(import.meta.url));
const workspaceRoot = resolve(currentDir, "../../..");
const webRoot = resolve(currentDir, "..");
const runtimeEnvPath = resolve(currentDir, ".playwright-runtime.json");
const webEnvLocal = resolve(webRoot, ".env.local");
const webEnvBackup = resolve(webRoot, ".env.local.e2e-backup");
const webDistDir = ".next-e2e";
const webDistDirPath = resolve(webRoot, webDistDir);
const managedEnvBanner = "# Managed by apps/web/e2e/test-server.mjs";
const e2ePort = process.env.PLAYWRIGHT_TEST_PORT ?? "3110";
const e2eBaseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL ?? `http://localhost:${e2ePort}`;
const e2eAuthSecret = process.env.PLAYWRIGHT_AUTH_SECRET ?? `capakraken-e2e-${randomBytes(24).toString("hex")}`;
const manageWebEnvFile = process.env.PLAYWRIGHT_MANAGE_WEB_ENV_FILE === "true";
const composeProjectName = `capakraken-e2e-${process.pid}`;
const managedEnvKeys = [
"DATABASE_URL",
@@ -68,12 +73,24 @@ function applyEnv(env) {
}
function writeManagedWebEnv(rootEnv) {
if (existsSync(webEnvBackup)) {
if (!manageWebEnvFile) {
restoreWebEnv();
return;
}
if (existsSync(webEnvBackup) && isManagedEnvFile(webEnvBackup)) {
rmSync(webEnvBackup, { force: true });
}
if (existsSync(webEnvLocal)) {
renameSync(webEnvLocal, webEnvBackup);
if (isManagedEnvFile(webEnvLocal)) {
rmSync(webEnvLocal, { force: true });
} else {
if (existsSync(webEnvBackup)) {
rmSync(webEnvBackup, { force: true });
}
renameSync(webEnvLocal, webEnvBackup);
}
}
const contents = managedEnvKeys
@@ -84,16 +101,41 @@ function writeManagedWebEnv(rootEnv) {
.filter(Boolean)
.join("\n");
writeFileSync(webEnvLocal, `${contents}\n`, "utf8");
writeFileSync(webEnvLocal, `${managedEnvBanner}\n${contents}\n`, "utf8");
}
function restoreWebEnv() {
if (existsSync(webEnvLocal)) {
if (existsSync(webEnvLocal) && isManagedEnvFile(webEnvLocal)) {
rmSync(webEnvLocal, { force: true });
}
if (existsSync(webEnvBackup)) {
renameSync(webEnvBackup, webEnvLocal);
if (isManagedEnvFile(webEnvBackup)) {
rmSync(webEnvBackup, { force: true });
} else {
renameSync(webEnvBackup, webEnvLocal);
}
}
}
let restoredManagedEnv = false;
function restoreWebEnvOnce() {
if (restoredManagedEnv) {
return;
}
restoredManagedEnv = true;
restoreWebEnv();
rmSync(runtimeEnvPath, { force: true });
}
function isManagedEnvFile(filePath) {
try {
const contents = readFileSync(filePath, "utf8");
return contents.includes(managedEnvBanner) || contents.includes("E2E_TEST_MODE=true");
} catch {
return false;
}
}
@@ -307,15 +349,33 @@ if (!/(^|_)(test|e2e|ci)$/u.test(playwrightDatabaseName)) {
process.env.DATABASE_URL = playwrightDatabaseUrl;
process.env.PLAYWRIGHT_DATABASE_URL = playwrightDatabaseUrl;
process.env.POSTGRES_TEST_PORT = String(selectedTestDbPort);
process.env.CAPAKRAKEN_EXPECTED_DB_NAME = playwrightDatabaseName;
process.env.ALLOW_DESTRUCTIVE_DB_TOOLS = "true";
process.env.CONFIRM_DESTRUCTIVE_DB_NAME = playwrightDatabaseName;
process.env.NODE_ENV = process.env.NODE_ENV ?? "development";
process.env.PORT = e2ePort;
process.env.NEXTAUTH_URL = e2eBaseUrl;
process.env.AUTH_URL = e2eBaseUrl;
process.env.NEXTAUTH_SECRET = e2eAuthSecret;
process.env.AUTH_SECRET = e2eAuthSecret;
process.env.NEXT_DIST_DIR = webDistDir;
process.env.E2E_TEST_MODE = "true";
writeFileSync(
runtimeEnvPath,
JSON.stringify(
{
DATABASE_URL: process.env.DATABASE_URL,
PLAYWRIGHT_DATABASE_URL: process.env.PLAYWRIGHT_DATABASE_URL,
POSTGRES_TEST_PORT: process.env.POSTGRES_TEST_PORT,
BASE_URL: e2eBaseUrl,
},
null,
2,
),
"utf8",
);
writeManagedWebEnv(rootEnv);
process.on("exit", restoreWebEnvOnce);
try {
await cleanupStaleE2eArtifacts();
@@ -333,19 +393,19 @@ try {
for (const signal of ["SIGINT", "SIGTERM"]) {
process.on(signal, () => {
restoreWebEnv();
restoreWebEnvOnce();
void cleanupComposeProject();
server.kill(signal);
});
}
server.on("exit", async (code) => {
restoreWebEnv();
restoreWebEnvOnce();
await cleanupComposeProject();
process.exit(code ?? 0);
});
} catch (error) {
restoreWebEnv();
restoreWebEnvOnce();
await cleanupComposeProject();
throw error;
}
File diff suppressed because it is too large Load Diff