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
4 changes: 4 additions & 0 deletions src/components/MarketplaceFilter/MarketplaceFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ const MarketplaceFilters = ({
/>
{/* Draggable handle */}
<input
aria-label="Maximum price"
type="range"
min="0"
max="1000000"
Expand Down Expand Up @@ -221,6 +222,7 @@ const MarketplaceFilters = ({
}}
/>
<input
aria-label="Maximum duration remaining"
type="range"
min="0"
max="90"
Expand Down Expand Up @@ -254,6 +256,7 @@ const MarketplaceFilters = ({
<div className="relative h-1.5 bg-white/10 rounded-full">
<div className="absolute h-1.5 bg-[#4A6B8A] rounded-full" style={{ width: `${localFilters.minCompliance}%` }} />
<input
aria-label="Minimum compliance score"
type="range"
min="0"
max="100"
Expand Down Expand Up @@ -284,6 +287,7 @@ const MarketplaceFilters = ({
<div className="relative h-1.5 bg-white/10 rounded-full">
<div className="absolute h-1.5 bg-[#4A6B8A] rounded-full" style={{ width: `${localFilters.maxLoss}%` }} />
<input
aria-label="Maximum loss threshold"
type="range"
min="0"
max="100"
Expand Down
89 changes: 89 additions & 0 deletions tests/components/MarketplaceFilters.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// @vitest-environment happy-dom

import '@testing-library/jest-dom/vitest';
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import MarketplaceFilters from '../../src/components/MarketplaceFilter/MarketplaceFilters';

const defaultFilters = {
sortBy: 'price',
commitmentType: ['balanced' as const],
priceRange: [0, 1000000] as [number, number],
durationRange: [0, 90] as [number, number],
minCompliance: 0,
maxLoss: 100,
};

describe('MarketplaceFilters', () => {
it('renders accessible filter controls and tracks sidebar search text', () => {
render(<MarketplaceFilters filters={defaultFilters} />);

const searchInput = screen.getByPlaceholderText('Search filters...');
fireEvent.change(searchInput, { target: { value: 'balanced only' } });

expect(searchInput).toHaveValue('balanced only');
expect(screen.getByRole('button', { name: /balanced/i })).toHaveAttribute(
'aria-pressed',
'true',
);
expect(screen.getByLabelText('Maximum price')).toBeInTheDocument();
expect(screen.getByLabelText('Maximum duration remaining')).toBeInTheDocument();
expect(screen.getByLabelText('Minimum compliance score')).toBeInTheDocument();
expect(screen.getByLabelText('Maximum loss threshold')).toBeInTheDocument();
});

it('emits updated filters for type toggles, ranges, and reset', () => {
const onFilterChange = vi.fn();
render(
<MarketplaceFilters
filters={{
...defaultFilters,
commitmentType: ['balanced', 'aggressive'],
priceRange: [0, 250000],
durationRange: [0, 30],
minCompliance: 25,
maxLoss: 60,
}}
onFilterChange={onFilterChange}
/>,
);

fireEvent.click(screen.getByRole('button', { name: /safe/i }));
expect(onFilterChange).toHaveBeenLastCalledWith(
expect.objectContaining({
commitmentType: ['balanced', 'aggressive', 'conservative'],
}),
);

fireEvent.change(screen.getByLabelText('Maximum price'), {
target: { value: '500000' },
});
expect(onFilterChange).toHaveBeenLastCalledWith(
expect.objectContaining({ priceRange: [0, 500000] }),
);

fireEvent.change(screen.getByLabelText('Maximum duration remaining'), {
target: { value: '45' },
});
expect(onFilterChange).toHaveBeenLastCalledWith(
expect.objectContaining({ durationRange: [0, 45] }),
);

fireEvent.change(screen.getByLabelText('Minimum compliance score'), {
target: { value: '70' },
});
expect(onFilterChange).toHaveBeenLastCalledWith(
expect.objectContaining({ minCompliance: 70 }),
);

fireEvent.change(screen.getByLabelText('Maximum loss threshold'), {
target: { value: '35' },
});
expect(onFilterChange).toHaveBeenLastCalledWith(
expect.objectContaining({ maxLoss: 35 }),
);

fireEvent.click(screen.getByRole('button', { name: /reset filters/i }));
expect(onFilterChange).toHaveBeenLastCalledWith(defaultFilters);
});
});
74 changes: 74 additions & 0 deletions tests/components/MarketplaceGrid.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// @vitest-environment happy-dom

import '@testing-library/jest-dom/vitest';
import { render, screen, within } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { MarketplaceGrid } from '../../src/components/MarketplaceGrid';
import { MarketplaceGridSkeleton } from '../../src/components/MarketplaceGridSkeleton';
import type { MarketplaceCardProps } from '../../src/components/MarketplaceCard';

vi.mock('../../src/components/modals/CommitmentDetailsModal', () => ({
CommitmentDetailsModal: () => null,
}));

const listings: MarketplaceCardProps[] = [
{
id: '7',
type: 'Balanced',
score: 91,
amount: '$10,000',
duration: '45 days',
yield: '8.2%',
maxLoss: '12%',
owner: 'GABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890',
price: '$1,250',
forSale: true,
},
{
id: '8',
type: 'Safe',
score: 87,
amount: '$5,000',
duration: '30 days',
yield: '4.5%',
maxLoss: '5%',
owner: 'GSHORT',
price: '$900',
forSale: false,
},
];

describe('MarketplaceGrid', () => {
it('renders marketplace listing cards with trade and unavailable states', () => {
render(<MarketplaceGrid items={listings} />);

const grid = screen.getByRole('region', { name: /marketplace listings/i });
expect(within(grid).getByRole('article', { name: /commitment 7/i })).toBeInTheDocument();
expect(within(grid).getByRole('article', { name: /commitment 8/i })).toBeInTheDocument();
expect(screen.getByText('#CMT-007')).toBeInTheDocument();
expect(screen.getByText('$10,000')).toBeInTheDocument();
expect(screen.getByRole('link', { name: /trade 7/i })).toHaveAttribute(
'href',
'/marketplace/trade?id=7',
);
expect(screen.getByText('Not for sale')).toBeInTheDocument();
});

it('renders an empty state when no listings match the filters', () => {
render(<MarketplaceGrid items={[]} />);

expect(screen.getByRole('region', { name: /marketplace listings/i })).toBeInTheDocument();
expect(screen.getByText('No commitments available')).toBeInTheDocument();
expect(
screen.getByText('New offers will appear here once they are listed.'),
).toBeInTheDocument();
});

it('renders the marketplace loading skeleton with the requested card count', () => {
render(<MarketplaceGridSkeleton showFilters={false} cardCount={3} />);

expect(screen.getByRole('status', { name: /loading marketplace listings/i })).toBeInTheDocument();
const skeletonGrid = screen.getByRole('region', { name: /marketplace listings/i });
expect(within(skeletonGrid).getAllByRole('listitem')).toHaveLength(3);
});
});