From 758b2a839b30c6a221315a69e66c6f43ddb1a0ba Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 17 Feb 2026 15:15:54 -0600 Subject: [PATCH] fix(web-tests): stabilize async auth and usage page assertions --- apps/web/src/app/(auth)/login/page.test.tsx | 44 ++++++++++++++++--- .../app/(authenticated)/usage/page.test.tsx | 44 ++++++++++++++++--- 2 files changed, 75 insertions(+), 13 deletions(-) diff --git a/apps/web/src/app/(auth)/login/page.test.tsx b/apps/web/src/app/(auth)/login/page.test.tsx index dc75f8b..d2b8d57 100644 --- a/apps/web/src/app/(auth)/login/page.test.tsx +++ b/apps/web/src/app/(auth)/login/page.test.tsx @@ -104,19 +104,28 @@ describe("LoginPage", (): void => { expect(screen.getByText("Loading authentication options")).toBeInTheDocument(); }); - it("renders the page heading and description", (): void => { + it("renders the page heading and description", async (): Promise => { mockFetchConfig(EMAIL_ONLY_CONFIG); render(); + await waitFor((): void => { + expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); + }); + expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Welcome to Mosaic Stack"); expect(screen.getByText(/Your personal assistant platform/i)).toBeInTheDocument(); }); - it("has proper layout styling", (): void => { + it("has proper layout styling", async (): Promise => { mockFetchConfig(EMAIL_ONLY_CONFIG); const { container } = render(); + + await waitFor((): void => { + expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); + }); + const main = container.querySelector("main"); expect(main).toHaveClass("flex", "min-h-screen"); }); @@ -430,37 +439,56 @@ describe("LoginPage", (): void => { /* ------------------------------------------------------------------ */ describe("responsive layout", (): void => { - it("applies mobile-first padding to main element", (): void => { + it("applies mobile-first padding to main element", async (): Promise => { mockFetchConfig(EMAIL_ONLY_CONFIG); const { container } = render(); + + await waitFor((): void => { + expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); + }); + const main = container.querySelector("main"); expect(main).toHaveClass("p-4", "sm:p-8"); }); - it("applies responsive text size to heading", (): void => { + it("applies responsive text size to heading", async (): Promise => { mockFetchConfig(EMAIL_ONLY_CONFIG); render(); + await waitFor((): void => { + expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); + }); + const heading = screen.getByRole("heading", { level: 1 }); expect(heading).toHaveClass("text-2xl", "sm:text-4xl"); }); - it("applies responsive padding to card container", (): void => { + it("applies responsive padding to card container", async (): Promise => { mockFetchConfig(EMAIL_ONLY_CONFIG); const { container } = render(); + + await waitFor((): void => { + expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); + }); + const card = container.querySelector(".bg-white"); expect(card).toHaveClass("p-4", "sm:p-8"); }); - it("card container has full width with max-width constraint", (): void => { + it("card container has full width with max-width constraint", async (): Promise => { mockFetchConfig(EMAIL_ONLY_CONFIG); const { container } = render(); + + await waitFor((): void => { + expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); + }); + const wrapper = container.querySelector(".max-w-md"); expect(wrapper).toHaveClass("w-full", "max-w-md"); @@ -539,7 +567,9 @@ describe("LoginPage", (): void => { }); // LoginForm auto-focuses the email input on mount - expect(screen.getByLabelText(/email/i)).toHaveFocus(); + await waitFor((): void => { + expect(screen.getByLabelText(/email/i)).toHaveFocus(); + }); // Tab forward through form: email -> password -> submit await user.tab(); diff --git a/apps/web/src/app/(authenticated)/usage/page.test.tsx b/apps/web/src/app/(authenticated)/usage/page.test.tsx index 4d97ff6..c136ffb 100644 --- a/apps/web/src/app/(authenticated)/usage/page.test.tsx +++ b/apps/web/src/app/(authenticated)/usage/page.test.tsx @@ -1,5 +1,6 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, waitFor, fireEvent } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import type { ReactNode } from "react"; import UsagePage from "./page"; @@ -113,6 +114,15 @@ function setupMocks(overrides?: { empty?: boolean; error?: boolean }): void { vi.mocked(fetchTaskOutcomes).mockResolvedValue(mockTaskOutcomes); } +function setupPendingMocks(): void { + // eslint-disable-next-line @typescript-eslint/no-empty-function -- intentionally unresolved for loading-state test + const pending = new Promise(() => {}); + vi.mocked(fetchUsageSummary).mockReturnValue(pending); + vi.mocked(fetchTokenUsage).mockReturnValue(pending); + vi.mocked(fetchCostBreakdown).mockReturnValue(pending); + vi.mocked(fetchTaskOutcomes).mockReturnValue(pending); +} + // ─── Tests ─────────────────────────────────────────────────────────── describe("UsagePage", (): void => { @@ -120,23 +130,32 @@ describe("UsagePage", (): void => { vi.clearAllMocks(); }); - it("should render the page title and subtitle", (): void => { + it("should render the page title and subtitle", async (): Promise => { setupMocks(); render(); + await waitFor((): void => { + expect(screen.getByTestId("summary-cards")).toBeInTheDocument(); + }); + expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Usage"); expect(screen.getByText("Token usage and cost overview")).toBeInTheDocument(); }); - it("should have proper layout structure", (): void => { + it("should have proper layout structure", async (): Promise => { setupMocks(); const { container } = render(); + + await waitFor((): void => { + expect(screen.getByTestId("summary-cards")).toBeInTheDocument(); + }); + const main = container.querySelector("main"); expect(main).toBeInTheDocument(); }); it("should show loading skeleton initially", (): void => { - setupMocks(); + setupPendingMocks(); render(); expect(screen.getByTestId("loading-skeleton")).toBeInTheDocument(); }); @@ -171,25 +190,34 @@ describe("UsagePage", (): void => { }); }); - it("should render the time range selector with three options", (): void => { + it("should render the time range selector with three options", async (): Promise => { setupMocks(); render(); + await waitFor((): void => { + expect(screen.getByTestId("summary-cards")).toBeInTheDocument(); + }); + expect(screen.getByText("7 Days")).toBeInTheDocument(); expect(screen.getByText("30 Days")).toBeInTheDocument(); expect(screen.getByText("90 Days")).toBeInTheDocument(); }); - it("should have 30 Days selected by default", (): void => { + it("should have 30 Days selected by default", async (): Promise => { setupMocks(); render(); + await waitFor((): void => { + expect(screen.getByTestId("summary-cards")).toBeInTheDocument(); + }); + const button30d = screen.getByText("30 Days"); expect(button30d).toHaveAttribute("aria-pressed", "true"); }); it("should change time range when a different option is clicked", async (): Promise => { setupMocks(); + const user = userEvent.setup(); render(); // Wait for initial load @@ -199,7 +227,11 @@ describe("UsagePage", (): void => { // Click 7 Days const button7d = screen.getByText("7 Days"); - fireEvent.click(button7d); + await user.click(button7d); + + await waitFor((): void => { + expect(fetchUsageSummary).toHaveBeenCalledWith("7d"); + }); expect(button7d).toHaveAttribute("aria-pressed", "true"); expect(screen.getByText("30 Days")).toHaveAttribute("aria-pressed", "false");