diff --git a/tests/e2e/fixtures/resume.ts b/tests/e2e/fixtures/resume.ts index f3a2af568..eae64b063 100644 --- a/tests/e2e/fixtures/resume.ts +++ b/tests/e2e/fixtures/resume.ts @@ -27,3 +27,11 @@ export async function openSidebarSection(page: Page, title: string) { await page.getByTitle(title, { exact: true }).click(); await expect(page.getByRole("heading", { name: title, exact: true })).toBeVisible(); } + +export async function openResumeCardMenu(page: Page, resumeName: string) { + await page.goto("/dashboard/resumes"); + const resumeLink = page.getByRole("link", { name: new RegExp(resumeName) }); + await expect(resumeLink).toBeVisible(); + await resumeLink.click({ button: "right" }); + await expect(page.getByRole("menuitem", { name: "Open" })).toBeVisible(); +} diff --git a/tests/e2e/specs/dashboard-lifecycle.spec.ts b/tests/e2e/specs/dashboard-lifecycle.spec.ts new file mode 100644 index 000000000..96aa371bf --- /dev/null +++ b/tests/e2e/specs/dashboard-lifecycle.spec.ts @@ -0,0 +1,30 @@ +import { createSampleResumeFromDashboard, openResumeCardMenu } from "../fixtures/resume"; +import { expect, test } from "../fixtures/test"; + +test("renames, duplicates and deletes a resume from the dashboard", async ({ authPage: page }, testInfo) => { + const resumeName = await createSampleResumeFromDashboard(page, testInfo); + + // Rename via the card context menu (resume names are capped at 64 chars, keep it short) + const renamedTo = `E2E Renamed ${Date.now().toString(36)}`; + await openResumeCardMenu(page, resumeName); + await page.getByRole("menuitem", { name: "Update" }).click(); + const updateDialog = page.getByRole("dialog", { name: "Update Resume" }); + await updateDialog.getByLabel("Name").fill(renamedTo); + await updateDialog.getByRole("button", { name: "Save Changes" }).click(); + await expect(page.getByRole("link", { name: new RegExp(renamedTo) })).toBeVisible(); + + // Duplicate — the copy defaults to " (Copy)" + await openResumeCardMenu(page, renamedTo); + await page.getByRole("menuitem", { name: "Duplicate" }).click(); + const duplicateDialog = page.getByRole("dialog", { name: "Duplicate Resume" }); + await duplicateDialog.getByRole("button", { name: "Duplicate" }).click(); + const copyLink = page.getByRole("link", { name: new RegExp(`${renamedTo} \\(Copy\\)`) }); + await expect(copyLink).toBeVisible(); + + // Delete the copy and verify it disappears while the original stays + await openResumeCardMenu(page, `${renamedTo} \\(Copy\\)`); + await page.getByRole("menuitem", { name: "Delete" }).click(); + await page.getByRole("alertdialog").getByRole("button", { name: "Confirm" }).click(); + await expect(copyLink).toBeHidden(); + await expect(page.getByRole("link", { name: new RegExp(renamedTo) })).toBeVisible(); +}); diff --git a/tests/e2e/specs/lock-resume.spec.ts b/tests/e2e/specs/lock-resume.spec.ts new file mode 100644 index 000000000..a7f4c5bb4 --- /dev/null +++ b/tests/e2e/specs/lock-resume.spec.ts @@ -0,0 +1,34 @@ +import { createSampleResumeFromDashboard, openResumeCardMenu } from "../fixtures/resume"; +import { expect, test } from "../fixtures/test"; + +test("locks a resume, blocking updates and deletion until unlocked", async ({ authPage: page }, testInfo) => { + const resumeName = await createSampleResumeFromDashboard(page, testInfo); + + await openResumeCardMenu(page, resumeName); + const lockPromise = page.waitForResponse((response) => { + if (!response.url().includes("/api/rpc")) return false; + if (!response.ok()) return false; + return (response.request().postData() ?? "").includes('"isLocked":true'); + }); + await page.getByRole("menuitem", { name: "Lock" }).click(); + await page.getByRole("alertdialog").getByRole("button", { name: "Confirm" }).click(); + await lockPromise; + + // Locked: the menu now offers Unlock, and destructive/edit actions are disabled + await openResumeCardMenu(page, resumeName); + await expect(page.getByRole("menuitem", { name: "Unlock" })).toBeVisible(); + await expect(page.getByRole("menuitem", { name: "Update" })).toBeDisabled(); + await expect(page.getByRole("menuitem", { name: "Delete" })).toBeDisabled(); + + // Unlock restores the actions — wait for the mutation to land before re-reading the menu + const unlockPromise = page.waitForResponse((response) => { + if (!response.url().includes("/api/rpc")) return false; + if (!response.ok()) return false; + return (response.request().postData() ?? "").includes('"isLocked":false'); + }); + await page.getByRole("menuitem", { name: "Unlock" }).click(); + await unlockPromise; + await openResumeCardMenu(page, resumeName); + await expect(page.getByRole("menuitem", { name: "Lock" })).toBeVisible(); + await expect(page.getByRole("menuitem", { name: "Delete" })).toBeEnabled(); +}); diff --git a/tests/e2e/specs/section-editing.spec.ts b/tests/e2e/specs/section-editing.spec.ts new file mode 100644 index 000000000..293964d57 --- /dev/null +++ b/tests/e2e/specs/section-editing.spec.ts @@ -0,0 +1,34 @@ +import { createSampleResumeFromDashboard, openSidebarSection } from "../fixtures/resume"; +import { expect, test } from "../fixtures/test"; + +test("adds an experience item and persists it across reloads", async ({ authPage: page }, testInfo) => { + await createSampleResumeFromDashboard(page, testInfo); + + const company = `E2E Corp ${Date.now()}`; + const position = "Principal Tester"; + + await openSidebarSection(page, "Experience"); + await page.getByRole("button", { name: "Add a new experience" }).click(); + + const dialog = page.getByRole("dialog", { name: "Create a new experience" }); + await dialog.getByLabel("Company").fill(company); + await dialog.getByLabel("Position").fill(position); + + const savePromise = page.waitForResponse((response) => { + if (!response.url().includes("/api/rpc")) return false; + if (response.request().method() !== "POST") return false; + if (!response.ok()) return false; + return (response.request().postData() ?? "").includes(company); + }); + await dialog.getByRole("button", { name: "Create" }).click(); + await savePromise; + + // The new item shows up in the section list with the company as its title + await expect(page.getByText(company)).toBeVisible(); + + // And it survives a full reload + await page.reload(); + await openSidebarSection(page, "Experience"); + await expect(page.getByText(company)).toBeVisible(); + await expect(page.getByText(position)).toBeVisible(); +}); diff --git a/tests/e2e/specs/settings-profile.spec.ts b/tests/e2e/specs/settings-profile.spec.ts new file mode 100644 index 000000000..d70dc25bb --- /dev/null +++ b/tests/e2e/specs/settings-profile.spec.ts @@ -0,0 +1,15 @@ +import { expect, test } from "../fixtures/test"; + +test("updates the profile name from settings and persists it", async ({ authPage: page }) => { + await page.goto("/dashboard/settings/profile"); + + const updatedName = `E2E Renamed ${Date.now()}`; + const nameField = page.getByLabel("Name", { exact: true }); + await expect(nameField).toBeVisible(); + await nameField.fill(updatedName); + await page.getByRole("button", { name: "Save Changes" }).click(); + + // The save round-trips through the API; a reload must show the new value + await page.reload(); + await expect(page.getByLabel("Name", { exact: true })).toHaveValue(updatedName); +}); diff --git a/tests/e2e/specs/sharing-password.spec.ts b/tests/e2e/specs/sharing-password.spec.ts new file mode 100644 index 000000000..983f9e7a8 --- /dev/null +++ b/tests/e2e/specs/sharing-password.spec.ts @@ -0,0 +1,34 @@ +import { createSampleResumeFromDashboard, openSidebarSection } from "../fixtures/resume"; +import { expect, test } from "../fixtures/test"; + +test("password-protects a public resume and unlocks it as a visitor", async ({ browser, authPage: page }, testInfo) => { + await createSampleResumeFromDashboard(page, testInfo); + await openSidebarSection(page, "Sharing"); + + await page.getByRole("switch", { name: /Allow Public Access/ }).click(); + const sharingUrl = page.locator("#sharing-url"); + await expect(sharingUrl).toHaveValue(/\/e2e_/); + const publicUrl = await sharingUrl.inputValue(); + + const password = "e2e-secret-42"; + await page.getByRole("button", { name: "Set Password" }).click(); + const prompt = page.getByRole("alertdialog"); + await prompt.locator('input[type="password"]').fill(password); + await prompt.getByRole("button", { name: "Set Password" }).click(); + await expect(page.getByRole("button", { name: "Remove Password" })).toBeVisible(); + + const anonymous = await browser.newPage(); + try { + // Anonymous visitors hit the password gate first + await anonymous.goto(publicUrl); + await anonymous.waitForURL(/\/auth\/resume-password/); + + await anonymous.getByLabel("Password", { exact: true }).fill(password); + await anonymous.getByRole("button", { name: "Unlock" }).click(); + + // The correct password reveals the actual resume + await expect(anonymous.getByRole("button", { name: "Download PDF" })).toBeVisible(); + } finally { + await anonymous.close(); + } +}); diff --git a/tests/e2e/specs/template-switch.spec.ts b/tests/e2e/specs/template-switch.spec.ts new file mode 100644 index 000000000..4c080218e --- /dev/null +++ b/tests/e2e/specs/template-switch.spec.ts @@ -0,0 +1,27 @@ +import { createSampleResumeFromDashboard, openSidebarSection } from "../fixtures/resume"; +import { expect, test } from "../fixtures/test"; + +test("switches the resume template and persists the choice", async ({ authPage: page }, testInfo) => { + await createSampleResumeFromDashboard(page, testInfo); + + await openSidebarSection(page, "Template"); + // Sample resumes start on Azurill; its preview button opens the gallery + await page.getByRole("button", { name: "Azurill", exact: true }).click(); + const gallery = page.getByRole("dialog", { name: "Template Gallery" }); + await expect(gallery).toBeVisible(); + + const savePromise = page.waitForResponse((response) => { + if (!response.url().includes("/api/rpc")) return false; + if (response.request().method() !== "POST") return false; + if (!response.ok()) return false; + return (response.request().postData() ?? "").includes("bronzor"); + }); + await gallery.getByRole("img", { name: "Bronzor", exact: true }).click(); + await savePromise; + await page.keyboard.press("Escape"); + + // After a reload the Template section previews the newly selected template + await page.reload(); + await openSidebarSection(page, "Template"); + await expect(page.getByRole("img", { name: "Bronzor", exact: true })).toBeVisible(); +});