b673363351
Adds a working Dark/Light toggle (next to the language switcher, same row
as EN/VI) and re-themes the whole dashboard, not just the handful of
components that already used semantic tokens.
- Tailwind darkMode:"class" + CSS-variable color tokens (client/src/index.css,
tailwind.config.js): surface.0-5, border/border-light, accent/accent-hover,
fg.primary/secondary/muted, status.success/danger/warning. One class flip
on <html> re-themes everything — no per-element dark: variant pairs.
- useTheme() hook: localStorage-persisted, defaults to dark, no
prefers-color-scheme fallback (client/src/hooks/useTheme.ts).
- Mechanical, table-driven migration (scripts/migrate-color-tokens.mjs,
scripts/tokenize-status-colors.mjs, scripts/darken-status-colors.mjs) of
every raw neutral/gray/slate + emerald/red/amber Tailwind utility across
client/src onto the new tokens, so every badge/button/component pulls the
same shade per status/role instead of each picking its own.
- Palette values are the literal Radix Colors (radix-ui.com/colors) scale
constants — slate/blue/green/red/amber steps 1-12 — adopted after three
rounds of hand-picked values that kept overshooting (flat, then too dark,
then glaring); see docs/superpowers/specs/2026-07-31-color-redesign-
dark-light-mode-design.md for the full history and role mapping.
- PipelineMap: done/current/failed/passed-no-evidence/detected share one
visual language (border + text + translucent wash of the same status
color); `current` alone stays a solid accent fill, the one state that
gets to look bolder ("you are here").
- LaneCard: removed the stage/kind/auto-stage chips that duplicated the
Workspace lane-detail header already showing them.
Categorical/decorative hues (violet, indigo, cyan, teal, sky, rose, pink,
orange, yellow, and blue where it plays a role-coloring part e.g. message
bubbles) are deliberately out of scope — collapsing those onto shared
tokens would erase the distinction between different kinds of thing, not
a status.
75 lines
3.4 KiB
TypeScript
75 lines
3.4 KiB
TypeScript
/**
|
|
* @file StatCard.test.tsx
|
|
* @description Unit tests for the StatCard component, which is a reusable React component that displays a statistic with a label, value, icon, and optional trend information. The tests cover rendering of the label, value (both numeric and string), trend information, and the icon. The tests also verify that custom accent colors are applied correctly. The tests use React Testing Library and Vitest for assertions and mocking.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
import { render, screen } from "@testing-library/react";
|
|
import { StatCard } from "../StatCard";
|
|
import { Activity } from "lucide-react";
|
|
|
|
describe("StatCard", () => {
|
|
it("should render label", () => {
|
|
render(<StatCard label="Total Sessions" value={42} icon={Activity} />);
|
|
expect(screen.getByText("Total Sessions")).toBeInTheDocument();
|
|
});
|
|
|
|
it("should render numeric value", () => {
|
|
render(<StatCard label="Events" value={156} icon={Activity} />);
|
|
expect(screen.getByText("156")).toBeInTheDocument();
|
|
});
|
|
|
|
it("should render string value", () => {
|
|
render(<StatCard label="Status" value="-" icon={Activity} />);
|
|
expect(screen.getByText("-")).toBeInTheDocument();
|
|
});
|
|
|
|
it("should render trend when provided", () => {
|
|
render(<StatCard label="Sessions" value={10} icon={Activity} trend="3 active" />);
|
|
expect(screen.getByText("3 active")).toBeInTheDocument();
|
|
});
|
|
|
|
it("should not render trend when not provided", () => {
|
|
render(<StatCard label="Sessions" value={10} icon={Activity} />);
|
|
expect(screen.queryByText("active")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("should render the icon", () => {
|
|
const { container } = render(<StatCard label="Test" value={0} icon={Activity} />);
|
|
// Lucide renders as SVG
|
|
const svg = container.querySelector("svg");
|
|
expect(svg).toBeInTheDocument();
|
|
});
|
|
|
|
it("should apply custom accent color", () => {
|
|
const { container } = render(
|
|
<StatCard label="Test" value={0} icon={Activity} accentColor="text-status-success" />
|
|
);
|
|
const svg = container.querySelector("svg");
|
|
expect(svg?.className?.baseVal ?? svg?.getAttribute("class")).toContain("text-status-success");
|
|
});
|
|
|
|
it("should apply default accent color when not specified", () => {
|
|
const { container } = render(<StatCard label="Test" value={0} icon={Activity} />);
|
|
const svg = container.querySelector("svg");
|
|
expect(svg?.className?.baseVal ?? svg?.getAttribute("class")).toContain("text-accent");
|
|
});
|
|
|
|
it("should render a skeleton placeholder when loading and hide the real value", () => {
|
|
const { container } = render(<StatCard label="Total" value="" icon={Activity} loading />);
|
|
// value text should NOT appear so users never see a flash of "-" or 0
|
|
expect(screen.queryByText("-")).not.toBeInTheDocument();
|
|
expect(screen.queryByText("0")).not.toBeInTheDocument();
|
|
// skeleton primitive renders an aria-busy node
|
|
expect(container.querySelector('[aria-busy="true"]')).toBeInTheDocument();
|
|
});
|
|
|
|
it("should swap from skeleton to value when loading flips false", () => {
|
|
const { rerender } = render(<StatCard label="Total" value="" icon={Activity} loading />);
|
|
expect(screen.queryByText("42")).not.toBeInTheDocument();
|
|
rerender(<StatCard label="Total" value={42} icon={Activity} loading={false} />);
|
|
expect(screen.getByText("42")).toBeInTheDocument();
|
|
});
|
|
});
|