From 79439ff8270006d7fe61a2a7deba2fbecf45b52d Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 23:03:24 +0000 Subject: [PATCH] CodeRabbit Generated Unit Tests: Add unit tests for PR changes --- .../OrderGrid/__tests__/OrderGrid.test.tsx | 140 ++++++- .../__tests__/order-grid.navigation.test.ts | 67 ++++ .../__tests__/order-grid.query.test.ts | 67 ++++ lib/orders/__tests__/orders.service.test.ts | 63 +++- lib/orders/__tests__/orders.sort.test.ts | 341 ++++++++++++++++++ 5 files changed, 676 insertions(+), 2 deletions(-) create mode 100644 components/OrderGrid/__tests__/order-grid.navigation.test.ts create mode 100644 components/OrderGrid/__tests__/order-grid.query.test.ts create mode 100644 lib/orders/__tests__/orders.sort.test.ts diff --git a/components/OrderGrid/__tests__/OrderGrid.test.tsx b/components/OrderGrid/__tests__/OrderGrid.test.tsx index 8ffe3e8..0264668 100644 --- a/components/OrderGrid/__tests__/OrderGrid.test.tsx +++ b/components/OrderGrid/__tests__/OrderGrid.test.tsx @@ -76,4 +76,142 @@ describe('OrderGrid', () => { expect(OrdersGrid.Body).toBeDefined(); expect(OrdersGrid.defaultColumns).toBe(defaultColumns); }); -}); + + it('renders sortable header links with active indicator', () => { + render( + , + ); + + 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( + , + ); + + expect(screen.getByText('Instrumento')).toBeInTheDocument(); + expect(screen.queryByRole('link', { name: /Ordenar/ })).not.toBeInTheDocument(); + }); + + it('shows ascending indicator (▲) for active ascending sort column', () => { + render( + , + ); + + const priceSortLink = screen.getByRole('link', { name: 'Ordenar por Preço' }); + expect(priceSortLink).toHaveTextContent('▲'); + }); + + it('shows neutral indicator (↕) for inactive sortable column', () => { + render( + , + ); + + const instrumentLink = screen.getByRole('link', { name: 'Ordenar por Instrumento' }); + expect(instrumentLink).toHaveTextContent('↕'); + }); + + it('sets aria-sort descending on active descending column', () => { + const { container } = render( + , + ); + + const priceHeader = container.querySelector('th[aria-sort="descending"]'); + expect(priceHeader).toBeInTheDocument(); + }); + + it('sets aria-sort ascending on active ascending column', () => { + const { container } = render( + , + ); + + 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( + , + ); + + 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( + , + ); + + 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); + }); +}); \ No newline at end of file diff --git a/components/OrderGrid/__tests__/order-grid.navigation.test.ts b/components/OrderGrid/__tests__/order-grid.navigation.test.ts new file mode 100644 index 0000000..3a90fce --- /dev/null +++ b/components/OrderGrid/__tests__/order-grid.navigation.test.ts @@ -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'); + }); +}); \ No newline at end of file diff --git a/components/OrderGrid/__tests__/order-grid.query.test.ts b/components/OrderGrid/__tests__/order-grid.query.test.ts new file mode 100644 index 0000000..85107e6 --- /dev/null +++ b/components/OrderGrid/__tests__/order-grid.query.test.ts @@ -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', + }); + }); +}); \ No newline at end of file diff --git a/lib/orders/__tests__/orders.service.test.ts b/lib/orders/__tests__/orders.service.test.ts index 4b6efc3..58e1b95 100644 --- a/lib/orders/__tests__/orders.service.test.ts +++ b/lib/orders/__tests__/orders.service.test.ts @@ -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']); + }); +}); \ No newline at end of file diff --git a/lib/orders/__tests__/orders.sort.test.ts b/lib/orders/__tests__/orders.sort.test.ts new file mode 100644 index 0000000..edb08e1 --- /dev/null +++ b/lib/orders/__tests__/orders.sort.test.ts @@ -0,0 +1,341 @@ +import { makeOrder } from './fixtures/orders.fixture'; +import { + getDefaultSortDirForField, + getSortedOrders, + isOrdersSortBy, + resolveOrdersSortBy, + resolveOrdersSortDir, +} from '../orders.sort'; + +describe('orders.sort', () => { + describe('isOrdersSortBy', () => { + it('returns true for all valid sort fields', () => { + expect(isOrdersSortBy('timestamp')).toBe(true); + expect(isOrdersSortBy('instrument')).toBe(true); + expect(isOrdersSortBy('side')).toBe(true); + expect(isOrdersSortBy('price')).toBe(true); + expect(isOrdersSortBy('quantity')).toBe(true); + expect(isOrdersSortBy('remainingQuantity')).toBe(true); + expect(isOrdersSortBy('status')).toBe(true); + }); + + it('returns false for unknown field names', () => { + expect(isOrdersSortBy('unknown')).toBe(false); + expect(isOrdersSortBy('PRICE')).toBe(false); + expect(isOrdersSortBy('')).toBe(false); + expect(isOrdersSortBy('id')).toBe(false); + }); + }); + + describe('resolveOrdersSortBy', () => { + it('returns default (timestamp) when value is undefined', () => { + expect(resolveOrdersSortBy(undefined)).toBe('timestamp'); + }); + + it('returns default when value is empty string', () => { + expect(resolveOrdersSortBy('')).toBe('timestamp'); + }); + + it('returns default when value is not a valid sort field', () => { + expect(resolveOrdersSortBy('invalid')).toBe('timestamp'); + expect(resolveOrdersSortBy('PRICE')).toBe('timestamp'); + }); + + it('returns the value when it is a valid sort field', () => { + expect(resolveOrdersSortBy('price')).toBe('price'); + expect(resolveOrdersSortBy('instrument')).toBe('instrument'); + expect(resolveOrdersSortBy('side')).toBe('side'); + expect(resolveOrdersSortBy('quantity')).toBe('quantity'); + expect(resolveOrdersSortBy('remainingQuantity')).toBe('remainingQuantity'); + expect(resolveOrdersSortBy('status')).toBe('status'); + expect(resolveOrdersSortBy('timestamp')).toBe('timestamp'); + }); + }); + + describe('resolveOrdersSortDir', () => { + it('returns default (desc) when value is undefined', () => { + expect(resolveOrdersSortDir(undefined)).toBe('desc'); + }); + + it('returns default when value is not asc or desc', () => { + expect(resolveOrdersSortDir('invalid')).toBe('desc'); + expect(resolveOrdersSortDir('')).toBe('desc'); + expect(resolveOrdersSortDir('ASC')).toBe('desc'); + }); + + it('returns asc when value is asc', () => { + expect(resolveOrdersSortDir('asc')).toBe('asc'); + }); + + it('returns desc when value is desc', () => { + expect(resolveOrdersSortDir('desc')).toBe('desc'); + }); + }); + + describe('getDefaultSortDirForField', () => { + it('returns desc for timestamp', () => { + expect(getDefaultSortDirForField('timestamp')).toBe('desc'); + }); + + it('returns asc for all other sort fields', () => { + expect(getDefaultSortDirForField('instrument')).toBe('asc'); + expect(getDefaultSortDirForField('side')).toBe('asc'); + expect(getDefaultSortDirForField('price')).toBe('asc'); + expect(getDefaultSortDirForField('quantity')).toBe('asc'); + expect(getDefaultSortDirForField('remainingQuantity')).toBe('asc'); + expect(getDefaultSortDirForField('status')).toBe('asc'); + }); + }); + + describe('getSortedOrders', () => { + it('does not mutate the original array', () => { + const orders = [ + makeOrder({ id: '1', price: 20 }), + makeOrder({ id: '2', price: 10 }), + ]; + const original = [...orders]; + + getSortedOrders(orders, 'price', 'asc'); + + expect(orders).toEqual(original); + }); + + describe('sort by timestamp', () => { + it('sorts ascending by timestamp', () => { + const orders = [ + makeOrder({ id: '1', timestamp: '2026-03-15T12:00:00Z' }), + makeOrder({ id: '2', timestamp: '2026-03-15T10:00:00Z' }), + makeOrder({ id: '3', timestamp: '2026-03-15T11:00:00Z' }), + ]; + + const result = getSortedOrders(orders, 'timestamp', 'asc'); + + expect(result.map((o) => o.id)).toEqual(['2', '3', '1']); + }); + + it('sorts descending by timestamp', () => { + const orders = [ + 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 = getSortedOrders(orders, 'timestamp', 'desc'); + + expect(result.map((o) => o.id)).toEqual(['2', '3', '1']); + }); + }); + + describe('sort by instrument', () => { + it('sorts ascending alphabetically by instrument', () => { + const orders = [ + makeOrder({ id: '1', instrument: 'VALE3' }), + makeOrder({ id: '2', instrument: 'ABEV3' }), + makeOrder({ id: '3', instrument: 'PETR4' }), + ]; + + const result = getSortedOrders(orders, 'instrument', 'asc'); + + expect(result.map((o) => o.id)).toEqual(['2', '3', '1']); + }); + + it('sorts descending alphabetically by instrument', () => { + const orders = [ + makeOrder({ id: '1', instrument: 'ABEV3' }), + makeOrder({ id: '2', instrument: 'VALE3' }), + makeOrder({ id: '3', instrument: 'PETR4' }), + ]; + + const result = getSortedOrders(orders, 'instrument', 'desc'); + + expect(result.map((o) => o.id)).toEqual(['2', '3', '1']); + }); + }); + + describe('sort by side', () => { + it('sorts ascending: Compra before Venda', () => { + const orders = [ + makeOrder({ id: '1', side: 'Venda' }), + makeOrder({ id: '2', side: 'Compra' }), + ]; + + const result = getSortedOrders(orders, 'side', 'asc'); + + expect(result.map((o) => o.id)).toEqual(['2', '1']); + }); + + it('sorts descending: Venda before Compra', () => { + const orders = [ + makeOrder({ id: '1', side: 'Compra' }), + makeOrder({ id: '2', side: 'Venda' }), + ]; + + const result = getSortedOrders(orders, 'side', 'desc'); + + expect(result.map((o) => o.id)).toEqual(['2', '1']); + }); + }); + + describe('sort by price', () => { + it('sorts ascending by price', () => { + const orders = [ + makeOrder({ id: '1', price: 50.0 }), + makeOrder({ id: '2', price: 10.0 }), + makeOrder({ id: '3', price: 30.0 }), + ]; + + const result = getSortedOrders(orders, 'price', 'asc'); + + expect(result.map((o) => o.id)).toEqual(['2', '3', '1']); + }); + + it('sorts descending by price', () => { + const orders = [ + makeOrder({ id: '1', price: 10.0 }), + makeOrder({ id: '2', price: 50.0 }), + makeOrder({ id: '3', price: 30.0 }), + ]; + + const result = getSortedOrders(orders, 'price', 'desc'); + + expect(result.map((o) => o.id)).toEqual(['2', '3', '1']); + }); + }); + + describe('sort by quantity', () => { + it('sorts ascending by quantity', () => { + const orders = [ + makeOrder({ id: '1', quantity: 300 }), + makeOrder({ id: '2', quantity: 100 }), + makeOrder({ id: '3', quantity: 200 }), + ]; + + const result = getSortedOrders(orders, 'quantity', 'asc'); + + expect(result.map((o) => o.id)).toEqual(['2', '3', '1']); + }); + + it('sorts descending by quantity', () => { + const orders = [ + makeOrder({ id: '1', quantity: 100 }), + makeOrder({ id: '2', quantity: 300 }), + makeOrder({ id: '3', quantity: 200 }), + ]; + + const result = getSortedOrders(orders, 'quantity', 'desc'); + + expect(result.map((o) => o.id)).toEqual(['2', '3', '1']); + }); + }); + + describe('sort by remainingQuantity', () => { + it('sorts ascending by remainingQuantity', () => { + const orders = [ + makeOrder({ id: '1', remainingQuantity: 300 }), + makeOrder({ id: '2', remainingQuantity: 0 }), + makeOrder({ id: '3', remainingQuantity: 150 }), + ]; + + const result = getSortedOrders(orders, 'remainingQuantity', 'asc'); + + expect(result.map((o) => o.id)).toEqual(['2', '3', '1']); + }); + + it('sorts descending by remainingQuantity', () => { + const orders = [ + makeOrder({ id: '1', remainingQuantity: 0 }), + makeOrder({ id: '2', remainingQuantity: 300 }), + makeOrder({ id: '3', remainingQuantity: 150 }), + ]; + + const result = getSortedOrders(orders, 'remainingQuantity', 'desc'); + + expect(result.map((o) => o.id)).toEqual(['2', '3', '1']); + }); + }); + + describe('sort by status', () => { + it('sorts ascending by status priority (Cancelada < Executada < Parcial < Aberta)', () => { + const orders = [ + makeOrder({ id: '1', status: 'Aberta' }), + makeOrder({ id: '2', status: 'Cancelada' }), + makeOrder({ id: '3', status: 'Parcial' }), + makeOrder({ id: '4', status: 'Executada' }), + ]; + + const result = getSortedOrders(orders, 'status', 'asc'); + + expect(result.map((o) => o.id)).toEqual(['2', '4', '3', '1']); + }); + + it('sorts descending by status priority (Aberta > Parcial > Executada > Cancelada)', () => { + const orders = [ + makeOrder({ id: '1', status: 'Executada' }), + makeOrder({ id: '2', status: 'Aberta' }), + makeOrder({ id: '3', status: 'Cancelada' }), + makeOrder({ id: '4', status: 'Parcial' }), + ]; + + const result = getSortedOrders(orders, 'status', 'desc'); + + expect(result.map((o) => o.id)).toEqual(['2', '4', '1', '3']); + }); + }); + + describe('tie-breaking', () => { + it('breaks ties by timestamp descending when primary sort values are equal', () => { + const orders = [ + makeOrder({ + id: '1', + price: 10, + timestamp: '2026-03-15T10:00:00Z', + }), + makeOrder({ + id: '2', + price: 10, + timestamp: '2026-03-15T12:00:00Z', + }), + makeOrder({ + id: '3', + price: 10, + timestamp: '2026-03-15T11:00:00Z', + }), + ]; + + const result = getSortedOrders(orders, 'price', 'asc'); + + // All prices equal → tie-break by timestamp desc + expect(result.map((o) => o.id)).toEqual(['2', '3', '1']); + }); + + it('breaks ties by id descending (numeric) when timestamp is also equal', () => { + const sameTimestamp = '2026-03-15T10:00:00Z'; + const orders = [ + makeOrder({ id: '1', price: 10, timestamp: sameTimestamp }), + makeOrder({ id: '3', price: 10, timestamp: sameTimestamp }), + makeOrder({ id: '2', price: 10, timestamp: sameTimestamp }), + ]; + + const result = getSortedOrders(orders, 'price', 'asc'); + + // All equal → tie-break by id desc numerically + expect(result.map((o) => o.id)).toEqual(['3', '2', '1']); + }); + }); + + it('returns empty array when given empty input', () => { + const result = getSortedOrders([], 'price', 'asc'); + + expect(result).toEqual([]); + }); + + it('returns single-element array unchanged', () => { + const order = makeOrder({ id: '1', price: 42 }); + + const result = getSortedOrders([order], 'price', 'asc'); + + expect(result).toHaveLength(1); + expect(result[0].id).toBe('1'); + }); + }); +}); \ No newline at end of file