From f89a1fd106dfaa2ce65340f5998efadd8aebac49 Mon Sep 17 00:00:00 2001 From: ezekiel Date: Fri, 16 Oct 2026 07:46:40 +0100 Subject: [PATCH] test(ngo-admin): cover page states --- src/app/ngo-admin/page.test.tsx | 117 +++++++++++++++++++++++++++++--- 1 file changed, 108 insertions(+), 9 deletions(-) diff --git a/src/app/ngo-admin/page.test.tsx b/src/app/ngo-admin/page.test.tsx index 83d7b2d..7ca66d7 100644 --- a/src/app/ngo-admin/page.test.tsx +++ b/src/app/ngo-admin/page.test.tsx @@ -1,6 +1,8 @@ import { render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { Stream } from '@/lib/api'; + import NgoAdminPage from './page'; const NGO_ADDRESS = 'G' + 'N'.repeat(55); @@ -10,8 +12,10 @@ const { lookupNgoByAddress, getStreams } = vi.hoisted(() => ({ getStreams: vi.fn(), })); +let currentAddress: string | null = NGO_ADDRESS; + vi.mock('@/components/wallet/WalletProvider', () => ({ - useWallet: () => ({ address: NGO_ADDRESS }), + useWallet: () => ({ address: currentAddress }), })); vi.mock('@/components/toast/ToastProvider', () => ({ @@ -20,18 +24,44 @@ vi.mock('@/components/toast/ToastProvider', () => ({ vi.mock('@/lib/api', () => ({ lookupNgoByAddress, getStreams })); -// Everything that isn't under test is stubbed out; EmbedSnippet stays real -// so the test counts what a verified NGO actually sees. +vi.mock('@/lib/stellar', () => ({ + getNativeAssetAddress: () => 'CNATIVE', + getUsdcAssetAddress: () => 'CUSDC', + DONATION_VAULT_CONTRACT_ID: 'CVAULT', +})); + vi.mock('@/components/layout/Header', () => ({ Header: () => null })); vi.mock('@/components/layout/Footer', () => ({ Footer: () => null })); vi.mock('@/components/ngos/DonateQrCode', () => ({ DonateQrCode: () => null })); -vi.mock('@/components/ngoAdmin/NgoAdminStreamList', () => ({ NgoAdminStreamList: () => null })); +vi.mock('@/components/ngoAdmin/NgoAdminStreamList', () => ({ + NgoAdminStreamList: ({ streams }: { streams: Stream[] }) => ( +
Streams count: {streams.length}
+ ), +})); vi.mock('@/components/ngoAdmin/WithdrawalHistory', () => ({ WithdrawalHistory: () => null })); vi.mock('@/components/streams/StreamDetailsModal', () => ({ StreamDetailsModal: () => null })); -describe('NgoAdminPage', () => { +function makeStream(overrides: Partial = {}): Stream { + return { + id: 's1', + onChainId: '1', + tokenAddress: 'CTOKEN', + rate: '100', + balance: '50000000', + withdrawn: '20000000', + status: 'ACTIVE', + createdAt: '2026-01-15T00:00:00.000Z', + updatedAt: '2026-01-15T00:00:00.000Z', + donor: { address: 'GDONOR' }, + ngo: { id: 'ngo-1', name: 'Test NGO', ownerAddress: NGO_ADDRESS }, + ...overrides, + }; +} + +describe('NgoAdminPage states', () => { beforeEach(() => { vi.clearAllMocks(); + currentAddress = NGO_ADDRESS; lookupNgoByAddress.mockResolvedValue({ id: 'ngo-1', ownerAddress: NGO_ADDRESS, @@ -41,13 +71,82 @@ describe('NgoAdminPage', () => { getStreams.mockResolvedValue([]); }); - it('renders a single embed snippet for a verified NGO', async () => { + it('renders connect wallet prompt when wallet is not connected', () => { + currentAddress = null; + render(); + + expect( + screen.getByText("Connect your NGO's wallet to manage your streams."), + ).toBeInTheDocument(); + expect(lookupNgoByAddress).not.toHaveBeenCalled(); + expect(screen.queryByTestId('ngo-admin-stream-list')).not.toBeInTheDocument(); + }); + + it('renders not-registered state when no NGO is found for the connected wallet', async () => { + lookupNgoByAddress.mockResolvedValue(null); + render(); + + expect( + await screen.findByText(/this wallet isn't registered as an NGO yet/i), + ).toBeInTheDocument(); + expect(screen.getByRole('link', { name: /apply here/i })).toHaveAttribute('href', '/apply'); + expect(screen.queryByTestId('ngo-admin-stream-list')).not.toBeInTheDocument(); + }); + + it('renders awaiting review state when NGO is registered but unverified', async () => { + lookupNgoByAddress.mockResolvedValue({ + id: 'ngo-1', + ownerAddress: NGO_ADDRESS, + name: 'Hope Org', + verified: false, + }); render(); + expect(await screen.findByText('Your application is awaiting review.')).toBeInTheDocument(); expect( - await screen.findByRole('heading', { name: 'Embed your donate widget' }), + screen.getByText(/We've received the application for Hope Org/i), ).toBeInTheDocument(); - expect(screen.getAllByRole('heading', { name: 'Embed your donate widget' })).toHaveLength(1); - expect(screen.getAllByRole('textbox')).toHaveLength(1); + expect(screen.queryByTestId('ngo-admin-stream-list')).not.toBeInTheDocument(); + }); + + it('renders verified view showing totals and the stream list', async () => { + const streams = [ + makeStream({ + id: 's1', + balance: '50000000', // 5 XLM + withdrawn: '20000000', // 2 XLM + status: 'ACTIVE', + }), + makeStream({ + id: 's2', + balance: '30000000', // 3 XLM + withdrawn: '10000000', // 1 XLM + status: 'CANCELLED', + }), + ]; + getStreams.mockResolvedValue(streams); + + render(); + + expect(await screen.findByText('Managing streams for Test NGO.')).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Embed your donate widget' })).toBeInTheDocument(); + + // Total withdrawn = 2 + 1 = 3 CTOKEN + expect(screen.getByText('Total withdrawn')).toBeInTheDocument(); + expect(screen.getByText('3 CTOKEN')).toBeInTheDocument(); + + // Remaining balance (ACTIVE streams only) = 5 CTOKEN + expect(screen.getByText('Remaining balance')).toBeInTheDocument(); + expect(screen.getByText('5 CTOKEN')).toBeInTheDocument(); + + // Active streams count = 1 + expect(screen.getByText('Active streams')).toBeInTheDocument(); + expect(screen.getByText('1')).toBeInTheDocument(); + + // Stream list + const streamList = screen.getByTestId('ngo-admin-stream-list'); + expect(streamList).toBeInTheDocument(); + expect(streamList).toHaveTextContent('Streams count: 2'); }); }); +