Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions apps/frontend/component/dashboard/EscrowCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import EscrowCard from './EscrowCard';

const mockEscrow: any = {
id: '1',
title: 'Test Escrow',
description: 'Test Description',
amount: '100',
asset: 'XLM',
creatorAddress: 'G...',
counterpartyAddress: 'G1234567890abcdef',
deadline: '2025-12-31T23:59:59Z',
status: 'funded',
createdAt: '2025-01-01T00:00:00Z',
updatedAt: '2025-01-01T00:00:00Z',
};

describe('EscrowCard', () => {
it('renders escrow details correctly', () => {
render(<EscrowCard escrow={mockEscrow} />);

expect(screen.getByText('Test Escrow')).toBeInTheDocument();
expect(screen.getByText('Test Description')).toBeInTheDocument();
expect(screen.getByText('100 XLM')).toBeInTheDocument();
expect(screen.getByText('Funded')).toBeInTheDocument();
});

it('renders correct status colors for funded status', () => {
const { container } = render(<EscrowCard escrow={mockEscrow} />);
const badge = screen.getByText('Funded');
expect(badge).toHaveClass('bg-blue-100');
expect(badge).toHaveClass('text-blue-800');
});

it('renders correct status colors for disputed status', () => {
const disputedEscrow = { ...mockEscrow, status: 'disputed' };
render(<EscrowCard escrow={disputedEscrow} />);
const badge = screen.getByText('Disputed');
expect(badge).toHaveClass('bg-red-100');
expect(badge).toHaveClass('text-red-800');
});

it('shows View Details action for funded status', () => {
render(<EscrowCard escrow={mockEscrow} />);
const link = screen.getByText('View Details');
expect(link).toHaveAttribute('href', '/escrow/1');
});

it('shows Confirm Delivery and Dispute actions for confirmed status', () => {
const confirmedEscrow = { ...mockEscrow, status: 'confirmed' };
render(<EscrowCard escrow={confirmedEscrow} />);

const confirmLink = screen.getByText('Confirm Delivery');
const disputeLink = screen.getByText('Dispute');

expect(confirmLink).toHaveAttribute('href', '/escrow/1/confirm');
expect(disputeLink).toHaveAttribute('href', '/escrow/1/dispute');
});
});
74 changes: 74 additions & 0 deletions apps/frontend/component/escrow/CreateEscrowWizard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import CreateEscrowWizard from './CreateEscrowWizard';
import { isConnected } from '@stellar/freighter-api';

jest.mock('@stellar/freighter-api', () => ({
isConnected: jest.fn(),
getAddress: jest.fn(),
signTransaction: jest.fn(),
}));

jest.mock('next/link', () => {
return ({ children, href }: { children: React.ReactNode, href: string }) => (
<a href={href}>{children}</a>
);
});

describe('CreateEscrowWizard', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('renders the first step by default', () => {
render(<CreateEscrowWizard />);
expect(screen.getByText('Basic Information')).toBeInTheDocument();
});

it('validates current step before moving to the next one', async () => {
render(<CreateEscrowWizard />);
const nextButton = screen.getByText('Next');
fireEvent.click(nextButton);
await waitFor(() => {
expect(screen.getByText('Title must be at least 5 characters')).toBeInTheDocument();
});
});

it('navigates through all steps with valid data', async () => {
const user = userEvent.setup();
render(<CreateEscrowWizard />);

// Step 0: Basic Info
const title = screen.getByLabelText(/Title/i);
const category = screen.getByLabelText(/Category/i);
const description = screen.getByLabelText(/Description/i);

await user.type(title, 'Project Development');
await user.selectOptions(category, 'service');
await user.type(description, 'This is a long enough description for the test.');

await user.click(screen.getByRole('button', { name: /Next/i }));

// Step 1: Parties
await waitFor(() => expect(screen.getByText(/Counterparty Address/i)).toBeInTheDocument());
await user.type(screen.getByLabelText(/Counterparty Address/i), 'GBAH4VETEJSTLXU7I6I7DTH2W57YI6XWUT2C7O7XWS6QW2LWSXUUT2C7');
await user.click(screen.getByRole('button', { name: /Next/i }));

// Step 2: Terms
await waitFor(() => expect(screen.getByText(/Amount/i)).toBeInTheDocument());
await user.type(screen.getByLabelText(/Amount/i), '100');

const dateInput = screen.getByLabelText(/Deadline/i);
const futureDate = new Date();
futureDate.setDate(futureDate.getDate() + 7);
const dateString = futureDate.toISOString().slice(0, 16);
fireEvent.change(dateInput, { target: { value: dateString } });

await user.click(screen.getByRole('button', { name: /Next/i }));

// Step 3: Review
await waitFor(() => expect(screen.getByText(/Review & Confirm/i)).toBeInTheDocument());
expect(screen.getByText('Project Development')).toBeInTheDocument();
});
});
72 changes: 72 additions & 0 deletions apps/frontend/component/wallet/ConnectWalletModal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { ConnectWalletModal } from './ConnectWalletModal';
import { useWallet } from '@/app/contexts/WalletContext';

// Mock useWallet hook
jest.mock('@/app/contexts/WalletContext', () => ({
useWallet: jest.fn(),
}));

