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
171 changes: 171 additions & 0 deletions src/__tests__/FormInputs.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { render, screen } from '@testing-library/react';
import {
TextInput,
NumberInput,
Textarea,
Select,
DatePicker,
Toggle,
Checkbox,
} from '@/components/FormInputs';

describe('FormInputs - aria-describedby', () => {
describe('TextInput', () => {
it('sets aria-describedby pointing to error element', () => {
render(<TextInput id="email" label="Email" error="Required" />);
const input = screen.getByRole('textbox');
expect(input).toHaveAttribute('aria-describedby', 'email-error');
expect(screen.getByText('Required')).toHaveAttribute('id', 'email-error');
});

it('sets aria-describedby pointing to helper text element', () => {
render(<TextInput id="email" label="Email" helperText="Your email address" />);
const input = screen.getByRole('textbox');
expect(input).toHaveAttribute('aria-describedby', 'email-helper');
expect(screen.getByText('Your email address')).toHaveAttribute('id', 'email-helper');
});

it('does not set aria-describedby when no error or helper text', () => {
render(<TextInput id="email" label="Email" />);
const input = screen.getByRole('textbox');
expect(input).not.toHaveAttribute('aria-describedby');
});

it('sets aria-invalid when error is present', () => {
render(<TextInput id="email" label="Email" error="Invalid email" />);
expect(screen.getByRole('textbox')).toHaveAttribute('aria-invalid', 'true');
});

it('sets aria-invalid=false when no error', () => {
render(<TextInput id="email" label="Email" />);
expect(screen.getByRole('textbox')).toHaveAttribute('aria-invalid', 'false');
});
});

describe('NumberInput', () => {
it('sets aria-describedby pointing to error element', () => {
render(<NumberInput id="count" label="Count" error="Must be positive" />);
const input = screen.getByRole('spinbutton');
expect(input).toHaveAttribute('aria-describedby', 'count-error');
expect(screen.getByText('Must be positive')).toHaveAttribute('id', 'count-error');
});

it('sets aria-describedby pointing to helper text', () => {
render(<NumberInput id="count" label="Count" helperText="Minimum 1" />);
const input = screen.getByRole('spinbutton');
expect(input).toHaveAttribute('aria-describedby', 'count-helper');
});
});

describe('Textarea', () => {
it('sets aria-describedby pointing to error element', () => {
render(<Textarea id="notes" label="Notes" error="Too long" />);
const textarea = screen.getByRole('textbox');
expect(textarea).toHaveAttribute('aria-describedby', 'notes-error');
expect(screen.getByText('Too long')).toHaveAttribute('id', 'notes-error');
});

it('sets aria-describedby pointing to helper text', () => {
render(<Textarea id="notes" label="Notes" helperText="Max 500 characters" />);
const textarea = screen.getByRole('textbox');
expect(textarea).toHaveAttribute('aria-describedby', 'notes-helper');
});
});

describe('Select', () => {
const options = [
{ value: 'a', label: 'Option A' },
{ value: 'b', label: 'Option B' },
];

it('sets aria-describedby pointing to error element', () => {
render(<Select id="choice" label="Choice" options={options} error="Invalid" />);
const select = screen.getByRole('combobox');
expect(select).toHaveAttribute('aria-describedby', 'choice-error');
expect(screen.getByText('Invalid')).toHaveAttribute('id', 'choice-error');
});

it('sets aria-describedby pointing to helper text', () => {
render(<Select id="choice" label="Choice" options={options} helperText="Pick one" />);
const select = screen.getByRole('combobox');
expect(select).toHaveAttribute('aria-describedby', 'choice-helper');
});
});

describe('DatePicker', () => {
it('sets aria-describedby pointing to error element', () => {
render(<DatePicker id="date" label="Date" error="Invalid date" />);
const input = screen.getByRole('textbox');
expect(input).toHaveAttribute('aria-describedby', 'date-error');
expect(screen.getByText('Invalid date')).toHaveAttribute('id', 'date-error');
});

it('sets aria-describedby pointing to helper text', () => {
render(<DatePicker id="date" label="Date" helperText="YYYY-MM-DD" />);
const input = screen.getByRole('textbox');
expect(input).toHaveAttribute('aria-describedby', 'date-helper');
});
});

describe('Toggle', () => {
it('sets aria-describedby pointing to error element', () => {
render(
<Toggle
id="enabled"
label="Enabled"
checked={false}
onChange={() => {}}
error="Must be enabled"
/>
);
const button = screen.getByRole('switch');
expect(button).toHaveAttribute('aria-describedby', 'enabled-error');
expect(screen.getByText('Must be enabled')).toHaveAttribute('id', 'enabled-error');
});

it('sets aria-describedby pointing to helper text', () => {
render(
<Toggle
id="enabled"
label="Enabled"
checked={false}
onChange={() => {}}
helperText="Turn this on to proceed"
/>
);
const button = screen.getByRole('switch');
expect(button).toHaveAttribute('aria-describedby', 'enabled-helper');
});
});

describe('Checkbox', () => {
it('sets aria-describedby pointing to error element', () => {
render(
<Checkbox
id="accept"
label="Accept"
checked={false}
onChange={() => {}}
error="You must accept"
/>
);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toHaveAttribute('aria-describedby', 'accept-error');
expect(screen.getByText('You must accept')).toHaveAttribute('id', 'accept-error');
});

it('sets aria-describedby pointing to helper text', () => {
render(
<Checkbox
id="accept"
label="Accept"
checked={false}
onChange={() => {}}
helperText="I agree to the terms"
/>
);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toHaveAttribute('aria-describedby', 'accept-helper');
});
});
});
6 changes: 6 additions & 0 deletions src/__tests__/TextInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,10 @@ describe('TextInput', () => {
render(<TextInput label="Email" />);
expect(screen.queryByRole('paragraph')).not.toBeInTheDocument();
});

