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
115 changes: 115 additions & 0 deletions frontend/src/__tests__/hooks/useWalletManager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { renderHook, act, waitFor } from '@testing-library/react';
import { describe, expect, it, vi, beforeEach } from 'vitest';
import { useWalletManager } from '../../hooks/useWalletManager';
import { StellarWalletsKit } from '@creit.tech/stellar-wallets-kit';

vi.mock('@creit.tech/stellar-wallets-kit', () => ({
StellarWalletsKit: vi.fn(function () {
return {};
}),
WalletNetwork: { TESTNET: 'TESTNET', PUBLIC: 'PUBLIC' },
FreighterModule: vi.fn(function () {
return {};
}),
xBullModule: vi.fn(function () {
return {};
}),
LobstrModule: vi.fn(function () {
return {};
}),
FREIGHTER_ID: 'freighter',
LOBSTR_ID: 'lobstr',
}));

vi.mock('../../hooks/useNotification', () => ({
useNotification: () => ({
notifyWalletEvent: vi.fn(),
}),
}));

interface MockKitInstance {
setWallet: ReturnType<typeof vi.fn>;
getAddress: ReturnType<typeof vi.fn>;
getSupportedWallets: ReturnType<typeof vi.fn>;
disconnect: ReturnType<typeof vi.fn>;
signTransaction: ReturnType<typeof vi.fn>;
}

describe('useWalletManager', () => {
let mockKitInstance: MockKitInstance;

beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
mockKitInstance = {
setWallet: vi.fn(),
getAddress: vi.fn(),
getSupportedWallets: vi.fn().mockResolvedValue([]),
disconnect: vi.fn(),
signTransaction: vi.fn(),
};
vi.mocked(StellarWalletsKit).mockImplementation(
() => mockKitInstance as unknown as StellarWalletsKit
);
});

it('initializes and attempts silent reconnect if wallet in localStorage', async () => {
localStorage.setItem('payd:last_wallet_name', 'freighter');
mockKitInstance.getAddress.mockResolvedValue({ address: 'G123' });

const { result } = renderHook(() => useWalletManager());

// Initially connecting
expect(result.current.isConnecting).toBe(true);
expect(result.current.isInitialized).toBe(false);

await waitFor(() => {
expect(result.current.isInitialized).toBe(true);
});

expect(result.current.address).toBe('G123');
expect(result.current.walletName).toBe('freighter');
expect(result.current.isConnecting).toBe(false);
});

it('handles manual connect sequence appropriately', async () => {
mockKitInstance.getSupportedWallets.mockResolvedValue([
{ id: 'freighter', name: 'Freighter', isAvailable: true },
]);

const { result } = renderHook(() => useWalletManager());

await act(async () => {
await result.current.connect();
});

await waitFor(() => {
expect(result.current.walletModalOpen).toBe(true);
});
expect(result.current.walletOptions.length).toBe(1);

mockKitInstance.getAddress.mockResolvedValue({ address: 'G456' });

await act(async () => {
await result.current.connectWithWallet('freighter');
});

await waitFor(() => {
expect(result.current.walletModalOpen).toBe(false);
});
expect(result.current.address).toBe('G456');
expect(result.current.walletName).toBe('freighter');
});

it('handles disconnect', () => {
const { result } = renderHook(() => useWalletManager());

act(() => {
result.current.disconnect();
});

expect(result.current.address).toBeNull();
expect(result.current.walletName).toBeNull();
expect(localStorage.getItem('payd:last_wallet_name')).toBeNull();
});
});
3 changes: 1 addition & 2 deletions frontend/src/components/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import { Breadcrumb } from './Breadcrumb';
import { NetworkSwitcher } from './NetworkSwitcher';
import { useNetworkStore } from '../stores/networkStore';

const APP_VERSION =
(import.meta.env.PUBLIC_APP_VERSION as string | undefined)?.trim() ?? '0.0.1';
const APP_VERSION = (import.meta.env.PUBLIC_APP_VERSION as string | undefined)?.trim() ?? '0.0.1';
const APP_ENV = import.meta.env.MODE;

