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
140 changes: 139 additions & 1 deletion components/OrderGrid/__tests__/OrderGrid.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,142 @@ describe('OrderGrid', () => {
expect(OrdersGrid.Body).toBeDefined();
expect(OrdersGrid.defaultColumns).toBe(defaultColumns);
});
});

it('renders sortable header links with active indicator', () => {
render(
<OrdersGrid
orders={[makeOrder({ id: '1', instrument: 'VALE3' })]}
sortState={{
sortBy: 'price',
sortDir: 'desc',
sortLinks: {
price: '/?page=1&sortBy=price&sortDir=asc',
},
}}
/>,
);

const priceSortLink = screen.getByRole('link', { name: 'Ordenar por Preço' });
expect(priceSortLink).toBeInTheDocument();
expect(priceSortLink).toHaveTextContent('▼');
});

it('renders plain text headers when no sortState is provided', () => {
render(
<OrdersGrid orders={[makeOrder({ id: '1', instrument: 'VALE3' })]} />,
);

expect(screen.getByText('Instrumento')).toBeInTheDocument();
expect(screen.queryByRole('link', { name: /Ordenar/ })).not.toBeInTheDocument();
});

it('shows ascending indicator (▲) for active ascending sort column', () => {
render(
<OrdersGrid
orders={[makeOrder({ id: '1' })]}
sortState={{
sortBy: 'price',
sortDir: 'asc',
sortLinks: {
price: '/?page=1&sortBy=price&sortDir=desc',
},
}}
/>,
);

const priceSortLink = screen.getByRole('link', { name: 'Ordenar por Preço' });
expect(priceSortLink).toHaveTextContent('▲');
});

it('shows neutral indicator (↕) for inactive sortable column', () => {
render(
<OrdersGrid
orders={[makeOrder({ id: '1' })]}
sortState={{
sortBy: 'price',
sortDir: 'asc',
sortLinks: {
price: '/?page=1&sortBy=price&sortDir=desc',
instrument: '/?page=1&sortBy=instrument&sortDir=asc',
},
}}
/>,
);

const instrumentLink = screen.getByRole('link', { name: 'Ordenar por Instrumento' });
expect(instrumentLink).toHaveTextContent('↕');
});

it('sets aria-sort descending on active descending column', () => {
const { container } = render(
<OrdersGrid
orders={[makeOrder({ id: '1' })]}
sortState={{
sortBy: 'price',
sortDir: 'desc',
sortLinks: {
price: '/?page=1&sortBy=price&sortDir=asc',
},
}}
/>,
);

const priceHeader = container.querySelector('th[aria-sort="descending"]');
expect(priceHeader).toBeInTheDocument();
});

it('sets aria-sort ascending on active ascending column', () => {
const { container } = render(
<OrdersGrid
orders={[makeOrder({ id: '1' })]}
sortState={{
sortBy: 'instrument',
sortDir: 'asc',
sortLinks: {
instrument: '/?page=1&sortBy=instrument&sortDir=desc',
},
}}
/>,
);

const instrumentHeader = container.querySelector('th[aria-sort="ascending"]');
expect(instrumentHeader).toBeInTheDocument();
});

it('sets aria-sort none on all headers when no sortState is provided', () => {
const { container } = render(
<OrdersGrid orders={[makeOrder({ id: '1' })]} />,
);

const headers = container.querySelectorAll('th');
headers.forEach((th) => {
expect(th).toHaveAttribute('aria-sort', 'none');
});
});

