Skip to content
Open
Show file tree
Hide file tree
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
101 changes: 101 additions & 0 deletions __tests__/dashboard-empty-state.test.tsx
Original file line number Diff line number Diff line change
@@ -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: () => <div data-testid="navbar" />,
}));

vi.mock("@/app/components/LoadingSkeleton", () => ({
default: () => <div data-testid="loading-skeleton" />,
}));

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(<Dashboard />);

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(<Dashboard />);

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(<Dashboard />);

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(<Dashboard />);
expect(screen.queryByTestId("empty-state")).not.toBeInTheDocument();
});
});
38 changes: 38 additions & 0 deletions __tests__/empty-state.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<EmptyState title="Nothing here" description="Try again later." />);
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(<EmptyState title="Nothing here" description="Try again later." />);
expect(screen.getByText("🗂️")).toBeInTheDocument();
});

it("renders a custom icon when supplied", () => {
render(<EmptyState title="Nothing here" description="Try again later." icon="📭" />);
expect(screen.getByText("📭")).toBeInTheDocument();
});

it("hides the decorative icon from assistive tech", () => {
render(<EmptyState title="Nothing here" description="Try again later." />);
expect(screen.getByText("🗂️")).toHaveAttribute("aria-hidden", "true");
});

it("applies the fade-in micro-animation on mount", () => {
render(<EmptyState title="Nothing here" description="Try again later." />);
expect(screen.getByTestId("empty-state")).toHaveClass("animate-fade-in");
});
});
24 changes: 24 additions & 0 deletions app/components/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
interface EmptyStateProps {
title: string;
description: string;
icon?: string;
}

export default function EmptyState({ title, description, icon = "🗂️" }: EmptyStateProps) {
return (
<div
data-testid="empty-state"
className="flex flex-col items-center justify-center text-center gap-2 border border-gray-800 rounded-xl bg-gray-900 p-10 animate-fade-in"
>
<span aria-hidden="true" className="text-4xl">
{icon}
</span>
<p data-testid="empty-state-title" className="text-white font-semibold">
{title}
</p>
<p data-testid="empty-state-description" className="text-gray-400 text-sm max-w-sm">
{description}
</p>
</div>
);
}
7 changes: 6 additions & 1 deletion app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -491,7 +492,11 @@ export default function Dashboard() {
Error: {error}
</div>
) : jobs.length === 0 ? (
<p className="text-center text-gray-400">No jobs found for this wallet</p>
<EmptyState
title="No jobs found"
description="You don't have any escrow jobs for this wallet yet. Create one to get started."
icon="🗂️"
/>
) : (
<div className="space-y-5">
<div className="border border-gray-800 rounded-xl bg-gray-900 overflow-hidden">
Expand Down