it('has visible focus ring on keyboard focus', () => {
render(<TextInput label="Email" id="email-field" />);
const input = screen.getByRole('textbox');
expect(input).toHaveClass('focus-visible:ring-2', 'focus-visible:ring-indigo-500');
});
});
90 changes: 90 additions & 0 deletions src/__tests__/ToastContainer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { render, screen, waitFor } from '@testing-library/react';
import ToastContainer from '@/components/ToastContainer';

describe('ToastContainer', () => {
beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.runOnlyPendingTimers();
jest.useRealTimers();
});

it('renders no toasts when none are added', () => {
const { container } = render(<ToastContainer />);
expect(container.firstChild).toBeNull();
});

it('deduplicates identical toasts fired in rapid succession', () => {
render(<ToastContainer />);

const addToast = (window as any).__toastContainer?.addToast;
if (!addToast) throw new Error('addToast not exposed');

addToast('Saved!', 'success');
addToast('Saved!', 'success');
addToast('Saved!', 'success');

const toastMessages = screen.getAllByText('Saved!');
expect(toastMessages).toHaveLength(1);
});

it('does not deduplicate toasts with different messages', () => {
render(<ToastContainer />);

const addToast = (window as any).__toastContainer?.addToast;
if (!addToast) throw new Error('addToast not exposed');

addToast('Saved!', 'success');
addToast('Error!', 'error');

expect(screen.getByText('Saved!')).toBeInTheDocument();
expect(screen.getByText('Error!')).toBeInTheDocument();
});

it('does not deduplicate toasts with different types', () => {
render(<ToastContainer />);

const addToast = (window as any).__toastContainer?.addToast;
if (!addToast) throw new Error('addToast not exposed');

addToast('Updated', 'success');
addToast('Updated', 'error');

const toastMessages = screen.getAllByText('Updated');
expect(toastMessages).toHaveLength(2);
});

it('resets auto-dismiss timer when duplicate toast is added', () => {
render(<ToastContainer />);

const addToast = (window as any).__toastContainer?.addToast;
if (!addToast) throw new Error('addToast not exposed');

addToast('Saving...', 'info');

jest.advanceTimersByTime(4000);
expect(screen.getByText('Saving...')).toBeInTheDocument();

addToast('Saving...', 'info');
jest.advanceTimersByTime(4000);
expect(screen.getByText('Saving...')).toBeInTheDocument();

jest.advanceTimersByTime(1000);
expect(screen.queryByText('Saving...')).not.toBeInTheDocument();
});

