test: add core e2e fixtures and specs

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
Cursor Agent
2026-06-20 04:08:57 +00:00
parent 4fb4a6c5f0
commit 3c222660c1
11 changed files with 276 additions and 0 deletions
+2
View File
@@ -51,11 +51,13 @@
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^25.9.3",
"@types/pg": "^8.20.0",
"@vitest/coverage-v8": "^4.1.9",
"happy-dom": "^20.10.5",
"knip": "^6.17.1",
"lefthook": "^2.1.9",
"npm-check-updates": "^22.2.3",
"pg": "^8.21.0",
"turbo": "^2.9.18",
"typescript": "^6.0.3",
"vitest": "^4.1.9"
+6
View File
@@ -48,6 +48,9 @@ importers:
'@types/node':
specifier: ^25.9.3
version: 25.9.3
'@types/pg':
specifier: ^8.20.0
version: 8.20.0
'@vitest/coverage-v8':
specifier: ^4.1.9
version: 4.1.9(vitest@4.1.9)
@@ -63,6 +66,9 @@ importers:
npm-check-updates:
specifier: ^22.2.3
version: 22.2.3
pg:
specifier: ^8.21.0
version: 8.21.0
turbo:
specifier: ^2.9.18
version: 2.9.18
+66
View File
@@ -0,0 +1,66 @@
import type { APIRequestContext, Browser, BrowserContext, Page } from "@playwright/test";
import type { E2EAccount } from "./data";
async function assertAuthResponse(response: Awaited<ReturnType<APIRequestContext["post"]>>) {
if (response.ok()) return;
throw new Error(`Authentication request failed with ${response.status()}: ${await response.text()}`);
}
export async function registerViaUi(page: Page, account: E2EAccount) {
await page.goto("/auth/register");
await page.getByLabel("Name").fill(account.name);
await page.getByLabel("Username").fill(account.username);
await page.getByLabel("Email Address").fill(account.email);
await page.getByLabel("Password").fill(account.password);
await page.getByRole("button", { name: "Sign up" }).click();
await page.getByRole("link", { name: "Continue" }).click();
await page.waitForURL(/\/dashboard/);
}
export async function loginViaUi(page: Page, account: E2EAccount) {
await page.goto("/auth/login");
await page.getByLabel("Email Address").fill(account.email);
await page.getByLabel("Password").fill(account.password);
await page.getByRole("button", { name: "Sign in" }).click();
await page.waitForURL(/\/dashboard/);
}
export async function logoutViaUi(page: Page, account: E2EAccount) {
await page.getByText(account.email).click();
await page.getByRole("menuitem", { name: "Logout" }).click();
await page.goto("/auth/login");
}
export async function registerViaApi(request: APIRequestContext, account: E2EAccount, baseURL: string) {
const response = await request.post("/api/auth/sign-up/email", {
headers: {
origin: baseURL,
referer: `${baseURL}/auth/register`,
},
data: {
name: account.name,
email: account.email,
password: account.password,
username: account.username,
displayUsername: account.username,
callbackURL: "/dashboard",
},
});
await assertAuthResponse(response);
}
export async function createAuthenticatedContext(
browser: Browser,
request: APIRequestContext,
account: E2EAccount,
baseURL: string,
): Promise<BrowserContext> {
await registerViaApi(request, account, baseURL);
return browser.newContext({
baseURL,
storageState: await request.storageState(),
});
}
+37
View File
@@ -0,0 +1,37 @@
import type { TestInfo } from "@playwright/test";
const sanitize = (value: string) =>
value
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
export type E2EAccount = {
name: string;
username: string;
email: string;
password: string;
};
export function createRunSlug(testInfo: TestInfo) {
const worker = testInfo.workerIndex;
const title = sanitize(testInfo.titlePath.join("-")).slice(0, 32);
const suffix = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
return `e2e-${worker}-${title}-${suffix}`;
}
export function createAccount(testInfo: TestInfo): E2EAccount {
const username = createRunSlug(testInfo).replaceAll("-", "_").slice(0, 64);
return {
name: "E2E Test User",
username,
email: `${username}@example.test`,
password: "Password123!",
};
}
export function createResumeName(testInfo: TestInfo) {
return `E2E Resume ${createRunSlug(testInfo)}`;
}
+19
View File
@@ -0,0 +1,19 @@
import type { E2EAccount } from "./data";
import { Pool } from "pg";
function getDatabaseUrl() {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) throw new Error("DATABASE_URL is required for E2E cleanup.");
return databaseUrl;
}
export async function deleteE2EUser(account: E2EAccount) {
const pool = new Pool({ connectionString: getDatabaseUrl() });
try {
await pool.query('delete from "user" where email = $1 or username = $2', [account.email, account.username]);
} finally {
await pool.end();
}
}
+29
View File
@@ -0,0 +1,29 @@
import type { Page, TestInfo } from "@playwright/test";
import { expect } from "@playwright/test";
import { createResumeName } from "./data";
export async function createSampleResumeFromDashboard(page: Page, testInfo: TestInfo) {
const resumeName = createResumeName(testInfo);
await page.goto("/dashboard/resumes");
await page.getByText("Create a new resume").click();
const dialog = page.getByRole("dialog", { name: "Create a new resume" });
await dialog.getByLabel("Name").fill(resumeName);
const createGroup = dialog.getByRole("group", { name: "Create resume with options" });
await createGroup.getByRole("button").nth(1).click();
await page.getByRole("menuitem", { name: "Create a Sample Resume" }).click();
const resumeLink = page.getByRole("link", { name: new RegExp(resumeName) });
await expect(resumeLink).toBeVisible();
await resumeLink.click();
await page.waitForURL(/\/builder\/.+/);
return resumeName;
}
export async function openSidebarSection(page: Page, title: string) {
await page.getByTitle(title, { exact: true }).click();
await expect(page.getByRole("heading", { name: title })).toBeVisible();
}
+46
View File
@@ -0,0 +1,46 @@
import type { BrowserContext, Page } from "@playwright/test";
import type { E2EAccount } from "./data";
import { test as base, expect } from "@playwright/test";
import { createAuthenticatedContext } from "./auth";
import { createAccount } from "./data";
import { deleteE2EUser } from "./db";
type Fixtures = {
account: E2EAccount;
authContext: BrowserContext;
authPage: Page;
};
export const test = base.extend<Fixtures>({
account: async ({ baseURL }, use, testInfo) => {
void baseURL;
const account = createAccount(testInfo);
try {
await use(account);
} finally {
await deleteE2EUser(account);
}
},
authContext: async ({ browser, request, account }, use, testInfo) => {
const baseURL = String(testInfo.project.use.baseURL ?? "http://localhost:3000");
const context = await createAuthenticatedContext(browser, request, account, baseURL);
try {
await use(context);
} finally {
await context.close();
}
},
authPage: async ({ authContext }, use) => {
const page = await authContext.newPage();
try {
await use(page);
} finally {
await page.close();
}
},
});
export { expect };
+13
View File
@@ -0,0 +1,13 @@
import { loginViaUi, logoutViaUi, registerViaUi } from "../fixtures/auth";
import { expect, test } from "../fixtures/test";
test("registers and logs in with email credentials", async ({ page, account }) => {
await registerViaUi(page, account);
await expect(page.getByRole("heading", { name: "Resumes" })).toBeVisible();
await logoutViaUi(page, account);
await expect(page.getByRole("heading", { name: "Sign in to your account" })).toBeVisible();
await loginViaUi(page, account);
await expect(page.getByRole("heading", { name: "Resumes" })).toBeVisible();
});
@@ -0,0 +1,28 @@
import { createSampleResumeFromDashboard, openSidebarSection } from "../fixtures/resume";
import { expect, test } from "../fixtures/test";
test("exports and imports a resume JSON backup", async ({ authPage: page }, testInfo) => {
const resumeName = await createSampleResumeFromDashboard(page, testInfo);
await openSidebarSection(page, "Export");
const downloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: /^JSON/ }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toMatch(/\.json$/);
const downloadPath = await download.path();
if (!downloadPath) throw new Error("Expected Playwright to provide a downloaded JSON path.");
await page.goto("/dashboard/resumes");
await page.getByText("Import an existing resume").click();
await page.getByRole("combobox").click();
await page.getByRole("option", { name: "Reactive Resume (JSON)" }).click();
await page.locator('input[type="file"]').setInputFiles(downloadPath);
await page.getByRole("button", { name: "Import" }).click();
await page.waitForURL(/\/builder\/.+/);
await openSidebarSection(page, "Basics");
await expect(page.getByLabel("Name")).toHaveValue(/.+/);
await expect(page.getByText(resumeName)).toBeVisible();
});
+16
View File
@@ -0,0 +1,16 @@
import { createSampleResumeFromDashboard, openSidebarSection } from "../fixtures/resume";
import { expect, test } from "../fixtures/test";
test("publishes a resume and renders it for an anonymous visitor", async ({ browser, authPage: page }, testInfo) => {
await createSampleResumeFromDashboard(page, testInfo);
await openSidebarSection(page, "Sharing");
await page.getByLabel("Allow Public Access").click();
const publicUrl = await page.getByLabel("URL").inputValue();
expect(publicUrl).toMatch(/\/e2e_/);
const anonymous = await browser.newPage();
await anonymous.goto(publicUrl);
await expect(anonymous.getByRole("button", { name: "Download PDF" })).toBeVisible();
await anonymous.close();
});
+14
View File
@@ -0,0 +1,14 @@
import { createSampleResumeFromDashboard, openSidebarSection } from "../fixtures/resume";
import { expect, test } from "../fixtures/test";
test("creates a sample resume and persists a basics edit", async ({ authPage: page }, testInfo) => {
await createSampleResumeFromDashboard(page, testInfo);
const updatedName = `E2E Edited ${Date.now()}`;
await openSidebarSection(page, "Basics");
await page.getByLabel("Name").fill(updatedName);
await page.reload();
await openSidebarSection(page, "Basics");
await expect(page.getByLabel("Name")).toHaveValue(updatedName);
});