describe('ConnectWalletModal', () => {
const mockOnClose = jest.fn();
const mockConnect = jest.fn();
const mockGetAvailableWallets = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
(useWallet as jest.Mock).mockReturnValue({
connect: mockConnect,
getAvailableWallets: mockGetAvailableWallets,
isConnecting: false,
error: null,
});
mockGetAvailableWallets.mockResolvedValue(['freighter']);
});

it('does not render when isOpen is false', () => {
render(<ConnectWalletModal isOpen={false} onClose={mockOnClose} />);
expect(screen.queryByText('Connect Wallet')).not.toBeInTheDocument();
});

it('renders correctly when open', async () => {
render(<ConnectWalletModal isOpen={true} onClose={mockOnClose} />);
expect(screen.getByText('Connect Wallet')).toBeInTheDocument();

await waitFor(() => {
expect(screen.getByText('Freighter')).toBeInTheDocument();
});
});

it('calls connect when a wallet is clicked', async () => {
render(<ConnectWalletModal isOpen={true} onClose={mockOnClose} />);

await waitFor(() => screen.getByText('Freighter'));

fireEvent.click(screen.getByText('Freighter'));

expect(mockConnect).toHaveBeenCalledWith('freighter');
});

it('shows error message if there is an error', () => {
(useWallet as jest.Mock).mockReturnValue({
connect: mockConnect,
getAvailableWallets: mockGetAvailableWallets,
isConnecting: false,
error: 'Failed to connect',
});

render(<ConnectWalletModal isOpen={true} onClose={mockOnClose} />);
expect(screen.getByText('Failed to connect')).toBeInTheDocument();
});

it('calls onClose when close button is clicked', () => {
render(<ConnectWalletModal isOpen={true} onClose={mockOnClose} />);
const closeButton = screen.getByRole('button', { name: '' }); // The X icon button
// Alternatively, find by the SVG class or similar if name is empty
// Let's use the first button which is the close button in our case
const buttons = screen.getAllByRole('button');
fireEvent.click(buttons[0]);
expect(mockOnClose).toHaveBeenCalled();
});
});
122 changes: 122 additions & 0 deletions apps/frontend/components/common/ActivityFeed.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import ActivityFeed from './ActivityFeed';
import { useEvents } from '@/hooks/useEvents';

// Mock useEvents hook
jest.mock('@/hooks/useEvents', () => ({
useEvents: jest.fn(),
}));

// Mock ActivityFeedSkeleton to simplify
jest.mock('../ui/ActivityFeedSkeleton', () => ({
ActivityFeedSkeleton: () => <div data-testid="skeleton">Loading...</div>,
}));

// Mock ActivityItem to simplify
jest.mock('./ActivityItem', () => {
return ({ event }: { event: any }) => <div data-testid="activity-item">{event.title}</div>;
});

// Mock Framer Motion
jest.mock('framer-motion', () => ({
motion: {
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
},
AnimatePresence: ({ children }: any) => <>{children}</>,
}));

describe('ActivityFeed', () => {
const mockRefetch = jest.fn();
const mockFetchNextPage = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
(useEvents as jest.Mock).mockReturnValue({
data: {
pages: [
{
events: [
{ id: '1', title: 'Event 1', type: 'FUNDED' },
{ id: '2', title: 'Event 2', type: 'COMPLETED' },
],
},
],
},
fetchNextPage: mockFetchNextPage,
hasNextPage: true,
isFetchingNextPage: false,
status: 'success',
refetch: mockRefetch,
isFetching: false,
});

// Mock IntersectionObserver
const mockIntersectionObserver = jest.fn();
mockIntersectionObserver.mockReturnValue({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
});
window.IntersectionObserver = mockIntersectionObserver;
});

it('renders events correctly', () => {
render(<ActivityFeed />);
expect(screen.getByText('Live Activity')).toBeInTheDocument();
expect(screen.getAllByTestId('activity-item')).toHaveLength(2);
expect(screen.getByText('Event 1')).toBeInTheDocument();
expect(screen.getByText('Event 2')).toBeInTheDocument();
});

it('shows skeleton when status is pending', () => {
(useEvents as jest.Mock).mockReturnValue({
status: 'pending',
isFetching: true,
});

render(<ActivityFeed />);
expect(screen.getByTestId('skeleton')).toBeInTheDocument();
});

it('shows empty state when no events', () => {
(useEvents as jest.Mock).mockReturnValue({
data: { pages: [{ events: [] }] },
status: 'success',
isFetching: false,
});

render(<ActivityFeed />);
expect(screen.getByText('No activity yet')).toBeInTheDocument();
});

it('changes filter when a filter button is clicked', () => {
render(<ActivityFeed />);

const foundingFilter = screen.getByText('Founding');
fireEvent.click(foundingFilter);

expect(useEvents).toHaveBeenCalledWith(expect.objectContaining({
eventType: 'FUNDED'
}));
});

it('calls refetch when refresh button is clicked', () => {
render(<ActivityFeed />);

// The refresh button is the one with RefreshCw icon
// It's the only other button in the header besides filter buttons
const buttons = screen.getAllByRole('button');
// Filter buttons are 5, Close button/Refresh button depends on structure
// Let's find it by testing for the RefreshCw icon component if possible,
// but icons are often harder to find by text.
// In our case, filters are in the second div, refresh is in the first.

// Let's use the first button that's NOT a filter button if possible or just get index.
// Based on the code: filter buttons come after the zap icon.
// Refresh button is the very first button in the component.
fireEvent.click(buttons[0]);

expect(mockRefetch).toHaveBeenCalled();
});
});
Loading