Skip to content

Commit 7235494

Browse files
authored
Merge pull request floxxih#96 from Wilfred007/Feat-No-frontend-unit
ran frontend tests
2 parents f65fbf7 + f277335 commit 7235494

10 files changed

Lines changed: 2995 additions & 56 deletions

File tree

frontend/package-lock.json

Lines changed: 2390 additions & 54 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"build": "next build",
88
"start": "next start",
99
"lint": "eslint",
10-
"test": "echo \"No tests defined yet\" && exit 0"
10+
"test": "vitest",
11+
"test:coverage": "vitest run --coverage"
1112
},
1213
"dependencies": {
1314
"@hookform/resolvers": "^5.2.2",
@@ -26,12 +27,19 @@
2627
},
2728
"devDependencies": {
2829
"@tailwindcss/postcss": "^4",
30+
"@testing-library/jest-dom": "^6.9.1",
31+
"@testing-library/react": "^16.3.2",
32+
"@testing-library/user-event": "^14.6.1",
2933
"@types/node": "^20",
3034
"@types/react": "^19",
3135
"@types/react-dom": "^19",
36+
"@vitejs/plugin-react": "^5.1.4",
3237
"eslint": "^9",
3338
"eslint-config-next": "16.1.6",
39+
"jsdom": "^28.1.0",
3440
"tailwindcss": "^4",
35-
"typescript": "^5"
41+
"typescript": "^5",
42+
"vitest": "^4.0.18",
43+
"vitest-canvas-mock": "^1.1.3"
3644
}
3745
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
2+
import { BidForm } from './BidForm';
3+
import { vi, describe, it, expect, beforeEach } from 'vitest';
4+
import { usePlaceBid } from '@/hooks/useContract';
5+
import { Auction } from '@/types/contracts';
6+
7+
// Mock the hook
8+
vi.mock('@/hooks/useContract', () => ({
9+
usePlaceBid: vi.fn(),
10+
}));
11+
12+
const mockAuction: Auction = {
13+
auction_id: 1n,
14+
publisher: 'GABC...789',
15+
impression_slot: 'Homepage Banner',
16+
floor_price: 10000000n, // 1 XLM in stroops (10^7)
17+
reserve_price: 12000000n,
18+
start_time: 0n,
19+
end_time: BigInt(Math.floor(Date.now() / 1000) + 3600),
20+
status: 'Open',
21+
winning_bid: null,
22+
winner: null,
23+
bid_count: 0,
24+
};
25+
26+
describe('BidForm', () => {
27+
const mockMutateAsync = vi.fn();
28+
29+
beforeEach(() => {
30+
vi.clearAllMocks();
31+
vi.mocked(usePlaceBid).mockReturnValue({
32+
mutateAsync: mockMutateAsync,
33+
isPending: false,
34+
} as any);
35+
});
36+
37+
it('should render floor price correctly', () => {
38+
render(<BidForm auction={mockAuction} />);
39+
expect(screen.getByText('1 XLM')).toBeDefined();
40+
});
41+
42+
it('should show error if bid is below floor price', async () => {
43+
render(<BidForm auction={mockAuction} />);
44+
45+
const bidInput = screen.getByLabelText(/Bid Amount/i);
46+
const campaignInput = screen.getByLabelText(/Campaign ID/i);
47+
const submitButton = screen.getByText('Submit Bid');
48+
49+
fireEvent.change(bidInput, { target: { value: '0.5' } });
50+
fireEvent.change(campaignInput, { target: { value: '10' } });
51+
fireEvent.click(submitButton);
52+
53+
expect(await screen.findByText(/Minimum bid is 1.0000 XLM/i)).toBeInTheDocument();
54+
expect(mockMutateAsync).not.toHaveBeenCalled();
55+
});
56+
57+
it('should call placeBid with correct parameters on valid submission', async () => {
58+
render(<BidForm auction={mockAuction} />);
59+
60+
const bidInput = screen.getByLabelText(/Bid Amount/i);
61+
const campaignInput = screen.getByLabelText(/Campaign ID/i);
62+
const submitButton = screen.getByText('Submit Bid');
63+
64+
fireEvent.change(bidInput, { target: { value: '2.5' } });
65+
fireEvent.change(campaignInput, { target: { value: '123' } });
66+
67+
mockMutateAsync.mockResolvedValue({ success: true });
68+
69+
fireEvent.click(submitButton);
70+
71+
await waitFor(() => {
72+
expect(mockMutateAsync).toHaveBeenCalledWith({
73+
auctionId: 1n,
74+
campaignId: 123,
75+
amountStroops: 25000000n, // Matches BidForm call to placeBid
76+
});
77+
});
78+
});
79+
80+
it('should show error if submission fails', async () => {
81+
mockMutateAsync.mockRejectedValue(new Error('Insufficent funds'));
82+
83+
render(<BidForm auction={mockAuction} />);
84+
85+
const bidInput = screen.getByLabelText(/Bid Amount/i);
86+
const campaignInput = screen.getByLabelText(/Campaign ID/i);
87+
fireEvent.change(bidInput, { target: { value: '5' } });
88+
fireEvent.change(campaignInput, { target: { value: '123' } });
89+
90+
fireEvent.click(screen.getByText('Submit Bid'));
91+
92+
expect(await screen.findByText(/Insufficent funds/i)).toBeInTheDocument();
93+
});
94+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
2+
import { CampaignForm } from './CampaignForm';
3+
import { vi, describe, it, expect, beforeEach } from 'vitest';
4+
import { useCreateCampaign } from '@/hooks/useContract';
5+
6+
// Mock the hook
7+
vi.mock('@/hooks/useContract', () => ({
8+
useCreateCampaign: vi.fn(),
9+
}));
10+
11+
describe('CampaignForm', () => {
12+
const mockCreateCampaign = vi.fn();
13+
const mockOnSuccess = vi.fn();
14+
15+
beforeEach(() => {
16+
vi.clearAllMocks();
17+
vi.mocked(useCreateCampaign).mockReturnValue({
18+
mutateAsync: mockCreateCampaign,
19+
isPending: false,
20+
} as any);
21+
});
22+
23+
it('should show error if title is missing', async () => {
24+
render(<CampaignForm />);
25+
26+
const submitButton = screen.getByText('Create Campaign');
27+
fireEvent.click(submitButton);
28+
29+
expect(await screen.findByText(/Title is required/i)).toBeInTheDocument();
30+
expect(mockCreateCampaign).not.toHaveBeenCalled();
31+
});
32+
33+
it('should call createCampaign with correct parameters on valid submission', async () => {
34+
render(<CampaignForm onSuccess={mockOnSuccess} />);
35+
36+
fireEvent.change(screen.getByLabelText(/Campaign Title/i), {
37+
target: { value: 'Test Campaign' },
38+
});
39+
fireEvent.change(screen.getByLabelText(/Content ID/i), {
40+
target: { value: 'ipfs://123' },
41+
});
42+
fireEvent.change(screen.getByLabelText(/Total Budget/i), {
43+
target: { value: '100' },
44+
});
45+
46+
mockCreateCampaign.mockResolvedValue(1); // Returns campaign ID
47+
48+
fireEvent.click(screen.getByText('Create Campaign'));
49+
50+
await waitFor(() => {
51+
expect(mockCreateCampaign).toHaveBeenCalledWith(expect.objectContaining({
52+
title: 'Test Campaign',
53+
contentId: 'ipfs://123',
54+
budgetXlm: 100,
55+
}));
56+
expect(mockOnSuccess).toHaveBeenCalledWith(1);
57+
});
58+
});
59+
60+
it('should handle submission error', async () => {
61+
mockCreateCampaign.mockRejectedValue(new Error('Contract call failed'));
62+
63+
render(<CampaignForm />);
64+
65+
fireEvent.change(screen.getByLabelText(/Campaign Title/i), {
66+
target: { value: 'Error Campaign' },
67+
});
68+
fireEvent.change(screen.getByLabelText(/Content ID/i), {
69+
target: { value: 'error' },
70+
});
71+
fireEvent.change(screen.getByLabelText(/Total Budget/i), {
72+
target: { value: '10' },
73+
});
74+
75+
fireEvent.click(screen.getByText('Create Campaign'));
76+
77+
expect(await screen.findByText(/Contract call failed/i)).toBeInTheDocument();
78+
});
79+
});
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { render, screen, fireEvent } from '@testing-library/react';
2+
import { ProposalCard } from './ProposalCard';
3+
import { vi, describe, it, expect } from 'vitest';
4+
import { GovernanceProposal } from '@/types/contracts';
5+
6+
const mockProposal: GovernanceProposal = {
7+
proposal_id: 1n,
8+
proposer: 'GABC...123',
9+
title: 'Upgrade Protocol',
10+
description: 'A detailed description of the upgrade.',
11+
status: 'Active',
12+
votes_for: 1000n,
13+
votes_against: 500n,
14+
votes_abstain: 100n,
15+
created_at: 0n,
16+
voting_ends_at: BigInt(Math.floor(Date.now() / 1000) + 86400 * 3), // 3 days left
17+
};
18+
19+
describe('ProposalCard', () => {
20+
it('should render proposal details correctly', () => {
21+
render(<ProposalCard proposal={mockProposal} />);
22+
23+
expect(screen.getByText('PIP-1')).toBeDefined();
24+
expect(screen.getByText('Upgrade Protocol')).toBeDefined();
25+
expect(screen.getByText('Active')).toBeDefined();
26+
expect(screen.getByText(/62.5% For/i)).toBeDefined(); // 1000 / (1000+500+100) = 0.625
27+
});
28+
29+
it('should call onVote when a vote button is clicked', () => {
30+
const mockOnVote = vi.fn();
31+
render(<ProposalCard proposal={mockProposal} onVote={mockOnVote} />);
32+
33+
const forButton = screen.getByText('For');
34+
fireEvent.click(forButton);
35+
36+
expect(mockOnVote).toHaveBeenCalledWith(1n, 'for');
37+
});
38+
39+
it('should show user vote if already voted', () => {
40+
render(<ProposalCard proposal={mockProposal} userVote="against" />);
41+
42+
expect(screen.getByText(/You voted:/i)).toBeDefined();
43+
expect(screen.getByText('against')).toBeDefined();
44+
expect(screen.queryByText('For')).toBeNull(); // Buttons should be hidden
45+
});
46+
47+
it('should disable buttons when isVoting is true', () => {
48+
const mockOnVote = vi.fn();
49+
render(<ProposalCard proposal={mockProposal} onVote={mockOnVote} isVoting={true} />);
50+
51+
const forButton = screen.getByText('For');
52+
expect(forButton).toBeInTheDocument();
53+
expect(forButton).toBeDisabled();
54+
});
55+
});
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { renderHook, act } from '@testing-library/react';
2+
import { useCampaign, useCreateCampaign } from './useContract';
3+
import { vi, describe, it, expect, beforeEach } from 'vitest';
4+
import { callReadOnly, callContract } from '@/lib/soroban-client';
5+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
6+
import React from 'react';
7+
import { useWalletStore } from '@/store/wallet-store';
8+
9+
// Mock soroban-client
10+
vi.mock('@/lib/soroban-client', () => ({
11+
callReadOnly: vi.fn(),
12+
callContract: vi.fn(),
13+
u64ToScVal: vi.fn((v) => ({ _type: 'u64', value: v })),
14+
stringToScVal: vi.fn((v) => ({ _type: 'string', value: v })),
15+
i128ToScVal: vi.fn((v) => ({ _type: 'i128', value: v })),
16+
addressToScVal: vi.fn((v) => ({ _type: 'address', value: v })),
17+
}));
18+
19+
const createWrapper = () => {
20+
const queryClient = new QueryClient({
21+
defaultOptions: {
22+
queries: {
23+
retry: false,
24+
},
25+
},
26+
});
27+
return ({ children }: { children: React.ReactNode }) => (
28+
<QueryClientProvider client= { queryClient } >
29+
{ children }
30+
</QueryClientProvider>
31+
);
32+
};
33+
34+
describe('useContract hooks', () => {
35+
beforeEach(() => {
36+
vi.clearAllMocks();
37+
useWalletStore.getState().setAddress('GABC...123');
38+
useWalletStore.getState().setConnected(true);
39+
});
40+
41+
describe('useCampaign', () => {
42+
it('should fetch campaign data successfully', async () => {
43+
const mockCampaign = { id: 1, title: 'Test Campaign' };
44+
vi.mocked(callReadOnly).mockResolvedValue(mockCampaign);
45+
46+
const { result } = renderHook(() => useCampaign(1), {
47+
wrapper: createWrapper(),
48+
});
49+
50+
// Wait for query to finish
51+
await act(async () => {
52+
await new Promise((resolve) => setTimeout(resolve, 0));
53+
});
54+
55+
expect(result.current.data).toEqual(mockCampaign);
56+
expect(result.current.isLoading).toBe(false);
57+
expect(callReadOnly).toHaveBeenCalled();
58+
});
59+
60+
it('should handle fetch error', async () => {
61+
vi.mocked(callReadOnly).mockRejectedValue(new Error('Contract error'));
62+
63+
const { result } = renderHook(() => useCampaign(1), {
64+
wrapper: createWrapper(),
65+
});
66+
67+
await act(async () => {
68+
await new Promise((resolve) => setTimeout(resolve, 0));
69+
});
70+
71+
expect(result.current.error).toBeDefined();
72+
expect(result.current.isError).toBe(true);
73+
});
74+
});
75+
76+
describe('useCreateCampaign', () => {
77+
it('should call contract to create campaign', async () => {
78+
vi.mocked(callContract).mockResolvedValue({ success: true, result: 123 });
79+
80+
const { result } = renderHook(() => useCreateCampaign(), {
81+
wrapper: createWrapper(),
82+
});
83+
84+
await act(async () => {
85+
await result.current.createCampaign({
86+
title: 'New Campaign',
87+
budgetXlm: 100,
88+
dailyBudgetXlm: 10,
89+
durationDays: 30,
90+
contentId: 'ipfs://...',
91+
campaignType: 1,
92+
});
93+
});
94+
95+
expect(callContract).toHaveBeenCalled();
96+
});
97+
});
98+
});

0 commit comments

Comments
 (0)