it('applies active CSS class only to the active sort link', () => {
const { container } = render(
<OrdersGrid
orders={[makeOrder({ id: '1' })]}
sortState={{
sortBy: 'price',
sortDir: 'asc',
sortLinks: {
price: '/?page=1&sortBy=price&sortDir=desc',
instrument: '/?page=1&sortBy=instrument&sortDir=asc',
},
}}
/>,
);

const activeLink = container.querySelector('.orders-grid__sort-link--active');
expect(activeLink).not.toBeNull();
expect(activeLink).toHaveAttribute('aria-label', 'Ordenar por Preço');

const allLinks = container.querySelectorAll('.orders-grid__sort-link');
const inactiveLinks = container.querySelectorAll(
'.orders-grid__sort-link:not(.orders-grid__sort-link--active)',
);
expect(inactiveLinks.length).toBe(allLinks.length - 1);
});
});
67 changes: 67 additions & 0 deletions components/OrderGrid/__tests__/order-grid.navigation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
jest.mock('@/lib/orders/orders.sort', () => ({
getDefaultSortDirForField: (field: string) =>
field === 'timestamp' ? 'desc' : 'asc',
}));

import {
buildOrderGridHref,
buildOrderGridSortLinks,
getNextSortDir,
} from '../order-grid.navigation';

describe('order-grid.navigation', () => {
it('builds grid href with page and sorting params', () => {
const href = buildOrderGridHref(3, 'price', 'asc');

expect(href).toBe('/?page=3&sortBy=price&sortDir=asc');
});

it('toggles direction when sorting same field', () => {
expect(getNextSortDir('price', 'asc', 'price')).toBe('desc');
expect(getNextSortDir('price', 'desc', 'price')).toBe('asc');
});

it('uses field default direction when sorting a different field', () => {
expect(getNextSortDir('timestamp', 'desc', 'price')).toBe('asc');
expect(getNextSortDir('price', 'asc', 'timestamp')).toBe('desc');
});

it('builds sort links for all sortable fields', () => {
const links = buildOrderGridSortLinks('price', 'asc');

expect(links.instrument).toBe('/?page=1&sortBy=instrument&sortDir=asc');
expect(links.side).toBe('/?page=1&sortBy=side&sortDir=asc');
expect(links.price).toBe('/?page=1&sortBy=price&sortDir=desc');
expect(links.quantity).toBe('/?page=1&sortBy=quantity&sortDir=asc');
expect(links.remainingQuantity).toBe(
'/?page=1&sortBy=remainingQuantity&sortDir=asc',
);
expect(links.status).toBe('/?page=1&sortBy=status&sortDir=asc');
expect(links.timestamp).toBe('/?page=1&sortBy=timestamp&sortDir=desc');
});

it('always resets to page 1 in sort links regardless of current page', () => {
const links = buildOrderGridSortLinks('instrument', 'asc');

// All links should point to page=1
Object.values(links).forEach((href) => {
expect(href).toMatch(/page=1/);
});
});

it('builds sort links when timestamp is the active sort field', () => {
const links = buildOrderGridSortLinks('timestamp', 'desc');

// timestamp is active with desc → next click should go asc
expect(links.timestamp).toBe('/?page=1&sortBy=timestamp&sortDir=asc');
// other fields use their default direction (asc)
expect(links.price).toBe('/?page=1&sortBy=price&sortDir=asc');
expect(links.instrument).toBe('/?page=1&sortBy=instrument&sortDir=asc');
});

it('builds href with page 1', () => {
const href = buildOrderGridHref(1, 'instrument', 'desc');

expect(href).toBe('/?page=1&sortBy=instrument&sortDir=desc');
});
});
67 changes: 67 additions & 0 deletions components/OrderGrid/__tests__/order-grid.query.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
jest.mock('@/lib/orders/orders.sort', () => ({
resolveOrdersSortBy: (value?: string) =>
value === 'price' || value === 'timestamp' ? value : 'timestamp',
resolveOrdersSortDir: (value?: string) =>
value === 'asc' || value === 'desc' ? value : 'desc',
}));

import { resolveOrderGridQuery } from '../order-grid.query';