it('auto-dismisses toasts after 5 seconds', () => {
render(<ToastContainer />);

const addToast = (window as any).__toastContainer?.addToast;
if (!addToast) throw new Error('addToast not exposed');

addToast('Processed', 'success');
expect(screen.getByText('Processed')).toBeInTheDocument();

jest.advanceTimersByTime(5000);
expect(screen.queryByText('Processed')).not.toBeInTheDocument();
});
});
120 changes: 120 additions & 0 deletions src/__tests__/WalletConnect.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { render, screen } from '@testing-library/react';
import WalletConnect from '@/components/WalletConnect';
import { useWalletContext } from '@/contexts/WalletContext';

jest.mock('@/contexts/WalletContext');

describe('WalletConnect', () => {
const mockUseWalletContext = useWalletContext as jest.MockedFunction<typeof useWalletContext>;
const defaultMockReturn = {
address: null,
walletType: null,
connecting: false,
error: null,
freighterInstalled: null,
connect: jest.fn(),
disconnect: jest.fn(),
};

beforeEach(() => {
jest.clearAllMocks();
mockUseWalletContext.mockReturnValue(defaultMockReturn);
});

describe('when Freighter is not installed', () => {
beforeEach(() => {
mockUseWalletContext.mockReturnValue({
...defaultMockReturn,
freighterInstalled: false,
});
});

it('shows install prompt message', () => {
render(<WalletConnect />);
expect(screen.getByText(/Freighter isn't installed/i)).toBeInTheDocument();
});

it('renders link to Freighter installation page', () => {
render(<WalletConnect />);
const link = screen.getByRole('link', { name: /Install Freighter/i });
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute('href', 'https://www.freighter.app/');
expect(link).toHaveAttribute('target', '_blank');
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
});

it('does not render connect buttons', () => {
render(<WalletConnect />);
expect(screen.queryByRole('button', { name: /Connect/i })).not.toBeInTheDocument();
});
});

describe('when Freighter is installed and disconnected', () => {
beforeEach(() => {
mockUseWalletContext.mockReturnValue({
...defaultMockReturn,
freighterInstalled: true,
});
});

it('renders connect buttons', () => {
render(<WalletConnect />);
expect(screen.getByRole('button', { name: /Connect with Freighter/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Connect with WalletConnect/i })).toBeInTheDocument();
});

it('does not show install prompt', () => {
render(<WalletConnect />);
expect(screen.queryByText(/Freighter isn't installed/i)).not.toBeInTheDocument();
});
});

describe('when connected', () => {
beforeEach(() => {
mockUseWalletContext.mockReturnValue({
...defaultMockReturn,
address: 'GBUQWP3BOUZX34ULNQG23RQ6F4OYFBB5L6CSTX32G37B5HMXVY5Z5AAA',
walletType: 'freighter',
freighterInstalled: true,
});
});

it('does not show install prompt', () => {
render(<WalletConnect />);
expect(screen.queryByText(/Freighter isn't installed/i)).not.toBeInTheDocument();
});

it('does not show connect buttons', () => {
render(<WalletConnect />);
expect(screen.queryByRole('button', { name: /Connect/i })).not.toBeInTheDocument();
});

it('shows address badge and disconnect button', () => {
render(<WalletConnect />);
expect(screen.getByText(/^GBU.*AAA$/)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Disconnect/i })).toBeInTheDocument();
});
});

describe('compact mode', () => {
it('renders compact connect button when disconnected', () => {
mockUseWalletContext.mockReturnValue({
...defaultMockReturn,
freighterInstalled: true,
});

render(<WalletConnect compact />);
expect(screen.getByRole('button', { name: /Connect Wallet/i })).toBeInTheDocument();
});

it('shows install prompt in compact mode when Freighter not installed', () => {
mockUseWalletContext.mockReturnValue({
...defaultMockReturn,
freighterInstalled: false,
});

render(<WalletConnect compact />);
expect(screen.getByText(/Freighter isn't installed/i)).toBeInTheDocument();
});
});
});
Loading