Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 201 additions & 0 deletions Clients/e2e/assessment.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { test, expect } from "./fixtures/auth.fixture";
import AxeBuilder from "@axe-core/playwright";

test.describe("Assessment Tracker", () => {
test.beforeEach(async ({ authedPage: page }) => {
// Dismiss tours to avoid UI interference
await page.evaluate(() => {
localStorage.setItem("home-tour", "true");
localStorage.setItem("compliance-tour", "true");
localStorage.setItem("assessment-tour", "true");
localStorage.setItem("projectFrameworks-tour", "true");
});
});

// --- Tier 0: Navigate to assessment via project view ---

test("can reach the frameworks tab in project view", async ({
authedPage: page,
}) => {
await page.goto("/project-view?tab=frameworks");
await expect(page).toHaveURL(/\/project-view/);

// Should show project content or framework-related elements
await expect(
page
.getByText(/framework/i)
.or(page.getByText(/regulation/i))
.or(page.getByText(/control/i))
.or(page.getByText(/assessment/i))
.or(page.getByRole("heading"))
.first()
).toBeVisible({ timeout: 15_000 });
});

test("frameworks tab has Controls and Assessments toggle", async ({
authedPage: page,
}) => {
await page.goto("/project-view?tab=frameworks");

// Look for the Controls / Assessments toggle buttons
const controlsBtn = page
.getByRole("button", { name: /controls/i })
.or(page.getByText(/controls/i));
const assessmentsBtn = page
.getByRole("button", { name: /assessments/i })
.or(page.getByText(/assessments/i));

if (
!(await controlsBtn.first().isVisible({ timeout: 10_000 }).catch(() => false))
) {
test.skip();
return;
}

await expect(controlsBtn.first()).toBeVisible();
await expect(assessmentsBtn.first()).toBeVisible();
});

// --- Tier 1: Assessment view content ---

test("switching to Assessments tab shows assessment content", async ({
authedPage: page,
}) => {
await page.goto("/project-view?tab=frameworks");

const assessmentsBtn = page
.getByRole("button", { name: /assessments/i })
.or(page.getByText(/assessments/i));

if (
!(await assessmentsBtn.first().isVisible({ timeout: 10_000 }).catch(() => false))
) {
test.skip();
return;
}

await assessmentsBtn.first().click();
await page.waitForTimeout(1000);

// Assessment view should show topic list or progress stats
const assessmentContent = page
.getByText(/assessment/i)
.or(page.getByText(/questions/i))
.or(page.getByText(/high risk/i))
.or(page.getByText(/conformity/i))
.or(page.getByRole("list"));

await expect(assessmentContent.first()).toBeVisible({ timeout: 10_000 });
});

test("assessment shows progress stats", async ({ authedPage: page }) => {
await page.goto("/project-view?tab=frameworks");

const assessmentsBtn = page
.getByRole("button", { name: /assessments/i })
.or(page.getByText(/assessments/i));

if (
!(await assessmentsBtn.first().isVisible({ timeout: 10_000 }).catch(() => false))
) {
test.skip();
return;
}

await assessmentsBtn.first().click();
await page.waitForTimeout(1000);

// Should display progress information (answered/total questions)
const progressContent = page
.getByText(/questions/i)
.or(page.getByText(/answered/i))
.or(page.getByText(/completed/i))
.or(page.getByText(/progress/i))
.or(page.locator('[class*="stats" i]'))
.or(page.locator('[class*="progress" i]'));

if (await progressContent.first().isVisible().catch(() => false)) {
await expect(progressContent.first()).toBeVisible();
}
});

// --- Tier 2: Topic navigation ---

test("clicking a topic loads subtopics", async ({ authedPage: page }) => {
await page.goto("/project-view?tab=frameworks");

const assessmentsBtn = page
.getByRole("button", { name: /assessments/i })
.or(page.getByText(/assessments/i));

if (
!(await assessmentsBtn.first().isVisible({ timeout: 10_000 }).catch(() => false))
) {
test.skip();
return;
}

await assessmentsBtn.first().click();
await page.waitForTimeout(1000);

// Look for topic list items in the sidebar
const topicItems = page
.getByRole("listitem")
.or(page.locator('[class*="topic" i]'))
.or(page.locator(".MuiListItem-root"));

if (await topicItems.first().isVisible({ timeout: 5_000 }).catch(() => false)) {
await topicItems.first().click();
await page.waitForTimeout(1000);

// After clicking a topic, subtopics or an accordion should appear
const subtopicContent = page
.locator('[class*="accordion" i]')
.or(page.locator(".MuiAccordion-root"))
.or(page.getByRole("button", { name: /expand/i }))
.or(page.locator('[class*="subtopic" i]'));

if (await subtopicContent.first().isVisible().catch(() => false)) {
await expect(subtopicContent.first()).toBeVisible();
}
}
});

// --- Accessibility ---

test("assessment view has no accessibility violations", async ({
authedPage: page,
}) => {
await page.goto("/project-view?tab=frameworks");
await page.waitForLoadState("domcontentloaded");

// Switch to assessments tab if visible
const assessmentsBtn = page
.getByRole("button", { name: /assessments/i })
.or(page.getByText(/assessments/i));

if (await assessmentsBtn.first().isVisible({ timeout: 10_000 }).catch(() => false)) {
await assessmentsBtn.first().click();
await page.waitForTimeout(1000);
}

const results = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
.disableRules([
"button-name",
"link-name",
"color-contrast",
"aria-command-name",
"aria-valid-attr-value",
"aria-input-field-name",
"label",
"select-name",
"scrollable-region-focusable",
"aria-progressbar-name",
"aria-prohibited-attr",
"nested-interactive",
])
.analyze();
expect(results.violations).toEqual([]);
});
});
23 changes: 13 additions & 10 deletions Clients/e2e/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@ test.describe("Authentication", () => {
await expect(page).toHaveURL(/\/forgot-password/);
});