describe('order-grid.query', () => {
it('returns defaults when query params are missing', () => {
const result = resolveOrderGridQuery({});

expect(result).toEqual({
requestedPage: 1,
sortBy: 'timestamp',
sortDir: 'desc',
});
});

it('clamps page to at least 1', () => {
expect(resolveOrderGridQuery({ page: '0' }).requestedPage).toBe(1);
expect(resolveOrderGridQuery({ page: '-10' }).requestedPage).toBe(1);
expect(resolveOrderGridQuery({ page: '5' }).requestedPage).toBe(5);
});

it('falls back to defaults for invalid sorting params', () => {
const result = resolveOrderGridQuery({
sortBy: 'invalid-field',
sortDir: 'invalid-dir',
});

expect(result.sortBy).toBe('timestamp');
expect(result.sortDir).toBe('desc');
});

it('resolves valid sorting params', () => {
const result = resolveOrderGridQuery({
sortBy: 'price',
sortDir: 'asc',
});

expect(result.sortBy).toBe('price');
expect(result.sortDir).toBe('asc');
});

it('falls back page to 1 when page is NaN', () => {
expect(resolveOrderGridQuery({ page: 'abc' }).requestedPage).toBe(1);
expect(resolveOrderGridQuery({ page: 'NaN' }).requestedPage).toBe(1);
});

it('returns page 1 when page param is absent', () => {
const result = resolveOrderGridQuery({ sortBy: 'price' });

expect(result.requestedPage).toBe(1);
});

it('returns all three resolved fields together', () => {
const result = resolveOrderGridQuery({ page: '3', sortBy: 'timestamp', sortDir: 'desc' });

expect(result).toEqual({
requestedPage: 3,
sortBy: 'timestamp',
sortDir: 'desc',
});
});
});
63 changes: 62 additions & 1 deletion lib/orders/__tests__/orders.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,65 @@ describe('orders.service', () => {
expect(result.prevPage).toBeUndefined();
expect(result.nextPage).toBeUndefined();
});
});

it('sorts by price ascending before pagination', async () => {
mockedFindAllOrders.mockResolvedValue([
makeOrder({ id: '1', price: 15.2 }),
makeOrder({ id: '2', price: 9.5 }),
makeOrder({ id: '3', price: 11.0 }),
]);

const result = await getPaginatedOrdersForGrid(1, 15, 'price', 'asc');

expect(result.items.map((order) => order.id)).toEqual(['2', '3', '1']);
});

it('sorts by status descending using market priority', async () => {
mockedFindAllOrders.mockResolvedValue([
makeOrder({ id: '1', status: 'Executada' }),
makeOrder({ id: '2', status: 'Aberta' }),
makeOrder({ id: '3', status: 'Cancelada' }),
makeOrder({ id: '4', status: 'Parcial' }),
]);

const result = await getPaginatedOrdersForGrid(1, 15, 'status', 'desc');

expect(result.items.map((order) => order.id)).toEqual(['2', '4', '1', '3']);
});

it('sorts by instrument ascending before pagination', async () => {
mockedFindAllOrders.mockResolvedValue([
makeOrder({ id: '1', instrument: 'VALE3' }),
makeOrder({ id: '2', instrument: 'ABEV3' }),
makeOrder({ id: '3', instrument: 'PETR4' }),
]);

const result = await getPaginatedOrdersForGrid(1, 15, 'instrument', 'asc');

expect(result.items.map((order) => order.id)).toEqual(['2', '3', '1']);
});

it('sorts by quantity descending before pagination', async () => {
mockedFindAllOrders.mockResolvedValue([
makeOrder({ id: '1', quantity: 100 }),
makeOrder({ id: '2', quantity: 500 }),
makeOrder({ id: '3', quantity: 300 }),
]);

const result = await getPaginatedOrdersForGrid(1, 15, 'quantity', 'desc');

expect(result.items.map((order) => order.id)).toEqual(['2', '3', '1']);
});

it('uses default sort (timestamp desc) when sort params are omitted', async () => {
mockedFindAllOrders.mockResolvedValue([
makeOrder({ id: '1', timestamp: '2026-03-15T10:00:00Z' }),
makeOrder({ id: '2', timestamp: '2026-03-15T12:00:00Z' }),
makeOrder({ id: '3', timestamp: '2026-03-15T11:00:00Z' }),
]);

const result = await getPaginatedOrdersForGrid(1, 15);

expect(result.items.map((order) => order.id)).toEqual(['2', '3', '1']);
});
});
Loading
Loading