From 940326e525ef659dfd8a8349dc4c93a233eb54e2 Mon Sep 17 00:00:00 2001 From: NancieDev Date: Sat, 29 Aug 2026 12:40:59 +0000 Subject: [PATCH] fix: show loading skeleton on StatsCard for null metric values - Change StatsCard value prop type from string to string|null|undefined - Render a (with accessible aria-label including the card title) instead of the raw value when value is null or undefined, preventing NaN/blank renders while analytics data is in-flight - Add 14 tests covering: normal value display, falsy-but-valid '0', null skeleton, undefined skeleton, accessible label, title visible during load, variant class presence for all four variants (including null state) Closes # --- .../components/Dashboard/StatsCard.test.tsx | 95 +++++++++++++++++++ .../src/components/Dashboard/StatsCard.tsx | 14 ++- 2 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/Dashboard/StatsCard.test.tsx diff --git a/frontend/src/components/Dashboard/StatsCard.test.tsx b/frontend/src/components/Dashboard/StatsCard.test.tsx new file mode 100644 index 0000000..1183d15 --- /dev/null +++ b/frontend/src/components/Dashboard/StatsCard.test.tsx @@ -0,0 +1,95 @@ +import { describe, it, expect } from "vitest"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import StatsCard from "./StatsCard"; + +describe("StatsCard", () => { + // ------------------------------------------------------------------------- + // Normal rendering + // ------------------------------------------------------------------------- + describe("normal value rendering", () => { + it("renders the title", () => { + render(); + expect(screen.getByText("Total Settlements")).toBeInTheDocument(); + }); + + it("renders the value when provided", () => { + render(); + expect(screen.getByText("42")).toBeInTheDocument(); + }); + + it("does not render a skeleton when value is a non-empty string", () => { + render(); + expect(screen.queryByRole("status")).not.toBeInTheDocument(); + }); + + it("renders value '0' without a skeleton (falsy-but-valid value)", () => { + render(); + expect(screen.getByText("0")).toBeInTheDocument(); + expect(screen.queryByRole("status")).not.toBeInTheDocument(); + }); + }); + + // ------------------------------------------------------------------------- + // Loading skeleton for null / undefined + // ------------------------------------------------------------------------- + describe("loading skeleton state", () => { + it("renders a loading skeleton when value is null", () => { + render(); + expect(screen.getByRole("status")).toBeInTheDocument(); + }); + + it("renders a loading skeleton when value is undefined", () => { + render(); + expect(screen.getByRole("status")).toBeInTheDocument(); + }); + + it("does not render the value paragraph when value is null", () => { + render(); + // The value paragraph should be absent; only the skeleton status element. + const valueEl = screen.queryByText(/^\d/); + expect(valueEl).not.toBeInTheDocument(); + }); + + it("skeleton has an accessible label that includes the card title", () => { + render(); + // The Skeleton component renders with aria-label on the status div. + expect(screen.getByRole("status", { name: /Active Disputes/i })).toBeInTheDocument(); + }); + + it("still renders the card title while the value is loading", () => { + render(); + expect(screen.getByText("Pending Approvals")).toBeInTheDocument(); + }); + }); + + // ------------------------------------------------------------------------- + // Variant classes + // ------------------------------------------------------------------------- + describe("variant prop", () => { + it("applies the default variant class by default", () => { + const { container } = render(); + expect(container.firstChild).toHaveClass("stats-card--default"); + }); + + it("applies the success variant class", () => { + const { container } = render(); + expect(container.firstChild).toHaveClass("stats-card--success"); + }); + + it("applies the warning variant class", () => { + const { container } = render(); + expect(container.firstChild).toHaveClass("stats-card--warning"); + }); + + it("applies the danger variant class", () => { + const { container } = render(); + expect(container.firstChild).toHaveClass("stats-card--danger"); + }); + + it("applies the correct variant class even when value is null (loading state)", () => { + const { container } = render(); + expect(container.firstChild).toHaveClass("stats-card--danger"); + }); + }); +}); diff --git a/frontend/src/components/Dashboard/StatsCard.tsx b/frontend/src/components/Dashboard/StatsCard.tsx index 3e00243..8704984 100644 --- a/frontend/src/components/Dashboard/StatsCard.tsx +++ b/frontend/src/components/Dashboard/StatsCard.tsx @@ -1,8 +1,9 @@ +import { Skeleton } from "../Skeleton"; import "./StatsCard.css"; interface StatsCardProps { title: string; - value: string; + value: string | null | undefined; variant?: "default" | "success" | "warning" | "danger"; } @@ -10,7 +11,16 @@ export default function StatsCard({ title, value, variant = "default" }: StatsCa return (

{title}

-

{value}

+ {value == null ? ( + + ) : ( +

{value}

+ )}
); }