mirror of
https://github.com/willchen96/mike.git
synced 2026-07-26 23:51:08 +02:00
Port of the amal66/mike fork's Playwright end-to-end suite onto the upstream backend/ + frontend/ layout: - e2e/: auth.setup (bootstraps a confirmed e2e@mike.local user via the Supabase admin API and saves storageState), auth-flows, critical-path (create project -> upload PDF -> ask a question -> streamed response), chat-management, project-management, tabular-reviews, workflows-account; fixtures/test.pdf - playwright.config.ts: single-worker (shared test user), setup project + chromium project with saved auth state; webServer adapted from the fork's monorepo command (npm run dev --workspace apps/web) to upstream's layouts: backend `npm run dev` (health-checked on :3001) and frontend `npm run dev` (:3000) - root package.json (upstream has none): @playwright/test, typescript dev-deps and test:e2e scripts, trimmed from the fork's root manifest - root tsconfig.json scoped to e2e/ + playwright.config.ts so `npx tsc --noEmit` typechecks the suite - .gitignore: playwright artifacts + e2e/.auth (session tokens) Specs select by ARIA role/name and placeholder text only - no data-testid attributes, so no app-code changes are required. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CEguyEgXa9JjCciXCcVemC
110 lines
4.3 KiB
TypeScript
110 lines
4.3 KiB
TypeScript
import { test as setup, expect } from "@playwright/test";
|
|
import path from "path";
|
|
import fs from "fs";
|
|
|
|
const authFile = path.join(__dirname, ".auth/user.json");
|
|
|
|
/**
|
|
* Read a key out of backend/.env so the setup can reach Supabase with the
|
|
* service-role key without requiring the operator to export it manually.
|
|
*/
|
|
function readApiEnv(key: string): string | undefined {
|
|
if (process.env[key]) return process.env[key];
|
|
const envPath = path.join(__dirname, "..", "backend", ".env");
|
|
try {
|
|
const contents = fs.readFileSync(envPath, "utf8");
|
|
// dotenv semantics: a later assignment wins over an earlier one. CI
|
|
// does `cp .env.example .env` (which ships a PLACEHOLDER SUPABASE_URL)
|
|
// and then APPENDS the real values, so returning the FIRST match would
|
|
// hand back the placeholder (getaddrinfo ENOTFOUND your-project...).
|
|
// Iterate every line and keep the LAST matching value, mirroring how
|
|
// the API's dotenv loader resolves the file.
|
|
let value: string | undefined;
|
|
for (const line of contents.split("\n")) {
|
|
const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
|
|
if (m && m[1] === key) value = m[2].trim();
|
|
}
|
|
return value;
|
|
} catch {
|
|
/* .env not present — fall through to undefined */
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Idempotently create a confirmed Supabase user via the admin API. If the user
|
|
* already exists the admin endpoint returns a 422 which we treat as success.
|
|
*/
|
|
async function ensureUser(email: string, password: string) {
|
|
const supabaseUrl =
|
|
readApiEnv("SUPABASE_URL") ?? "http://127.0.0.1:54321";
|
|
const serviceKey = readApiEnv("SUPABASE_SECRET_KEY");
|
|
if (!serviceKey) {
|
|
throw new Error(
|
|
"SUPABASE_SECRET_KEY not found (checked env and backend/.env); " +
|
|
"cannot bootstrap E2E users",
|
|
);
|
|
}
|
|
|
|
const res = await fetch(`${supabaseUrl}/auth/v1/admin/users`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
apikey: serviceKey,
|
|
Authorization: `Bearer ${serviceKey}`,
|
|
},
|
|
body: JSON.stringify({
|
|
email,
|
|
password,
|
|
email_confirm: true,
|
|
}),
|
|
});
|
|
|
|
if (!res.ok && res.status !== 422) {
|
|
const body = await res.text();
|
|
// 422 == user already registered, which is fine for an idempotent setup.
|
|
if (!body.includes("already been registered")) {
|
|
throw new Error(
|
|
`Failed to create user ${email}: ${res.status} ${body}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The main authenticated session shared by every non-destructive test.
|
|
* Stored to e2e/.auth/user.json and loaded via the chromium project config.
|
|
*/
|
|
setup("authenticate", async ({ page }) => {
|
|
// Default to the credentials the spec files use (the specs log in with
|
|
// e2e@mike.local / E2eTestPass1!), so the suite runs out-of-the-box against
|
|
// a local stack with no env juggling. The bootstrapped user MUST match the
|
|
// password the specs type, or the valid-login tests fail; keeping the
|
|
// default here is the single source of truth. Override via env in CI.
|
|
const email = process.env.E2E_EMAIL ?? "e2e@mike.local";
|
|
const password = process.env.E2E_PASSWORD ?? "E2eTestPass1!";
|
|
|
|
/* Bootstrap the shared user plus a dedicated user for destructive auth
|
|
tests (logout / account deletion). The logout test calls Supabase
|
|
signOut() which uses GLOBAL scope and revokes the user's session
|
|
server-side; running it against the shared user would 401 every other
|
|
parallel worker. Isolating it onto its own user keeps the suite stable. */
|
|
await ensureUser(email, password);
|
|
await ensureUser(
|
|
process.env.E2E_LOGOUT_EMAIL ?? "e2e-logout@mike.local",
|
|
process.env.E2E_LOGOUT_PASSWORD ?? "E2eLogoutPass1!",
|
|
);
|
|
|
|
await page.goto("/login");
|
|
await expect(page).toHaveURL(/\/login/);
|
|
|
|
await page.fill("#email", email);
|
|
await page.fill("#password", password);
|
|
await page.click('button[type="submit"]');
|
|
|
|
/* After login the app redirects to /assistant */
|
|
await page.waitForURL(/\/assistant/, { timeout: 15_000 });
|
|
|
|
/* Save the authenticated session for all subsequent tests */
|
|
await page.context().storageState({ path: authFile });
|
|
});
|