From ea2c2ec1c819104224e3514f2692ededd6d5e04c Mon Sep 17 00:00:00 2001 From: vicajohn Date: Thu, 27 Aug 2026 06:26:59 +0100 Subject: [PATCH 01/16] feat: implement descriptive empty state UI for dashboard jobs list - Replace hardcoded empty message with EmptyStateCard component - Display briefcase icon for job-related context - Show descriptive title and explanation text - Include role badges (Client, Freelancer, Arbiter) showing available participation options - Add comprehensive test coverage with 16 test cases - Ensure proper accessibility with region landmarks and aria-labels - Validates placeholder display under empty data states --- __tests__/dashboard-list-empty-state.test.tsx | 417 ++++++++++++++++++ app/dashboard/page.tsx | 10 +- 2 files changed, 426 insertions(+), 1 deletion(-) create mode 100644 __tests__/dashboard-list-empty-state.test.tsx diff --git a/__tests__/dashboard-list-empty-state.test.tsx b/__tests__/dashboard-list-empty-state.test.tsx new file mode 100644 index 0000000..5f0b419 --- /dev/null +++ b/__tests__/dashboard-list-empty-state.test.tsx @@ -0,0 +1,417 @@ +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(); +const mockUseToast = vi.fn(); + +vi.mock("@/app/context/WalletContext", () => ({ + useWallet: () => mockUseWallet(), +})); + +vi.mock("@/app/context/ToastContext", () => ({ + useToast: () => mockUseToast(), +})); + +vi.mock("@/app/components/Navbar", () => ({ + default: () =>
, +})); + +vi.mock("@/app/components/LoadingSkeleton", () => ({ + default: () =>
, +})); + +vi.mock("@/app/components/MilestoneCard", () => ({ + default: () =>
, +})); + +describe("Dashboard — empty state placeholder UI", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockUseWallet.mockReturnValue({ + address: "GCLIENT", + signTransaction: vi.fn(), + }); + mockUseToast.mockReturnValue({ + showToast: vi.fn(), + toasts: [], + hideToast: vi.fn(), + }); + }); + + describe("Placeholder elements rendering", () => { + it("displays descriptive empty state card when jobs list is empty", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + json: async () => ({ + success: true, + data: [], + page: 1, + limit: 5, + total: 0, + }), + }) + ); + + render(); + + await waitFor(() => { + expect(screen.getByTestId("dashboard-empty-state")).toBeInTheDocument(); + }); + }); + + it("renders with proper briefcase icon for job-related context", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + json: async () => ({ + success: true, + data: [], + page: 1, + limit: 5, + total: 0, + }), + }) + ); + + const { container } = render(); + + await waitFor(() => { + const emptyState = screen.getByTestId("dashboard-empty-state"); + expect(emptyState).toBeInTheDocument(); + // Check that SVG icon is rendered + const icons = container.querySelectorAll("svg"); + expect(icons.length).toBeGreaterThan(0); + }); + }); + + it("displays descriptive title 'No jobs found'", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + json: async () => ({ + success: true, + data: [], + page: 1, + limit: 5, + total: 0, + }), + }) + ); + + render(); + + await waitFor(() => { + expect(screen.getByText("No jobs found")).toBeInTheDocument(); + }); + }); + + it("displays descriptive subtitle explaining the empty state", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + json: async () => ({ + success: true, + data: [], + page: 1, + limit: 5, + total: 0, + }), + }) + ); + + render(); + + await waitFor(() => { + expect( + screen.getByText( + /You don't have any jobs yet. Connect your wallet to see jobs you're involved in as a client, freelancer, or arbiter/ + ) + ).toBeInTheDocument(); + }); + }); + }); + + describe("Role badges in empty state", () => { + it("renders role badges showing available participation options", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + json: async () => ({ + success: true, + data: [], + page: 1, + limit: 5, + total: 0, + }), + }) + ); + + render(); + + await waitFor(() => { + expect(screen.getByText("Client")).toBeInTheDocument(); + expect(screen.getByText("Freelancer")).toBeInTheDocument(); + expect(screen.getByText("Arbiter")).toBeInTheDocument(); + }); + }); + + it("displays all three role badges together in empty state", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + json: async () => ({ + success: true, + data: [], + page: 1, + limit: 5, + total: 0, + }), + }) + ); + + render(); + + await waitFor(() => { + const emptyState = screen.getByTestId("dashboard-empty-state"); + const clientBadge = screen.getByText("Client"); + const freelancerBadge = screen.getByText("Freelancer"); + const arbiterBadge = screen.getByText("Arbiter"); + + expect(emptyState).toContainElement(clientBadge); + expect(emptyState).toContainElement(freelancerBadge); + expect(emptyState).toContainElement(arbiterBadge); + }); + }); + }); + + describe("Accessibility and semantic markup", () => { + it("renders empty state as an accessible region landmark", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + json: async () => ({ + success: true, + data: [], + page: 1, + limit: 5, + total: 0, + }), + }) + ); + + render(); + + await waitFor(() => { + expect(screen.getByRole("region", { name: "No jobs" })).toBeInTheDocument(); + }); + }); + + it("has descriptive aria-label for screen reader context", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + json: async () => ({ + success: true, + data: [], + page: 1, + limit: 5, + total: 0, + }), + }) + ); + + render(); + + await waitFor(() => { + const region = screen.getByRole("region", { name: "No jobs" }); + expect(region).toHaveAttribute("aria-label", "No jobs"); + }); + }); + + it("has proper semantic styling with border and rounded container", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + json: async () => ({ + success: true, + data: [], + page: 1, + limit: 5, + total: 0, + }), + }) + ); + + render(); + + await waitFor(() => { + const emptyState = screen.getByTestId("dashboard-empty-state"); + expect(emptyState).toHaveClass("border", "rounded-lg", "bg-surface-card"); + }); + }); + }); + + describe("State transitions", () => { + it("transitions from loading skeleton to empty state", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + json: async () => ({ + success: true, + data: [], + page: 1, + limit: 5, + total: 0, + }), + }) + ); + + render(); + + // Initially shows loading + expect(screen.getByTestId("loading-skeleton")).toBeInTheDocument(); + + // Then transitions to empty state + await waitFor(() => { + expect(screen.queryByTestId("loading-skeleton")).not.toBeInTheDocument(); + expect(screen.getByTestId("dashboard-empty-state")).toBeInTheDocument(); + }); + }); + + it("hides empty state when jobs load successfully", 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("dashboard-empty-state")).not.toBeInTheDocument(); + }); + }); + }); + + describe("Empty state with different data conditions", () => { + it("shows empty state when API returns empty data array", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + json: async () => ({ + success: true, + data: [], + page: 1, + limit: 5, + total: 0, + }), + }) + ); + + render(); + + await waitFor(() => { + expect(screen.getByTestId("dashboard-empty-state")).toBeInTheDocument(); + expect(screen.getByText("No jobs found")).toBeInTheDocument(); + }); + }); + + it("shows empty state when filtering results in no matches", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + json: async () => ({ + success: true, + data: [], + page: 1, + limit: 5, + total: 0, + }), + }) + ); + + render(); + + await waitFor(() => { + expect(screen.getByTestId("dashboard-empty-state")).toBeInTheDocument(); + }); + }); + + it("displays empty state with coherent messaging for disconnected wallet", async () => { + mockUseWallet.mockReturnValue({ + address: null, + signTransaction: vi.fn(), + }); + + render(); + + expect( + screen.getByText(/Connect your wallet to view your jobs/) + ).toBeInTheDocument(); + }); + }); + + describe("Visual hierarchy and structure", () => { + it("centers content within the empty state card", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + json: async () => ({ + success: true, + data: [], + page: 1, + limit: 5, + total: 0, + }), + }) + ); + + render(); + + await waitFor(() => { + const emptyState = screen.getByTestId("dashboard-empty-state"); + expect(emptyState).toHaveClass("flex", "flex-col", "items-center", "text-center"); + }); + }); + + it("spaces content elements properly with gap utility", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + json: async () => ({ + success: true, + data: [], + page: 1, + limit: 5, + total: 0, + }), + }) + ); + + render(); + + await waitFor(() => { + const emptyState = screen.getByTestId("dashboard-empty-state"); + expect(emptyState).toHaveClass("gap-4"); + }); + }); + }); +}); diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 64df656..9840912 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 EmptyStateCard from "@/app/components/EmptyStateCard"; import { useActionStates } from "@/app/hooks/useActionStates"; import { useToast } from "@/app/context/ToastContext"; import { @@ -491,7 +492,14 @@ export default function Dashboard() { Error: {error}
) : jobs.length === 0 ? ( -

No jobs found for this wallet

+ ) : (
From 92aae616e43390ad09ba374937c6b9cc7e6d1d59 Mon Sep 17 00:00:00 2001 From: OBAZE SAMUEL OSHIOKE Date: Fri, 28 Aug 2026 16:52:30 +0100 Subject: [PATCH 02/16] feat: add DarkModeSwitcher base component for Storybook (closes #318) --- app/components/DarkModeSwitcher.tsx | 169 ++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 app/components/DarkModeSwitcher.tsx diff --git a/app/components/DarkModeSwitcher.tsx b/app/components/DarkModeSwitcher.tsx new file mode 100644 index 0000000..65dfc26 --- /dev/null +++ b/app/components/DarkModeSwitcher.tsx @@ -0,0 +1,169 @@ +"use client"; + +import ButtonSpinner from "./ButtonSpinner"; + +export interface DarkModeSwitcherProps { + /** Current theme state: true = dark, false = light, null/undefined = empty/no data */ + isDarkMode?: boolean | null; + /** Toggle handler */ + onToggle?: () => void; + /** Whether the switch is disabled */ + disabled?: boolean; + /** Loading state - shows spinner */ + loading?: boolean; + /** Optional id for the control */ + id?: string; + /** Additional className */ + className?: string; + /** Accessible label override */ + ariaLabel?: string; +} + +/** + * Empty state view for DarkModeSwitcher. + * Displayed when theme data is unavailable (isDarkMode is null/undefined). + * Uses design tokens and is fully accessible. + */ +export function DarkModeSwitcherEmptyState({ + className = "", +}: { + className?: string; +}) { + return ( +
+ +

+ No theme preferences available +

+

+ Theme data is empty. Once theme preferences are configured, the + dark/light toggle will appear here. You can still browse in the default + light theme. +

+ +
+ ); +} + +export default function DarkModeSwitcher({ + isDarkMode, + onToggle, + disabled = false, + loading = false, + id, + className = "", + ariaLabel, +}: DarkModeSwitcherProps) { + const checked = Boolean(isDarkMode); + const isEmpty = isDarkMode === null || isDarkMode === undefined; + const isDisabled = disabled || loading; + + // Empty state - descriptive placeholder when no data + if (isEmpty && !loading) { + return ; + } + + // Loading state + if (loading) { + return ( + + + Loading theme... + + ); + } + + const label = ariaLabel ?? (checked ? "Switch to light mode" : "Switch to dark mode"); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (isDisabled) return; + if (e.key === " " || e.key === "Enter") { + e.preventDefault(); + onToggle?.(); + } + }; + + const handleClick = () => { + if (isDisabled) return; + onToggle?.(); + }; + + return ( + + ); +} From 32be981c5af91d20cbf801110f3aa78c1fecc3f7 Mon Sep 17 00:00:00 2001 From: OBAZE SAMUEL OSHIOKE Date: Fri, 28 Aug 2026 16:53:26 +0100 Subject: [PATCH 03/16] feat: add Storybook stories for DarkModeSwitcher (closes #318) --- app/components/DarkModeSwitcher.stories.tsx | 189 ++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 app/components/DarkModeSwitcher.stories.tsx diff --git a/app/components/DarkModeSwitcher.stories.tsx b/app/components/DarkModeSwitcher.stories.tsx new file mode 100644 index 0000000..9634033 --- /dev/null +++ b/app/components/DarkModeSwitcher.stories.tsx @@ -0,0 +1,189 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { fn } from "@storybook/test"; + +import DarkModeSwitcher, { DarkModeSwitcherEmptyState } from "./DarkModeSwitcher"; + +const meta = { + title: "Components/DarkModeSwitcher", + component: DarkModeSwitcher, + tags: ["autodocs"], + parameters: { + layout: "centered", + backgrounds: { + default: "dark", + values: [ + { name: "dark", value: "#030712" }, + { name: "light", value: "#ffffff" }, + ], + }, + }, + argTypes: { + isDarkMode: { + control: "select", + options: [true, false, null], + description: "Theme state: true=dark, false=light, null=empty", + }, + disabled: { control: "boolean" }, + loading: { control: "boolean" }, + onToggle: { action: "toggled" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// --------------------------------------------------------------------------- +// 1. Light - default light theme +// --------------------------------------------------------------------------- +export const Light: Story = { + name: "Light - default", + args: { + isDarkMode: false, + onToggle: fn(), + }, +}; + +// --------------------------------------------------------------------------- +// 2. Dark - dark theme active +// --------------------------------------------------------------------------- +export const Dark: Story = { + name: "Dark - active", + args: { + isDarkMode: true, + onToggle: fn(), + }, +}; + +// --------------------------------------------------------------------------- +// 3. Disabled - light +// --------------------------------------------------------------------------- +export const DisabledLight: Story = { + name: "Disabled - light", + args: { + isDarkMode: false, + disabled: true, + onToggle: fn(), + }, +}; + +// --------------------------------------------------------------------------- +// 4. Disabled - dark +// --------------------------------------------------------------------------- +export const DisabledDark: Story = { + name: "Disabled - dark", + args: { + isDarkMode: true, + disabled: true, + onToggle: fn(), + }, +}; + +// --------------------------------------------------------------------------- +// 5. Loading - spinner state +// --------------------------------------------------------------------------- +export const Loading: Story = { + name: "Loading", + args: { + isDarkMode: false, + loading: true, + onToggle: fn(), + }, +}; + +// --------------------------------------------------------------------------- +// 6. Empty - null theme data +// --------------------------------------------------------------------------- +export const Empty: Story = { + name: "Empty - no theme data", + args: { + isDarkMode: null, + onToggle: fn(), + }, +}; + +// --------------------------------------------------------------------------- +// 7. Empty - undefined +// --------------------------------------------------------------------------- +export const EmptyUndefined: Story = { + name: "Empty - undefined", + args: { + isDarkMode: undefined, + onToggle: fn(), + }, +}; + +// --------------------------------------------------------------------------- +// 8. Dark with custom ariaLabel +// --------------------------------------------------------------------------- +export const CustomAriaLabel: Story = { + name: "Custom ariaLabel", + args: { + isDarkMode: false, + ariaLabel: "Toggle application theme", + onToggle: fn(), + }, +}; + +// --------------------------------------------------------------------------- +// 9. Interactive - hover/focus preview (dark) +// --------------------------------------------------------------------------- +export const InteractiveDark: Story = { + name: "Interactive - dark hover/focus", + args: { + isDarkMode: true, + onToggle: fn(), + }, + parameters: { + pseudo: { hover: true, focus: true }, + }, +}; + +// --------------------------------------------------------------------------- +// 10. Interactive - light hover +// --------------------------------------------------------------------------- +export const InteractiveLight: Story = { + name: "Interactive - light hover", + args: { + isDarkMode: false, + onToggle: fn(), + }, +}; + +// --------------------------------------------------------------------------- +// 11. Empty state component standalone +// --------------------------------------------------------------------------- +export const EmptyStateStandalone: StoryObj = { + name: "EmptyState - standalone", + render: () => , +}; + +// --------------------------------------------------------------------------- +// 12. All states overview +// --------------------------------------------------------------------------- +export const AllStates: Story = { + name: "All states - overview", + render: () => ( +
+
+ Light + +
+
+ Dark + +
+
+ Disabled + +
+
+ Loading + +
+
+ Empty + +
+
+ ), +}; From 0f77e298edefd7dc1c5bfd7c56f2f54cf9282b21 Mon Sep 17 00:00:00 2001 From: OBAZE SAMUEL OSHIOKE Date: Fri, 28 Aug 2026 16:53:27 +0100 Subject: [PATCH 04/16] test: add storybook verification tests (closes #318) --- __tests__/dark-mode-switcher-stories.test.tsx | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 __tests__/dark-mode-switcher-stories.test.tsx diff --git a/__tests__/dark-mode-switcher-stories.test.tsx b/__tests__/dark-mode-switcher-stories.test.tsx new file mode 100644 index 0000000..86383a6 --- /dev/null +++ b/__tests__/dark-mode-switcher-stories.test.tsx @@ -0,0 +1,104 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import DarkModeSwitcher from "@/app/components/DarkModeSwitcher"; +import * as stories from "@/app/components/DarkModeSwitcher.stories"; + +describe("DarkModeSwitcher - Storybook stories #318", () => { + it("exports default meta with title Components/DarkModeSwitcher", () => { + expect(stories.default.title).toBe("Components/DarkModeSwitcher"); + }); + + it("has Light story with isDarkMode false", () => { + expect(stories.Light.args?.isDarkMode).toBe(false); + }); + + it("has Dark story with isDarkMode true", () => { + expect(stories.Dark.args?.isDarkMode).toBe(true); + }); + + it("has DisabledLight story disabled", () => { + expect(stories.DisabledLight.args?.disabled).toBe(true); + expect(stories.DisabledLight.args?.isDarkMode).toBe(false); + }); + + it("has DisabledDark story disabled dark", () => { + expect(stories.DisabledDark.args?.disabled).toBe(true); + expect(stories.DisabledDark.args?.isDarkMode).toBe(true); + }); + + it("has Loading story with loading true", () => { + expect(stories.Loading.args?.loading).toBe(true); + }); + + it("has Empty story with null", () => { + expect(stories.Empty.args?.isDarkMode).toBe(null); + }); + + it("has EmptyUndefined story", () => { + expect(stories.EmptyUndefined.args?.isDarkMode).toBeUndefined(); + }); + + it("has CustomAriaLabel story", () => { + expect(stories.CustomAriaLabel.args?.ariaLabel).toBe("Toggle application theme"); + }); + + it("Light story renders switch with aria-checked false", () => { + render(); + expect(screen.getByRole("switch")).toHaveAttribute("aria-checked", "false"); + }); + + it("Dark story renders switch with aria-checked true", () => { + render(); + // last switch is dark + const switches = screen.getAllByRole("switch"); + expect(switches[switches.length - 1]).toHaveAttribute("aria-checked", "true"); + }); + + it("Disabled story renders disabled switch", () => { + render(); + const sw = screen.getAllByRole("switch").pop(); + expect(sw).toBeDisabled(); + }); + + it("Loading story renders status", () => { + render(); + expect(screen.getByText("Loading theme...")).toBeInTheDocument(); + }); + + it("Empty story renders empty placeholder", () => { + render(); + expect(screen.getByTestId("dark-mode-switcher-empty-state")).toBeInTheDocument(); + }); + + it("EmptyStateStandalone story renders empty state", () => { + const Story = stories.EmptyStateStandalone.render; + expect(Story).toBeDefined(); + if (Story) render(); + // check via previous empty? Need to isolate + }); + + it("AllStates story renders overview", () => { + const Story = stories.AllStates.render; + expect(Story).toBeDefined(); + if (Story) { + const { container } = render(); + expect(container.textContent).toContain("Light"); + expect(container.textContent).toContain("Dark"); + } + }); + + it("stories have mocked onToggle (fn)", () => { + expect(typeof stories.Light.args?.onToggle).toBe("function"); + expect(typeof stories.Dark.args?.onToggle).toBe("function"); + }); + + it("meta has argTypes for controls", () => { + expect(stories.default.argTypes?.isDarkMode).toBeDefined(); + expect(stories.default.argTypes?.disabled).toBeDefined(); + expect(stories.default.argTypes?.loading).toBeDefined(); + }); + + it("meta tags includes autodocs", () => { + expect(stories.default.tags).toContain("autodocs"); + }); +}); From 1075c45cf640318f87c498a086793a252b8e497c Mon Sep 17 00:00:00 2001 From: She-ge Date: Sat, 29 Aug 2026 16:00:43 +0100 Subject: [PATCH 05/16] Dispute Raise ModaL --- __tests__/wallet_selector_modal.test.tsx | 56 +++++++++++++++++++++++- app/components/WalletSelectorModal.tsx | 38 ++++++++-------- 2 files changed, 74 insertions(+), 20 deletions(-) diff --git a/__tests__/wallet_selector_modal.test.tsx b/__tests__/wallet_selector_modal.test.tsx index 8413081..0a5eb66 100644 --- a/__tests__/wallet_selector_modal.test.tsx +++ b/__tests__/wallet_selector_modal.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; import WalletSelectorModal, { detectAnyWalletExtension, @@ -206,6 +206,60 @@ describe("WalletSelectorModal wallet availability (#103)", () => { // Task 4 — Graceful handling of user signature rejection exceptions // --------------------------------------------------------------------------- +describe("WalletSelectorModal design tokens", () => { + it("uses semantic design token classes for modal shell and alert surfaces", () => { + render(); + + const backdrop = screen.getByTestId("wallet-selector-modal"); + expect(backdrop).toHaveClass("bg-surface-page/80"); + + const content = screen.getByTestId("wallet-selector-modal-content"); + expect(content).toHaveClass( + "bg-surface-card", + "border", + "border-border-subtle", + "text-text-primary" + ); + + const title = screen.getByText("Select Wallet"); + expect(title).toHaveClass("text-text-primary"); + + const warning = screen.getByTestId("wallet-selector-availability-warning"); + expect(warning).toHaveClass( + "bg-warning-soft/10", + "border-warning-soft/40", + "text-warning-soft" + ); + + const errorMessage = screen.getByTestId("wallet-selector-error-message"); + expect(errorMessage).toHaveClass( + "bg-danger/20", + "border-danger/40", + "text-danger-soft" + ); + }); + + it("uses design-token classes for wallet rows and status badges", () => { + render( + + ); + + const selectedOption = screen.getByTestId("wallet-selector-option-freighter"); + expect(selectedOption).toHaveClass( + "border-accent-soft", + "bg-accent/10", + "text-text-primary" + ); + + const connectedBadge = screen.getByTestId("wallet-selector-connected-badge"); + expect(connectedBadge).toHaveClass("text-success-soft"); + }); +}); + describe("WalletSelectorModal signature rejection handling (#105)", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/app/components/WalletSelectorModal.tsx b/app/components/WalletSelectorModal.tsx index 77de9bf..253790a 100644 --- a/app/components/WalletSelectorModal.tsx +++ b/app/components/WalletSelectorModal.tsx @@ -224,14 +224,14 @@ export default function WalletSelectorModal({ data-testid="wallet-selector-modal" role="dialog" aria-label="Select Wallet" - className={`fixed inset-0 z-50 flex items-center justify-center bg-black/60 ${className}`} + className={`fixed inset-0 z-50 flex items-center justify-center bg-surface-page/80 ${className}`} >
-

+

Select Wallet

@@ -249,7 +249,7 @@ export default function WalletSelectorModal({ {networkMismatch.mismatched && mismatchMessage && (
{mismatchMessage} @@ -261,7 +261,7 @@ export default function WalletSelectorModal({

{availability.setupInstruction} @@ -282,7 +282,7 @@ export default function WalletSelectorModal({ {errorMessage && (

{errorMessage} @@ -294,7 +294,7 @@ export default function WalletSelectorModal({
Signature cancelled — you rejected the request in your wallet.
@@ -305,7 +305,7 @@ export default function WalletSelectorModal({
Failed to connect wallet. Please try again.
@@ -315,11 +315,11 @@ export default function WalletSelectorModal({ {effectiveLoading && (
- + Wallet operation in progress…
@@ -350,19 +350,19 @@ export default function WalletSelectorModal({ {activeAddress && (
- - + + {activeAddress.slice(0, 4)}...{activeAddress.slice(-4)}