test("register link navigates correctly", async ({ page }) => {
await page.goto("/login");
await page.getByText("Register here").click();
await expect(page).toHaveURL(/\/register/);
test("register route redirects to login", async ({ page }) => {
// /register now redirects to /login in single-tenant mode
await page.goto("/register");
await expect(page).toHaveURL(/\/login/, { timeout: 10_000 });
});

test("login page has no accessibility violations", async ({ page }) => {
Expand Down Expand Up @@ -95,12 +95,14 @@ test.describe("Authentication", () => {
}

// The sidebar footer has a MoreVertical icon button (no accessible name)
// next to the user name/role. Navigate from "Admin" text up to the
// user footer container and find the button sibling.
// next to the user name/role. Find the IconButton sibling of the
// user-info stack containing the "Admin" role label.
const adminLabel = page.getByText("Admin", { exact: true });
await expect(adminLabel.first()).toBeVisible({ timeout: 10_000 });
// Admin text → parent (name+role container) → grandparent (user footer) → button
const moreBtn = adminLabel.first().locator("xpath=../../button");
// Walk up to the sidebar footer row and find the sibling button.
const moreBtn = adminLabel
.first()
.locator("xpath=ancestor::*[.//button][1]//button");
await expect(moreBtn.first()).toBeVisible({ timeout: 5_000 });
await moreBtn.first().click();
await page.waitForTimeout(1000);
Expand All @@ -117,7 +119,8 @@ test.describe("Authentication", () => {
// --- Registration form ---

test("registration page renders form fields", async ({ page }) => {
await page.goto("/register");
// /register redirects to /login; the user registration form lives at /user-reg
await page.goto("/user-reg");
await page.waitForLoadState("domcontentloaded");

// Verify registration form fields are present
Expand All @@ -137,7 +140,7 @@ test.describe("Authentication", () => {
test("registration form shows validation on empty submit", async ({
page,
}) => {
await page.goto("/register");
await page.goto("/user-reg");
await page.waitForLoadState("domcontentloaded");

// The "Get started" button should be present
Expand Down
Loading
Loading