Skip to content

Commit cc98dac

Browse files
authored
Merge pull request #164 from therealjhay/fix/issue#85386#87
Fix/issue#85386#87
2 parents fa92ccf + 76d9dfc commit cc98dac

22 files changed

Lines changed: 1214 additions & 13 deletions

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
name: CI Build & Verify
22

33
on:
4+
workflow_dispatch:
45
push:
56
branches: [ main ]
67
pull_request:

apps/web/app/invoice/[invoiceId]/InvoiceDetailClient.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import React, { useEffect, useMemo, useState } from 'react';
4-
import { useParams, useRouter } from 'next/navigation';
4+
import { useRouter } from 'next/navigation';
55
import { PageLayout } from '@/components/shared/PageLayout';
66
import { useInvoice, useInvoices } from '@/hooks/useInvoices';
77
import { useWalletStore } from '@/store/wallet';

apps/web/app/invoice/[invoiceId]/page.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import type { Metadata } from 'next';
2-
import { Inter } from 'next/font/google';
32
import { Invoice } from '@/types';
43
import { formatAmount } from '@/lib/assets';
54
import InvoiceDetailClient from './InvoiceDetailClient';
65

7-
const inter = Inter({ subsets: ['latin'] });
8-
96
const getIndexerApiUrl = () => {
107
return process.env.NEXT_PUBLIC_INDEXER_API_URL || 'http://localhost:8080';
118
};
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import { describe, it, expect, vi } from 'vitest';
4+
import { InvoiceCard } from './InvoiceCard';
5+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
6+
7+
// Mock the hooks
8+
vi.mock('@/store/wallet', () => ({
9+
useWalletStore: vi.fn(() => ({ address: 'GACR43ILX6H4PGAOO5QKSZLU4ZJMGT3E66EAUDPLM5J6YTP4Y3PSHWGB' }))
10+
}));
11+
12+
vi.mock('@/hooks/useProfile', () => ({
13+
useProfile: vi.fn(() => ({ isVerified: true }))
14+
}));
15+
16+
const mockInvoice = {
17+
id: 'abcd',
18+
status: 'Created',
19+
issuer: 'GACR43ILX6H4PGAOO5QKSZLU4ZJMGT3E66EAUDPLM5J6YTP4Y3PSHWGB',
20+
buyer: 'GACR43ILX6H4PGAOO5QKSZLU4ZJMGT3E66EAUDPLM5J6YTP4Y3PSHWGB',
21+
faceValue: 10000000000n, // 1000.00 USDC
22+
dueDate: 1234567890
23+
};
24+
25+
const queryClient = new QueryClient();
26+
27+
const renderWithQueryClient = (ui: React.ReactElement) => {
28+
return render(
29+
<QueryClientProvider client={queryClient}>
30+
{ui}
31+
</QueryClientProvider>
32+
);
33+
};
34+
35+
describe('InvoiceCard', () => {
36+
it('renders invoice details correctly', () => {
37+
renderWithQueryClient(<InvoiceCard invoice={mockInvoice as any} />);
38+
expect(screen.getByText(/1,000.00 USDC/)).toBeInTheDocument();
39+
});
40+
41+
it('renders correct action buttons for created status (issuer)', () => {
42+
renderWithQueryClient(<InvoiceCard invoice={{...mockInvoice, status: 'Created'} as any} role="issuer" />);
43+
// Issuer can list for financing when created
44+
expect(screen.getByText(/Configure financing terms/i)).toBeInTheDocument();
45+
});
46+
47+
it('renders correct action buttons for listed status (lp)', () => {
48+
renderWithQueryClient(<InvoiceCard invoice={{...mockInvoice, status: 'Listed'} as any} role="lp" />);
49+
// LP can fund when listed
50+
expect(screen.getByText(/FUND INVOICE/i)).toBeInTheDocument();
51+
});
52+
53+
it('renders correct action buttons for funded status (issuer)', () => {
54+
renderWithQueryClient(<InvoiceCard invoice={{...mockInvoice, status: 'Funded'} as any} role="issuer" />);
55+
// Issuer can mark shipped when funded
56+
expect(screen.getByText(/MARK GOODS SHIPPED/i)).toBeInTheDocument();
57+
});
58+
59+
it('renders correct action buttons for shipped status (buyer)', () => {
60+
renderWithQueryClient(<InvoiceCard invoice={{...mockInvoice, status: 'Active'} as any} role="buyer" />);
61+
// Buyer can confirm delivery when shipped (in contract it's Active)
62+
expect(screen.getByText(/CONFIRM DELIVERY/i)).toBeInTheDocument();
63+
});
64+
65+
it('renders correct action buttons for delivered status (buyer)', () => {
66+
renderWithQueryClient(<InvoiceCard invoice={{...mockInvoice, status: 'Confirmed'} as any} role="buyer" />);
67+
// Buyer can repay when delivered (in contract it's Confirmed)
68+
expect(screen.getByText(/REPAY INVOICE/i)).toBeInTheDocument();
69+
});
70+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React from 'react';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import { describe, it, expect, vi } from 'vitest';
4+
import { InvoiceForm } from './InvoiceForm';
5+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
6+
7+
vi.mock('@/hooks/useRole', () => ({
8+
useRole: () => ({ role: 'issuer' })
9+
}));
10+
vi.mock('@/hooks/useWallet', () => ({
11+
useWallet: () => ({ address: 'GACR43ILX6H4PGAOO5QKSZLU4ZJMGT3E66EAUDPLM5J6YTP4Y3PSHWGB' })
12+
}));
13+
14+
const queryClient = new QueryClient();
15+
16+
const renderWithQueryClient = (ui: React.ReactElement) => {
17+
return render(
18+
<QueryClientProvider client={queryClient}>
19+
{ui}
20+
</QueryClientProvider>
21+
);
22+
};
23+
24+
describe('InvoiceForm', () => {
25+
it('renders the first step of the wizard', () => {
26+
renderWithQueryClient(<InvoiceForm />);
27+
expect(screen.getByText(/Buyer Wallet Address/i)).toBeInTheDocument();
28+
expect(screen.getByText(/Face Value/i)).toBeInTheDocument();
29+
});
30+
31+
it('validates form fields before proceeding', async () => {
32+
renderWithQueryClient(<InvoiceForm />);
33+
const buyerInput = screen.getByPlaceholderText(/GBBD47IF6L/i);
34+
fireEvent.change(buyerInput, { target: { value: 'invalid_address' } });
35+
36+
const nextButton = screen.getByText(/REVIEW FINANCING TERMS/i);
37+
fireEvent.submit(nextButton);
38+
expect(await screen.findByText(/Buyer must be a valid Stellar public key/i)).toBeInTheDocument();
39+
});
40+
41+
it('proceeds to step 2 when valid and shows simulation state', async () => {
42+
renderWithQueryClient(<InvoiceForm />);
43+
const buyerInput = screen.getByPlaceholderText(/GBBD47IF6L/i);
44+
const valueInput = screen.getByPlaceholderText(/50,000.00/i);
45+
46+
fireEvent.change(buyerInput, { target: { value: 'GACR43ILX6H4PGAOO5QKSZLU4ZJMGT3E66EAUDPLM5J6YTP4Y3PSHWGB' } });
47+
fireEvent.change(valueInput, { target: { value: '1000' } });
48+
49+
const nextButton = screen.getByText(/REVIEW FINANCING TERMS/i);
50+
fireEvent.click(nextButton);
51+
52+
expect(await screen.findByText(/Invoice Face Value/i)).toBeInTheDocument();
53+
});
54+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import { describe, it, expect } from 'vitest';
4+
import { InvoiceStatus } from './InvoiceStatus';
5+
6+
describe('InvoiceStatus', () => {
7+
it('renders correctly for created status', () => {
8+
render(<InvoiceStatus status="created" />);
9+
expect(screen.getByText(/Created/i)).toBeInTheDocument();
10+
});
11+
12+
it('renders correctly for listed status', () => {
13+
render(<InvoiceStatus status="listed" />);
14+
expect(screen.getByText(/Listed/i)).toBeInTheDocument();
15+
});
16+
17+
it('renders correctly for funded status', () => {
18+
render(<InvoiceStatus status="funded" />);
19+
expect(screen.getByText(/Funded/i)).toBeInTheDocument();
20+
});
21+
22+
it('renders correctly for shipped status', () => {
23+
render(<InvoiceStatus status="shipped" />);
24+
expect(screen.getByText(/Shipped/i)).toBeInTheDocument();
25+
});
26+
27+
it('renders correctly for delivered status', () => {
28+
render(<InvoiceStatus status="delivered" />);
29+
expect(screen.getByText(/Delivered/i)).toBeInTheDocument();
30+
});
31+
32+
it('renders correctly for repaid status', () => {
33+
render(<InvoiceStatus status="repaid" />);
34+
expect(screen.getByText(/Repaid/i)).toBeInTheDocument();
35+
});
36+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import { describe, it, expect } from 'vitest';
4+
import { InvoiceTable } from './InvoiceTable';
5+
6+
const mockInvoices = [
7+
{
8+
id: '1',
9+
status: 'created',
10+
issuer: 'GACR43ILX6H4PGAOO5QKSZLU4ZJMGT3E66EAUDPLM5J6YTP4Y3PSHWGB',
11+
buyer: 'GACR43ILX6H4PGAOO5QKSZLU4ZJMGT3E66EAUDPLM5J6YTP4Y3PSHWGB',
12+
faceValue: 10000000000n, // 1000.00 USDC
13+
dueDate: 1234567890
14+
},
15+
{
16+
id: '2',
17+
status: 'Funded',
18+
issuer: 'GACR43ILX6H4PGAOO5QKSZLU4ZJMGT3E66EAUDPLM5J6YTP4Y3PSHWGB',
19+
buyer: 'GACR43ILX6H4PGAOO5QKSZLU4ZJMGT3E66EAUDPLM5J6YTP4Y3PSHWGB',
20+
faceValue: 20000000000n, // 2000.00 USDC
21+
dueDate: 1234567891
22+
}
23+
];
24+
25+
describe('InvoiceTable', () => {
26+
it('renders a list of invoices', () => {
27+
render(<InvoiceTable invoices={mockInvoices as any} />);
28+
expect(screen.getByText(/1,000.00 USDC/)).toBeInTheDocument();
29+
expect(screen.getByText(/2,000.00 USDC/)).toBeInTheDocument();
30+
});
31+
32+
it('renders empty state when no invoices', () => {
33+
render(<InvoiceTable invoices={[]} />);
34+
expect(screen.getByText(/No invoices found/i)).toBeInTheDocument();
35+
});
36+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import { describe, it, expect } from 'vitest';
4+
import { StatusTimeline } from './StatusTimeline';
5+
6+
describe('StatusTimeline', () => {
7+
it('renders all timeline steps', () => {
8+
render(<StatusTimeline status="Created" timestamps={{ created: 1620000000 }} />);
9+
expect(screen.getByText(/Created/i)).toBeInTheDocument();
10+
expect(screen.getByText(/Listed/i)).toBeInTheDocument();
11+
expect(screen.getByText(/Funded/i)).toBeInTheDocument();
12+
expect(screen.getByText(/Active/i)).toBeInTheDocument();
13+
expect(screen.getByText(/Confirmed/i)).toBeInTheDocument();
14+
expect(screen.getByText(/Repaid/i)).toBeInTheDocument();
15+
});
16+
17+
it('marks steps up to currentStatus as active or completed', () => {
18+
const { container } = render(<StatusTimeline status="Funded" timestamps={{ created: 1620000000, listed: 1620000001, funded: 1620000002 }} />);
19+
// Testing specific active/completed visual states would depend on the implementation
20+
// But we ensure it renders without crashing for a mid-way status
21+
expect(container).toBeInTheDocument();
22+
});
23+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import { describe, it, expect } from 'vitest';
4+
import { TransactionPending } from './TransactionPending';
5+
6+
describe('TransactionPending', () => {
7+
it('renders loading state correctly', () => {
8+
render(<TransactionPending isOpen={true} txHash="0x123" statusText="Processing... Please wait" />);
9+
expect(screen.getByText(/Processing... Please wait/i)).toBeInTheDocument();
10+
expect(screen.getByText(/TRANSACTION SIGNED/i)).toBeInTheDocument();
11+
expect(screen.getByText(/0x123/i)).toBeInTheDocument();
12+
});
13+
14+
it('renders empty state correctly (not pending)', () => {
15+
const { container } = render(<TransactionPending isOpen={false} txHash={null} />);
16+
expect(container).toBeEmptyDOMElement();
17+
});
18+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import { describe, it, expect, vi } from 'vitest';
4+
import { WalletConnect } from './WalletConnect';
5+
import { useWallet } from '@/hooks/useWallet';
6+
7+
vi.mock('@/hooks/useWallet', () => {
8+
const state = 'disconnected';
9+
return {
10+
useWallet: vi.fn(() => ({
11+
connected: state === 'connected',
12+
loading: state === 'connecting',
13+
address: state === 'connected' ? 'GACR43ILX6H4PGAOO5QKSZLU4ZJMGT3E66EAUDPLM5J6YTP4Y3PSHWGB' : null,
14+
error: state === 'error' ? 'Connection failed' : null,
15+
connectWallet: vi.fn(),
16+
disconnectWallet: vi.fn(),
17+
}))
18+
};
19+
});
20+
21+
describe('WalletConnect', () => {
22+
it('renders disconnected state', () => {
23+
vi.mocked(useWallet).mockReturnValue({ connected: false, loading: false, address: null, error: null } as any);
24+
render(<WalletConnect />);
25+
expect(screen.getByText(/Connect Wallet/i)).toBeInTheDocument();
26+
});
27+
28+
it('renders connecting state', () => {
29+
vi.mocked(useWallet).mockReturnValue({ connected: false, loading: true, address: null, error: null } as any);
30+
render(<WalletConnect />);
31+
expect(screen.getByText(/Connecting.../i)).toBeInTheDocument();
32+
});
33+
34+
it('renders connected state', () => {
35+
vi.mocked(useWallet).mockReturnValue({ connected: true, loading: false, address: 'GACR43ILX6H4PGAOO5QKSZLU4ZJMGT3E66EAUDPLM5J6YTP4Y3PSHWGBYZ', error: null } as any);
36+
render(<WalletConnect />);
37+
expect(screen.getByText(/GACR43\.\.\.GBYZ/i)).toBeInTheDocument();
38+
});
39+
40+
it('renders error state', () => {
41+
vi.mocked(useWallet).mockReturnValue({ connected: false, loading: false, address: null, error: 'Connection failed' } as any);
42+
render(<WalletConnect />);
43+
expect(screen.getByText(/Connection failed/i)).toBeInTheDocument();
44+
});
45+
});

0 commit comments

Comments
 (0)