From cbe6f324043a8bcd245d2f1f9606beb826cbdf72 Mon Sep 17 00:00:00 2001 From: otsimaofficial Date: Sat, 29 Aug 2026 00:45:49 +0100 Subject: [PATCH] feat(dashboard): descriptive empty-state view for the job list Replace the bare "No jobs found" line with a reusable EmptyState component (icon, heading, supporting copy) shown once loading finishes with zero jobs for the connected wallet. Closes #276 --- __tests__/dashboard-empty-state.test.tsx | 101 +++++++++++++++++++++++ __tests__/empty-state.test.tsx | 38 +++++++++ app/components/EmptyState.tsx | 24 ++++++ app/dashboard/page.tsx | 7 +- 4 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 __tests__/dashboard-empty-state.test.tsx create mode 100644 __tests__/empty-state.test.tsx create mode 100644 app/components/EmptyState.tsx diff --git a/__tests__/dashboard-empty-state.test.tsx b/__tests__/dashboard-empty-state.test.tsx new file mode 100644 index 0000000..f7046f6 --- /dev/null +++ b/__tests__/dashboard-empty-state.test.tsx @@ -0,0 +1,101 @@ +/** + * Issue #276 – Design empty list display views for loading_spinner_skeleton + * + * Verifies that the Dashboard job list renders a descriptive EmptyState + * placeholder (not a bare line of text) once loading finishes with zero + * jobs, and that it steps aside once jobs are present. + */ +import { render, screen, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import Dashboard from "@/app/dashboard/page"; + +const mockUseWallet = vi.fn(); + +vi.mock("@/app/context/WalletContext", () => ({ + useWallet: () => mockUseWallet(), +})); + +vi.mock("@/app/components/Navbar", () => ({ + default: () =>
, +})); + +vi.mock("@/app/components/LoadingSkeleton", () => ({ + default: () =>
, +})); + +describe("Dashboard – empty job list view (issue #276)", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockUseWallet.mockReturnValue({ address: "GCLIENT", signTransaction: vi.fn() }); + }); + + it("renders the EmptyState placeholder when the wallet has zero jobs", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + json: async () => ({ success: true, data: [], page: 1, limit: 5, total: 0 }), + }) + ); + + render(); + + await waitFor(() => { + expect(screen.getByTestId("empty-state")).toBeInTheDocument(); + }); + }); + + it("shows a descriptive title and supporting description, not bare text", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + json: async () => ({ success: true, data: [], page: 1, limit: 5, total: 0 }), + }) + ); + + render(); + + await waitFor(() => { + expect(screen.getByTestId("empty-state-title")).toHaveTextContent("No jobs found"); + expect(screen.getByTestId("empty-state-description")).toHaveTextContent( + /create one to get started/i + ); + }); + }); + + it("does NOT render the EmptyState placeholder once jobs are present", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + json: async () => ({ + success: true, + data: [ + { + id: "job-1", + client: "GCLIENT", + freelancer: "GFREELANCER", + arbiter: "GARBITER", + funded: true, + milestones: [], + }, + ], + page: 1, + limit: 5, + total: 1, + }), + }) + ); + + render(); + + await waitFor(() => { + expect(screen.queryByTestId("loading-skeleton")).not.toBeInTheDocument(); + }); + expect(screen.queryByTestId("empty-state")).not.toBeInTheDocument(); + }); + + it("does NOT render the EmptyState placeholder while no wallet is connected", () => { + mockUseWallet.mockReturnValue({ address: null, signTransaction: vi.fn() }); + render(); + expect(screen.queryByTestId("empty-state")).not.toBeInTheDocument(); + }); +}); diff --git a/__tests__/empty-state.test.tsx b/__tests__/empty-state.test.tsx new file mode 100644 index 0000000..60527ac --- /dev/null +++ b/__tests__/empty-state.test.tsx @@ -0,0 +1,38 @@ +/** + * Issue #276 – Design empty list display views for loading_spinner_skeleton + * + * Unit coverage for the reusable EmptyState placeholder component. + */ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import EmptyState from "@/app/components/EmptyState"; + +describe("EmptyState", () => { + it("renders the provided title and description", () => { + render(); + expect(screen.getByTestId("empty-state-title")).toHaveTextContent("Nothing here"); + expect(screen.getByTestId("empty-state-description")).toHaveTextContent( + "Try again later." + ); + }); + + it("falls back to a default icon when none is supplied", () => { + render(); + expect(screen.getByText("🗂️")).toBeInTheDocument(); + }); + + it("renders a custom icon when supplied", () => { + render(); + expect(screen.getByText("📭")).toBeInTheDocument(); + }); + + it("hides the decorative icon from assistive tech", () => { + render(); + expect(screen.getByText("🗂️")).toHaveAttribute("aria-hidden", "true"); + }); + + it("applies the fade-in micro-animation on mount", () => { + render(); + expect(screen.getByTestId("empty-state")).toHaveClass("animate-fade-in"); + }); +}); diff --git a/app/components/EmptyState.tsx b/app/components/EmptyState.tsx new file mode 100644 index 0000000..112f51d --- /dev/null +++ b/app/components/EmptyState.tsx @@ -0,0 +1,24 @@ +interface EmptyStateProps { + title: string; + description: string; + icon?: string; +} + +export default function EmptyState({ title, description, icon = "🗂️" }: EmptyStateProps) { + return ( +
+ +

+ {title} +

+

+ {description} +

+
+ ); +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 64df656..761b5bd 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -5,6 +5,7 @@ import { useWallet } from "@/app/context/WalletContext"; import Navbar from "@/app/components/Navbar"; import MilestoneCard from "@/app/components/MilestoneCard"; import LoadingSkeleton from "@/app/components/LoadingSkeleton"; +import EmptyState from "@/app/components/EmptyState"; import { useActionStates } from "@/app/hooks/useActionStates"; import { useToast } from "@/app/context/ToastContext"; import { @@ -491,7 +492,11 @@ export default function Dashboard() { Error: {error}
) : jobs.length === 0 ? ( -

No jobs found for this wallet

+ ) : (