feat(platform): checkpoint current implementation state
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+1274
-3
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user