diff --git a/apps/web/e2e/analytics.spec.ts b/apps/web/e2e/analytics.spec.ts new file mode 100644 index 0000000..ae982d1 --- /dev/null +++ b/apps/web/e2e/analytics.spec.ts @@ -0,0 +1,78 @@ +import { expect, test, type Page } from "@playwright/test"; + +async function signIn(page: Page) { + await page.goto("/auth/signin"); + await page.fill('input[type="email"]', "admin@capakraken.dev"); + await page.fill('input[type="password"]', "admin123"); + await page.click('button[type="submit"]'); + await expect(page).toHaveURL(/\/(dashboard|resources)/); +} + +test.describe("Analytics / Skills Hub", () => { + test.beforeEach(async ({ page }) => { + await signIn(page); + await page.goto("/analytics/skills"); + }); + + test("skills hub page loads with heading", async ({ page }) => { + await page.waitForLoadState("networkidle"); + await expect(page.getByRole("heading", { name: "Skills Hub" })).toBeVisible({ timeout: 10000 }); + }); + + test("tab bar is visible with expected tabs", async ({ page }) => { + await page.waitForLoadState("networkidle"); + for (const label of ["Overview", "Search", "Gaps", "People"]) { + await expect(page.locator("button", { hasText: label })).toBeVisible({ timeout: 10000 }); + } + }); + + test("search tab renders a search form", async ({ page }) => { + await page.waitForLoadState("networkidle"); + await page.locator("button", { hasText: "Search" }).click(); + await page.waitForTimeout(300); + // Search tab should contain an input for skill / resource search + await expect( + page.locator('input[type="text"]').or(page.locator('input[type="search"]')).first(), + ).toBeVisible({ timeout: 10000 }); + }); + + test("people tab renders content", async ({ page }) => { + await page.waitForLoadState("networkidle"); + await page.locator("button", { hasText: "People" }).click(); + await page.waitForTimeout(300); + await expect( + page.locator("table").or(page.locator("text=No results")).or(page.locator("input").first()), + ).toBeVisible({ timeout: 10000 }); + }); + + test("no JavaScript errors on page load", async ({ page }) => { + const consoleErrors: string[] = []; + page.on("console", (msg) => { + if (msg.type() === "error") { + consoleErrors.push(msg.text()); + } + }); + await page.goto("/analytics/skills"); + await page.waitForLoadState("networkidle"); + // Filter out known third-party / browser-extension noise + const appErrors = consoleErrors.filter( + (e) => !e.includes("extension") && !e.includes("favicon"), + ); + expect(appErrors, `Unexpected console errors: ${appErrors.join("\n")}`).toHaveLength(0); + }); +}); + +test.describe("Analytics / Insights", () => { + test.beforeEach(async ({ page }) => { + await signIn(page); + await page.goto("/analytics/insights"); + }); + + test("insights page loads without errors", async ({ page }) => { + await page.waitForLoadState("networkidle"); + // Page should render some heading or content area — not a hard 404 + await expect( + page.locator("h1").or(page.locator("main")).first(), + ).toBeVisible({ timeout: 10000 }); + }); +}); diff --git a/apps/web/e2e/bench.spec.ts b/apps/web/e2e/bench.spec.ts new file mode 100644 index 0000000..2415dff --- /dev/null +++ b/apps/web/e2e/bench.spec.ts @@ -0,0 +1,64 @@ +import { expect, test, type Page } from "@playwright/test"; + +async function signIn(page: Page) { + await page.goto("/auth/signin"); + await page.fill('input[type="email"]', "admin@capakraken.dev"); + await page.fill('input[type="password"]', "admin123"); + await page.click('button[type="submit"]'); + await expect(page).toHaveURL(/\/(dashboard|resources)/); +} + +test.describe("Bench Board", () => { + test.beforeEach(async ({ page }) => { + await signIn(page); + await page.goto("/bench"); + }); + + test("bench board page loads with heading", async ({ page }) => { + await page.waitForLoadState("networkidle"); + await expect( + page.locator("h1", { hasText: "Bench Board" }), + ).toBeVisible({ timeout: 10000 }); + }); + + test("date range filter inputs are visible", async ({ page }) => { + await page.waitForLoadState("networkidle"); + const dateInputs = page.locator('input[type="date"]'); + await expect(dateInputs.first()).toBeVisible({ timeout: 10000 }); + // Should have at least a From and To date input + expect(await dateInputs.count()).toBeGreaterThanOrEqual(2); + }); + + test("shows bench results or no-resources empty state", async ({ page }) => { + await page.waitForLoadState("networkidle"); + await expect( + page.locator("table") + .or(page.locator("text=No resources on bench")) + .or(page.locator("text=No results")) + .first(), + ).toBeVisible({ timeout: 15000 }); + }); + + test("role filter input is present", async ({ page }) => { + await page.waitForLoadState("networkidle"); + // There should be a text input to filter by role/name + await expect( + page.locator('input[type="text"]').or(page.locator('input[placeholder*="role" i]')).first(), + ).toBeVisible({ timeout: 10000 }); + }); + + test("no console errors on load", async ({ page }) => { + const consoleErrors: string[] = []; + page.on("console", (msg) => { + if (msg.type() === "error") { + consoleErrors.push(msg.text()); + } + }); + await page.goto("/bench"); + await page.waitForLoadState("networkidle"); + const appErrors = consoleErrors.filter( + (e) => !e.includes("extension") && !e.includes("favicon"), + ); + expect(appErrors, `Unexpected console errors: ${appErrors.join("\n")}`).toHaveLength(0); + }); +}); diff --git a/apps/web/e2e/project-detail.spec.ts b/apps/web/e2e/project-detail.spec.ts new file mode 100644 index 0000000..8ed666d --- /dev/null +++ b/apps/web/e2e/project-detail.spec.ts @@ -0,0 +1,114 @@ +import { expect, test, type Page } from "@playwright/test"; + +async function signIn(page: Page) { + await page.goto("/auth/signin"); + await page.fill('input[type="email"]', "admin@capakraken.dev"); + await page.fill('input[type="password"]', "admin123"); + await page.click('button[type="submit"]'); + await expect(page).toHaveURL(/\/(dashboard|resources)/); +} + +/** + * Navigate to the projects list and return the href of the first project row. + * Returns null if the list is empty (seed data may not be present). + */ +async function getFirstProjectHref(page: Page): Promise { + await page.goto("/projects"); + await page.waitForLoadState("networkidle"); + + // Try to find a link inside the projects table that points to /projects/ + const projectLink = page + .locator("table a[href*='/projects/']") + .or(page.locator("a[href*='/projects/']").filter({ hasNot: page.locator("nav") })) + .first(); + + if ((await projectLink.count()) === 0) { + return null; + } + return projectLink.getAttribute("href"); +} + +test.describe("Project Detail Page", () => { + test.beforeEach(async ({ page }) => { + await signIn(page); + }); + + test("project detail page loads from list link", async ({ page }) => { + const href = await getFirstProjectHref(page); + if (!href) { + test.skip(true, "No seeded projects available to navigate to"); + return; + } + + await page.goto(href); + await page.waitForLoadState("networkidle"); + + // Project detail renders an h1 with the project name + await expect(page.locator("h1").first()).toBeVisible({ timeout: 10000 }); + }); + + test("project detail shows assignments and demands sections", async ({ page }) => { + const href = await getFirstProjectHref(page); + if (!href) { + test.skip(true, "No seeded projects available to navigate to"); + return; + } + + await page.goto(href); + await page.waitForLoadState("networkidle"); + + // The detail page should contain stat labels for assignments and open demands + await expect( + page.locator("text=Assignments").or(page.locator("text=Open Demands")).first(), + ).toBeVisible({ timeout: 10000 }); + }); + + test("project detail shows budget status card", async ({ page }) => { + const href = await getFirstProjectHref(page); + if (!href) { + test.skip(true, "No seeded projects available to navigate to"); + return; + } + + await page.goto(href); + await page.waitForLoadState("networkidle"); + + // BudgetStatusCard renders budget-related content + await expect( + page.locator("text=Budget").or(page.locator("text=budget")).first(), + ).toBeVisible({ timeout: 10000 }); + }); + + test("unknown project id shows not-found state", async ({ page }) => { + await page.goto("/projects/does-not-exist-abc123"); + await page.waitForLoadState("networkidle"); + + // Server-side notFound() triggers the Next.js 404 page + await expect( + page.locator("text=404").or(page.locator("text=Not Found")).or(page.locator("text=not found")).first(), + ).toBeVisible({ timeout: 10000 }); + }); + + test("no console errors when viewing first project", async ({ page }) => { + const consoleErrors: string[] = []; + page.on("console", (msg) => { + if (msg.type() === "error") { + consoleErrors.push(msg.text()); + } + }); + + const href = await getFirstProjectHref(page); + if (!href) { + test.skip(true, "No seeded projects available to navigate to"); + return; + } + + await page.goto(href); + await page.waitForLoadState("networkidle"); + + const appErrors = consoleErrors.filter( + (e) => !e.includes("extension") && !e.includes("favicon"), + ); + expect(appErrors, `Unexpected console errors: ${appErrors.join("\n")}`).toHaveLength(0); + }); +}); diff --git a/apps/web/e2e/reports.spec.ts b/apps/web/e2e/reports.spec.ts new file mode 100644 index 0000000..255fc84 --- /dev/null +++ b/apps/web/e2e/reports.spec.ts @@ -0,0 +1,97 @@ +import { expect, test, type Page } from "@playwright/test"; + +async function signIn(page: Page) { + await page.goto("/auth/signin"); + await page.fill('input[type="email"]', "admin@capakraken.dev"); + await page.fill('input[type="password"]', "admin123"); + await page.click('button[type="submit"]'); + await expect(page).toHaveURL(/\/(dashboard|resources)/); +} + +test.describe("Chargeability Report", () => { + test.beforeEach(async ({ page }) => { + await signIn(page); + await page.goto("/reports/chargeability"); + }); + + test("chargeability forecast page loads with heading", async ({ page }) => { + await page.waitForLoadState("networkidle"); + await expect( + page.locator("h1", { hasText: "Chargeability Forecast" }), + ).toBeVisible({ timeout: 10000 }); + }); + + test("filter controls are present", async ({ page }) => { + await page.waitForLoadState("networkidle"); + // Should have at least one filter (e.g., chapter, period, resource search) + await expect( + page.locator('input[type="text"]') + .or(page.locator('input[type="search"]')) + .or(page.locator("select")) + .first(), + ).toBeVisible({ timeout: 10000 }); + }); + + test("data table or chart renders after load", async ({ page }) => { + await page.waitForLoadState("networkidle"); + // Either a table with chargeability rows or a chart container should appear + await expect( + page.locator("table").or(page.locator("[data-testid]")).or(page.locator("canvas")).first(), + ).toBeVisible({ timeout: 15000 }); + }); + + test("no console errors on load", async ({ page }) => { + const consoleErrors: string[] = []; + page.on("console", (msg) => { + if (msg.type() === "error") { + consoleErrors.push(msg.text()); + } + }); + await page.goto("/reports/chargeability"); + await page.waitForLoadState("networkidle"); + const appErrors = consoleErrors.filter( + (e) => !e.includes("extension") && !e.includes("favicon"), + ); + expect(appErrors, `Unexpected console errors: ${appErrors.join("\n")}`).toHaveLength(0); + }); +}); + +test.describe("Report Builder", () => { + test.beforeEach(async ({ page }) => { + await signIn(page); + await page.goto("/reports/builder"); + }); + + test("report builder page loads with heading", async ({ page }) => { + await page.waitForLoadState("networkidle"); + await expect( + page.getByRole("heading", { name: "Report Builder" }), + ).toBeVisible({ timeout: 10000 }); + }); + + test("entity selector is present with expected options", async ({ page }) => { + await page.waitForLoadState("networkidle"); + // The builder has a Template or Entity select + const select = page.locator("select").first(); + await expect(select).toBeVisible({ timeout: 10000 }); + }); + + test("run report button is visible", async ({ page }) => { + await page.waitForLoadState("networkidle"); + await expect( + page.locator("button", { hasText: /Run|Export|Generate/i }).first(), + ).toBeVisible({ timeout: 10000 }); + }); + + test("running a default report produces output or empty state", async ({ page }) => { + await page.waitForLoadState("networkidle"); + const runBtn = page.locator("button", { hasText: /Run|Export|Generate/i }).first(); + if ((await runBtn.count()) > 0) { + await runBtn.click(); + await page.waitForTimeout(1500); + await expect( + page.locator("table").or(page.locator("text=No rows")).or(page.locator("text=0 rows")).first(), + ).toBeVisible({ timeout: 15000 }); + } + }); +}); diff --git a/apps/web/e2e/scenarios.spec.ts b/apps/web/e2e/scenarios.spec.ts new file mode 100644 index 0000000..4d34026 --- /dev/null +++ b/apps/web/e2e/scenarios.spec.ts @@ -0,0 +1,56 @@ +import { expect, test, type Page } from "@playwright/test"; + +async function signIn(page: Page) { + await page.goto("/auth/signin"); + await page.fill('input[type="email"]', "admin@capakraken.dev"); + await page.fill('input[type="password"]', "admin123"); + await page.click('button[type="submit"]'); + await expect(page).toHaveURL(/\/(dashboard|resources)/); +} + +test.describe("Scenario Planning", () => { + test.beforeEach(async ({ page }) => { + await signIn(page); + await page.goto("/scenarios"); + }); + + test("scenarios page loads with heading", async ({ page }) => { + await page.waitForLoadState("networkidle"); + await expect( + page.locator("h1", { hasText: /Scenario Planning/i }), + ).toBeVisible({ timeout: 10000 }); + }); + + test("shows scenarios list or empty state", async ({ page }) => { + await page.waitForLoadState("networkidle"); + await expect( + page.locator("table") + .or(page.locator("text=No scenarios")) + .or(page.locator("text=Create a project first")) + .or(page.locator("[data-testid]")) + .first(), + ).toBeVisible({ timeout: 10000 }); + }); + + test("page subtitle describes what-if staffing scenarios", async ({ page }) => { + await page.waitForLoadState("networkidle"); + await expect( + page.locator("text=what-if").or(page.locator("text=staffing scenarios")).first(), + ).toBeVisible({ timeout: 10000 }); + }); + + test("no console errors on load", async ({ page }) => { + const consoleErrors: string[] = []; + page.on("console", (msg) => { + if (msg.type() === "error") { + consoleErrors.push(msg.text()); + } + }); + await page.goto("/scenarios"); + await page.waitForLoadState("networkidle"); + const appErrors = consoleErrors.filter( + (e) => !e.includes("extension") && !e.includes("favicon"), + ); + expect(appErrors, `Unexpected console errors: ${appErrors.join("\n")}`).toHaveLength(0); + }); +});