Skip to content
Draft
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
211 changes: 211 additions & 0 deletions tests/facility/appointments/appointmentsList.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { expect, test } from "@playwright/test";
import { format, addDays } from "date-fns";

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.

format and addDays are imported and never used. Forty years in and I still can't get people to read their own lint output. Delete the import before CI does it for you.

import { getFacilityId } from "tests/support/facilityId";

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

test.describe("Appointments List Page", () => {
let facilityId: string;

test.beforeEach(async ({ page }) => {
facilityId = getFacilityId();
await page.goto(`/facility/${facilityId}/appointments`);
});

/**
* Verifies the appointments page loads successfully and displays the main UI elements
*/
test("should load appointments page and display key elements", async ({
page,
}) => {
// Wait for page to load completely
await page.waitForLoadState("networkidle");

// Verify page heading is visible
await expect(
page.getByRole("heading", { name: /appointments/i }),
).toBeVisible();

// Verify view tabs are present (board view and list view)
await expect(page.getByRole("tab", { name: /board/i })).toBeVisible();
await expect(page.getByRole("tab", { name: /list/i })).toBeVisible();

// Verify filter button is visible
await expect(
page.getByRole("button", { name: /filter/i }),
).toBeVisible();
});

/**
* Tests switching between board and list views
*/
test("should toggle between board and list views", async ({ page }) => {
await page.waitForLoadState("networkidle");

// Start with board view (default)
const boardTab = page.getByRole("tab", { name: /board/i });
const listTab = page.getByRole("tab", { name: /list/i });

// Click list view
await listTab.click();
await expect(listTab).toHaveAttribute("data-state", "active");

// Verify table structure is visible in list view
await expect(page.getByRole("table")).toBeVisible();

// Switch back to board view
await boardTab.click();
await expect(boardTab).toHaveAttribute("data-state", "active");
});

/**
* Tests the date filter functionality for appointments
*/
test("should filter appointments by date range", async ({ page }) => {
await page.waitForLoadState("networkidle");

// Open filter menu
const filterButton = page.getByRole("button", { name: /filter/i });
await filterButton.click();

// Wait for filter popover to be visible
const dateFilterSection = page.getByText(/date/i).first();
await expect(dateFilterSection).toBeVisible();

// Select a date range option (e.g., "Today")
const todayOption = page.getByRole("button", { name: /^today$/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.

if (await todayOption.isVisible()) and then asserting url contains date_from anyway? If the button isn't there, this either flakes or asserts on state you never created. Either the filter exists and you assert it, or you don't test it. Conditional assertions are how tests quietly stop testing anything.

if (await todayOption.isVisible()) {
await todayOption.click();
}

// Verify filter is applied by checking URL parameters
await page.waitForTimeout(500);
const url = page.url();
expect(url).toContain("date_from");
});

/**
* Tests the practitioner filter functionality
*/
test("should show practitioner filter options", async ({ page }) => {
await page.waitForLoadState("networkidle");

// Look for practitioner filter/selector

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 entire test is if (visible) expect(visible). It is structurally incapable of failing. That's not a test, that's a 10-line no-op with a docblock. Delete it or assert the practitioner filter actually exists.

// This might be a dropdown or multi-select component
const practitionerFilter = page.getByText(/practitioner/i).first();

// If the filter exists, verify it's visible
if (await practitionerFilter.isVisible()) {
await expect(practitionerFilter).toBeVisible();
}
});

/**
* Tests appointment status filtering
*/
test("should display appointment status filters", async ({ page }) => {
await page.waitForLoadState("networkidle");

// Look for status filter buttons/badges
// Common appointment statuses: Pending, Confirmed, Cancelled, Completed
const statusFilters = [
/pending/i,
/booked/i,
/confirmed/i,
/checked.?in/i,
];

// Check if any status filters are visible
let statusFilterFound = false;
for (const statusPattern of statusFilters) {
const statusElement = page.getByText(statusPattern).first();
if (await statusElement.isVisible({ timeout: 2000 }).catch(() => false)) {
statusFilterFound = true;
break;
}
}

// If status filters are not immediately visible, they might be in a dropdown or filter menu

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.

Same disease: the loop result statusFilterFound is computed, branched on, and then thrown away — the only real assertion is "some heading exists". You could delete the status-filter logic entirely and this still passes. Assert the actual status filter, please.

if (!statusFilterFound) {
const filterButton = page.getByRole("button", { name: /filter/i });
if (await filterButton.isVisible()) {
await filterButton.click();
// Status filters might be in the filter menu
}
}

// At minimum, the page should have loaded without errors
await expect(page.getByRole("heading")).toBeVisible();
});

/**
* Tests that appointments are displayed in the list/board
*/
test("should display appointments or empty state", async ({ page }) => {
await page.waitForLoadState("networkidle");

// Wait a bit for data to load
await page.waitForTimeout(1000);

// Check for either appointments or empty state
const hasAppointments = await page
.getByRole("table")
.isVisible({ timeout: 2000 })

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.

[class*="appointment"] — coupling a test to Tailwind-ish class name fragments. That selector will match anything from a wrapper div to a stray utility class and will break the moment someone renames a component. Use a stable data-cy/data-testid, which the codebase already uses elsewhere.

.catch(() => false);

const hasCards = await page
.locator('[data-testid*="appointment"], [class*="appointment"]')
.first()
.isVisible({ timeout: 2000 })
.catch(() => false);

const hasEmptyState = await page
.getByText(/no appointments/i)
.isVisible({ timeout: 2000 })
.catch(() => false);

// At least one should be true

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.

waitForLoadState("networkidle") in every test plus hardcoded waitForTimeout(1000)/(500) sprinkled around. Playwright's docs explicitly discourage both — they're slow on CI and still racy. Web-first assertions with auto-retry already do this job.

const pageIsWorking = hasAppointments || hasCards || hasEmptyState;
expect(pageIsWorking).toBe(true);
});

/**
* Tests navigation to appointment detail when an appointment is clicked
*/
test("should allow navigation to appointment details", async ({ page }) => {
await page.waitForLoadState("networkidle");
await page.waitForTimeout(1000);

// Switch to list view for easier row selection
const listTab = page.getByRole("tab", { name: /list/i });
await listTab.click();

// Check if there are any appointment rows
const rows = page.getByRole("row");
const rowCount = await rows.count();

if (rowCount > 1) {
// Skip header row (index 0), click first data row
const firstDataRow = rows.nth(1);

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("link").first() || getByRole("button").first() does NOT fall back — a Locator object is always truthy, so the button branch is dead code and you get a strict-mode/visibility failure instead. Use .or(): firstDataRow.getByRole("link").or(firstDataRow.getByRole("button")).first().

// Look for a clickable element in the row (link or button)
const clickableElement =
firstDataRow.getByRole("link").first() ||
firstDataRow.getByRole("button").first();

if (await clickableElement.isVisible({ timeout: 2000 })) {
await clickableElement.click();

// Wait for navigation
await page.waitForURL(/appointments\/.*/, { timeout: 5000 });

// Verify we're on an appointment detail page
const url = page.url();
expect(url).toMatch(/appointments\/[^/]+$/);
}
} else {
// No appointments to click - test passes as the page structure is correct

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.

When there are no rows this test logs to the console and passes green. A test that silently succeeds because there was no data is worse than no test — seed an appointment in setup, or test.skip() so at least the report tells the truth.

console.log("No appointments available for navigation test");
}
});
});
Loading