diff --git a/.gitignore b/.gitignore index 06210c362..ea8e40e35 100644 --- a/.gitignore +++ b/.gitignore @@ -49,11 +49,12 @@ next-env.d.ts contracts/target/ **/target/ branch_structure.json +temp_auto_push.bat +temp_interactive_push.bat .npm-cache/ .env .env.local .vercel .env* - # soroban test snapshots (generated by cargo test) /contracts/escrow/test_snapshots/ diff --git a/src/app/dashboard/dashboard.test.tsx b/src/app/dashboard/dashboard.test.tsx new file mode 100644 index 000000000..60a482b4a --- /dev/null +++ b/src/app/dashboard/dashboard.test.tsx @@ -0,0 +1,131 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { render, screen, cleanup } from "@testing-library/react"; +import DashboardPage from "./page"; +import type { Commitment } from "@/lib/commitments"; + +// --------------------------------------------------------------------------- +// Mocks +// --------------------------------------------------------------------------- + +const walletState = { + address: undefined as string | undefined, + balance: "0.0000000", + connected: false, + connect: vi.fn(), + disconnect: vi.fn(), +}; + +vi.mock("@/lib/wallet", () => ({ + useWallet: () => ({ + address: walletState.address, + balance: walletState.balance, + connected: walletState.connected, + connect: walletState.connect, + disconnect: walletState.disconnect, + }), +})); + +const now = new Date("2025-01-01T00:00:00.000Z").getTime(); + +function makeCommitment(overrides: Partial): Commitment { + return { + id: "c-1", + owner: "GBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + asset: "USDC", + amount: 1000, + projectedYield: 100, + createdAt: now - 1000, + maturityAt: now + 86400000, + status: "active", + ...overrides, + } as Commitment; +} + +const ownerA = "GBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; +const ownerB = "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; + +const commitments: Commitment[] = [ + makeCommitment({ id: "a-usdc", owner: ownerA, asset: "USDC", amount: 2500, projectedYield: 250 }), + makeCommitment({ id: "a-xlm", owner: ownerA, asset: "XLM", amount: 10000, projectedYield: 1000 }), + makeCommitment({ id: "b-usdc", owner: ownerB, asset: "USDC", amount: 7500, projectedYield: 750 }), + makeCommitment({ id: "demo-1", owner: undefined as unknown as string, asset: "EURC", amount: 500, projectedYield: 50 }), +]; + +vi.mock("@/lib/commitments", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadCommitments: vi.fn(async () => commitments), + }; +}); + +function setWallet(address?: string) { + if (address) { + walletState.address = address; + walletState.connected = true; + } else { + walletState.address = undefined; + walletState.connected = false; + } +} + +beforeEach(() => { + cleanup(); + setWallet(undefined); +}); + +describe("Dashboard owner scoping", () => { + it("shows only unowned demo items when disconnected", async () => { + setWallet(undefined); + render(); + + // Demo group visible + expect(await screen.findByText(/demo/i)).toBeVisible(); + + // Owned rows not visible + expect(screen.queryByText(/a-usdc/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/b-usdc/i)).not.toBeInTheDocument(); + }); + + it("shows owner A commitments plus demo group", async () => { + setWallet(ownerA); + render(); + + expect(await screen.findByText(/a-usdc/i)).toBeVisible(); + expect(await screen.findByText(/a-xlm/i)).toBeVisible(); + expect(await screen.findByText(/demo/i)).toBeVisible(); + + // Owner B rows not visible + expect(screen.queryByText(/b-usdc/i)).not.toBeInTheDocument(); + }); + + it("updates the list when switching to owner B", async () => { + setWallet(ownerB); + render(); + + expect(await screen.findByText(/b-usdc/i)).toBeVisible(); + expect(await screen.findByText(/demo/i)).toBeVisible(); + + expect(screen.queryByText(/a-usdc/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/a-xlm/i)).not.toBeInTheDocument(); + }); + + it("never adds different assets into one total", async () => { + setWallet(ownerA); + render(); + + // Per-asset totals are rendered with units. + expect(await screen.findByText(/\b2,?500\s*USDC\b/i)).toBeVisible(); + expect(await screen.findByText(/\b10,000\s*XLM\b/i)).toBeVisible(); + + // The meaningless cross-asset sum must not appear. + expect(screen.queryByText(/\b12,500\b/)).not.toBeInTheDocument(); + }); + + it("always shows the Ready to settle tile", async () => { + setWallet(undefined); + render(); + + expect(await screen.findByText(/Ready to settle/i)).toBeVisible(); + }); +}); diff --git a/src/app/globals.css b/src/app/globals.css index 5bd01823d..9c48b5fe7 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -125,35 +125,18 @@ h2:focus { outline: none; } .doc-body h1 { font-size: 2.4rem; } .doc-body h2 { margin-top: 2.4rem; padding-top: 1rem; border-top: 1px solid var(--line); scroll-margin-top: 80px; } .doc-body p, .doc-body li { color: #cbd2e6; } -pre { background: var(--bg2); border: 1px solid var(--line); border-radius: 12px; padding: 16px; overflow-x: auto; } +pre { overflow-x: auto; padding: 12px; background: var(--bg2); border-radius: 10px; } +.stats-per-asset { display: flex; flex-direction: column; gap: 2px; margin: 4px 0 0; font-weight: 700; font-size: 1.1rem; } +.stats-per-asset > span { white-space: nowrap; } +.group-heading { margin: 24px 0 8px; font-size: .85rem; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; } .table-wrap { overflow-x: auto; } table { width: 100%; border-collapse: collapse; } th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--line); } th { color: var(--muted); font-size: .8rem; text-transform: uppercase; letter-spacing: .05em; } td code { word-break: break-all; } -@media (max-width: 800px) { - .grid3, .grid4, .stats { grid-template-columns: 1fr 1fr; } - .docs { grid-template-columns: minmax(0, 1fr); } - .toc { position: static; flex-direction: row; flex-wrap: wrap; gap: 12px; } - .nav { margin-left: auto; } - .nav-link { padding: 8px 8px; } -} -@media (max-width: 560px) { - .grid3, .grid4, .stats { grid-template-columns: 1fr; } - .header-inner { gap: 6px; } - .brand { font-size: 0; gap: 0; } - .btn { padding: 8px 12px; white-space: nowrap; } - .footer-links { flex-wrap: wrap; } - .nav { margin-right: 0; gap: 0; } - .nav-link { padding: 8px 6px; font-size: .88rem; } - .header-inner { gap: 4px; } - .header .btn { padding: 8px 10px; font-size: .85rem; } -} - .doc-body { min-width: 0; overflow-wrap: break-word; } .doc-body th, .doc-body td { overflow-wrap: normal; word-break: normal; } .doc-body td code { word-break: break-all; } -@media (max-width: 560px) { th, td { padding: 8px 6px; font-size: .88rem; } th { font-size: .7rem; letter-spacing: .02em; } } .btn { white-space: nowrap; } .row.between.wrap { gap: 12px 16px; margin-bottom: 24px; } @@ -248,3 +231,35 @@ td code { word-break: break-all; } margin: 16px 0 24px; text-align: left; } +@media (max-width: 800px) { + .grid3, .grid4, .stats { grid-template-columns: 1fr 1fr; } + .docs { grid-template-columns: minmax(0, 1fr); } + .toc { position: static; flex-direction: row; flex-wrap: wrap; gap: 12px; } + .nav { margin-left: auto; } + .nav-link { padding: 8px 8px; } +} +@media (max-width: 640px) { + .stats { grid-template-columns: repeat(2, 1fr); } + .grid3 { grid-template-columns: 1fr; } + .grid4 { grid-template-columns: repeat(2, 1fr); } + .docs { grid-template-columns: 1fr; } + .toc { position: static; } +} +@media (max-width: 560px) { + .grid3, .grid4, .stats { grid-template-columns: 1fr; } + .header-inner { gap: 6px; } + .brand { font-size: 0; gap: 0; } + .btn { padding: 8px 12px; white-space: nowrap; } + .footer-links { flex-wrap: wrap; } + .nav { margin-right: 0; gap: 0; } + .nav-link { padding: 8px 6px; font-size: .88rem; } + .header-inner { gap: 4px; } + .header .btn { padding: 8px 10px; font-size: .85rem; } + th, td { padding: 8px 6px; font-size: .88rem; } + th { font-size: .7rem; letter-spacing: .02em; } +} +@media (max-width: 390px) { + .stats { grid-template-columns: 1fr; } + .grid4 { grid-template-columns: 1fr; } + .container { padding: 0 12px; } +} \ No newline at end of file