// ── Page Wrapper ───────────────────────
Expand Down
13 changes: 3 additions & 10 deletions frontend/src/components/Breadcrumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ export function buildCrumbs(pathname: string): Crumb[] {
let accumulated = '';
for (const segment of segments) {
accumulated += `/${segment}`;
const label =
ROUTE_LABELS[segment] ?? segment.charAt(0).toUpperCase() + segment.slice(1);
const label = ROUTE_LABELS[segment] ?? segment.charAt(0).toUpperCase() + segment.slice(1);
crumbs.push({ label, href: accumulated });
}

Expand All @@ -61,15 +60,9 @@ export const Breadcrumb: React.FC = () => {
const isLast = i === crumbs.length - 1;
return (
<React.Fragment key={crumb.href}>
{i > 0 && (
<ChevronRight className="h-3 w-3 shrink-0 opacity-50" aria-hidden />
)}
{i > 0 && <ChevronRight className="h-3 w-3 shrink-0 opacity-50" aria-hidden />}
{isLast ? (
<span
className="font-medium"
style={{ color: 'var(--text)' }}
aria-current="page"
>
<span className="font-medium" style={{ color: 'var(--text)' }} aria-current="page">
{crumb.label}
</span>
) : (
Expand Down
9 changes: 2 additions & 7 deletions frontend/src/components/EmployeeList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,7 @@ export const EmployeeList: React.FC<EmployeeListProps> = ({
</thead>
<tbody className="divide-y divide-gray-200">
{isLoading ? (
Array.from({ length: SKELETON_ROW_COUNT }, (_, i) => (
<EmployeeSkeletonRow key={i} />
))
Array.from({ length: SKELETON_ROW_COUNT }, (_, i) => <EmployeeSkeletonRow key={i} />)
) : sortedEmployees.length === 0 ? (
<tr>
<td colSpan={6} className="p-6 text-center text-gray-500">
Expand All @@ -266,10 +264,7 @@ export const EmployeeList: React.FC<EmployeeListProps> = ({
</tr>
) : (
sortedEmployees.map((employee) => (
<tr
key={employee.id}
className="cursor-pointer transition-colors hover:bg-white/5"
>
<tr key={employee.id} className="cursor-pointer transition-colors hover:bg-white/5">
<td className="p-6">
<div className="flex items-center gap-3">
<Avatar
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/components/__tests__/AppNav.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ vi.mock('../../hooks/useWallet', () => ({
}));

// Dynamic import so mocks are registered first
const importNav = () =>
import('../AppNav').then((m) => m.default);
const importNav = () => import('../AppNav').then((m) => m.default);

describe('AppNav — mobile drawer', () => {
test('hamburger button is present and drawer is hidden initially', async () => {
Expand Down
5 changes: 1 addition & 4 deletions frontend/src/components/__tests__/Breadcrumb.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,7 @@ describe('Breadcrumb component', () => {
</MemoryRouter>
);
expect(screen.getByRole('link', { name: /home/i })).toHaveAttribute('href', '/');
expect(screen.getByRole('link', { name: /employer/i })).toHaveAttribute(
'href',
'/employer'
);
expect(screen.getByRole('link', { name: /employer/i })).toHaveAttribute('href', '/employer');
expect(screen.getByText('Payroll')).toHaveAttribute('aria-current', 'page');
});
});
8 changes: 2 additions & 6 deletions frontend/src/components/__tests__/EmployeeListHover.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ const employee = {

describe('EmployeeList row hover effects', () => {
test('data rows include hover background class', () => {
const { container } = render(
<EmployeeList employees={[employee]} onAddEmployee={vi.fn()} />
);
const { container } = render(<EmployeeList employees={[employee]} onAddEmployee={vi.fn()} />);

const rows = container.querySelectorAll('tbody tr');
expect(rows.length).toBeGreaterThan(0);
Expand All @@ -36,9 +34,7 @@ describe('EmployeeList row hover effects', () => {
});

test('data rows include transition class for smooth hover animation', () => {
const { container } = render(
<EmployeeList employees={[employee]} onAddEmployee={vi.fn()} />
);
const { container } = render(<EmployeeList employees={[employee]} onAddEmployee={vi.fn()} />);

const rows = container.querySelectorAll('tbody tr');
rows.forEach((row) => {
Expand Down
8 changes: 2 additions & 6 deletions frontend/src/components/__tests__/NetworkSwitcher.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ describe('NetworkSwitcher', () => {

test('renders a select element with an accessible label', () => {
render(<NetworkSwitcher />);
expect(
screen.getByRole('combobox', { name: /select stellar network/i })
).toBeInTheDocument();
expect(screen.getByRole('combobox', { name: /select stellar network/i })).toBeInTheDocument();
});

test('shows MAINNET as the default selected option', () => {
Expand Down Expand Up @@ -66,8 +64,6 @@ describe('NetworkSwitcher', () => {

test('wraps select in a group with an accessible label', () => {
render(<NetworkSwitcher />);
expect(
screen.getByRole('group', { name: /stellar network selector/i })
).toBeInTheDocument();
expect(screen.getByRole('group', { name: /stellar network selector/i })).toBeInTheDocument();
});
});
Loading