Skip to content
Open
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
7 changes: 6 additions & 1 deletion src/pages/UserDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,12 @@ export default function UserDashboard() {
) : (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="w-auto">
<Button
variant="outline"
size="sm"
className="w-auto"
aria-label={t("more_options")}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine, an aria-label on an icon-only button. Miraculously correct. Shame it is bolted onto a tablist twelve lines below that still hardcodes aria-label="Dashboard Sections" in English while everything else goes through t() — and the tests now depend on that untranslated string. If you are fixing a11y here, fix it consistently.

>
<CareIcon icon="l-ellipsis-v" className="text-inherit" />
</Button>
</DropdownMenuTrigger>
Expand Down
137 changes: 137 additions & 0 deletions tests/dashboard/userDashboard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { expect, test } from "@playwright/test";

/**
* User Dashboard E2E Tests
*
* Covers the authenticated landing page (`/`) for the admin fixture user
* (tests/.auth/user.json — a superuser with assigned facilities and a
* government organization). Asserts the deterministic state this user always
* renders: greeting + date, the superuser admin-dashboard link, the profile
* menu, and the Facilities / Governance tablist with switching and navigation.
Comment on lines +6 to +10
*/

test.use({ storageState: "tests/.auth/user.json" });

test.describe("User Dashboard", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
});

test("loads with greeting, date, and admin dashboard link", async ({
page,
}) => {
await test.step("Verify greeting and date", async () => {
await expect(page).toHaveTitle(/CARE/);
await expect(
page.getByRole("heading", { name: /^Hey .+$/ }),
).toBeVisible();
await expect(
page.getByText(
/Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday/,

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An unscoped getByText(/Monday|Tuesday|.../) across the whole page. Any facility, org, or future widget containing the substring "Sunday" makes this a strict-mode violation, and it silently passes even if the greeting date block vanishes. Scope it to the date paragraph and assert the actual formatted date.

),
).toBeVisible();
});

await test.step("Verify superuser admin dashboard link", async () => {
const adminLink = page.getByRole("link", { name: /admin dashboard/i });
await expect(adminLink).toBeVisible();
await expect(adminLink).toHaveAttribute("href", "/admin/questionnaire");
});
});

test("profile menu exposes edit profile and sign out", async ({ page }) => {
await test.step("Open the profile dropdown", async () => {
await page.getByRole("button", { name: /more options/i }).click();
});

await test.step("Verify menu items", async () => {
await expect(

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test only exists on desktop: isMobile renders two plain buttons instead of the dropdown, so on any mobile project in the Playwright config this fails outright rather than skipping. Either pin a viewport or branch on it — do not just hope the default project stays wide.

page.getByRole("menuitem", { name: /edit profile/i }),
).toBeVisible();
await expect(
page.getByRole("menuitem", { name: /sign out/i }),
).toBeVisible();
});
});

test("renders Facilities and Governance tabs with Facilities active", async ({
page,
}) => {
const tablist = page.getByRole("tablist", { name: /dashboard sections/i });

await test.step("Verify tabs are present", async () => {
await expect(tablist).toBeVisible();
await expect(
tablist.getByRole("tab", { name: /facilities/i }),
).toBeVisible();
await expect(
tablist.getByRole("tab", { name: /governance/i }),
).toBeVisible();
});

await test.step("Verify Facilities tab is selected by default", async () => {
await expect(
tablist.getByRole("tab", { name: /facilities/i }),
).toHaveAttribute("aria-selected", "true");
});
});

test("switching to Governance updates the selected tab and panel", async ({
page,
}) => {
const tablist = page.getByRole("tablist", { name: /dashboard sections/i });
const facilitiesTab = tablist.getByRole("tab", { name: /facilities/i });
const governanceTab = tablist.getByRole("tab", { name: /governance/i });

await test.step("Select the Governance tab", async () => {
await governanceTab.click();
await expect(governanceTab).toHaveAttribute("aria-selected", "true");
await expect(facilitiesTab).toHaveAttribute("aria-selected", "false");
});

await test.step("Verify the Governance panel is shown", async () => {
await expect(page.getByRole("tabpanel")).toBeVisible();
});

await test.step("Switch back to Facilities", async () => {
await facilitiesTab.click();
await expect(facilitiesTab).toHaveAttribute("aria-selected", "true");
await expect(governanceTab).toHaveAttribute("aria-selected", "false");
});
});

test("facility cards link to and navigate to facility overview", async ({

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getByRole("tabpanel").getByRole("link").first() assumes the first available tab is Facilities. availableTabs is data-driven: if the fixture user loses facilities but keeps responsibilities, this happily clicks an org card and the href assertion fails with a useless message. Click the Facilities tab explicitly first.

page,
}) => {
const panel = page.getByRole("tabpanel");
const facilityCard = panel.getByRole("link").first();

await test.step("Verify facility card links to an overview page", async () => {
await expect(facilityCard).toBeVisible();
await expect(facilityCard).toHaveAttribute(
"href",
/^\/facility\/[^/]+\/overview$/,
);
});

await test.step("Navigate to the facility overview", async () => {
await facilityCard.click();
await expect(page).toHaveURL(/\/facility\/[^/]+\/overview$/);
});
});

test("tablist and tabs expose the expected ARIA wiring", async ({ page }) => {
const tablist = page.getByRole("tablist", { name: /dashboard sections/i });

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Asserting aria-controls matches /.+/ and then never checking it points at the visible panel is testing that a string exists. Meanwhile the component gives the tabpanel aria-labelledby={tabId} — the panel's own id, not the tab's — which is broken ARIA this test cheerfully fails to catch. Assert the panel's id equals the tab's aria-controls.

const facilitiesTab = tablist.getByRole("tab", { name: /facilities/i });

await test.step("Verify tab ARIA attributes", async () => {
await expect(facilitiesTab).toHaveAttribute("aria-selected", "true");
await expect(facilitiesTab).toHaveAttribute("id", /.+/);
await expect(facilitiesTab).toHaveAttribute("aria-controls", /.+/);
});

await test.step("Verify the active panel is a tabpanel", async () => {
await expect(page.getByRole("tabpanel")).toBeVisible();
});
Comment on lines +127 to +135
});
});
Loading