Загрузка данных
import { render, screen, within } from '@testing-library/react';
import { Table } from '@src/components/Table';
import styles from '@src/components/Table/index.module.scss';
import type { TableColumn } from '@src/components/Table';
jest.mock('@src/components/Table/index.module.scss', () => ({
__esModule: true,
default: {
table: 'table',
table_content: 'table_content',
table_stickyHeader: 'table_stickyHeader',
table_stickyFirstColumn: 'table_stickyFirstColumn',
table_cell: 'table_cell',
table_cell_left: 'table_cell_left',
table_cell_center: 'table_cell_center',
table_cell_right: 'table_cell_right',
table_empty: 'table_empty',
},
}));
interface TestRow {
id: number;
name: string;
value: number;
}
const columns: TableColumn<TestRow>[] = [
{
id: 'name',
header: 'Name',
rowHeader: true,
renderCell: (row) => row.name,
},
{
id: 'value',
header: <span>Value</span>,
align: 'right',
className: 'amount',
renderCell: (row) => <strong>{row.value}</strong>,
getCellClassName: (row) => (row.value < 0 ? 'negative' : undefined),
},
];
const rows: TestRow[] = [
{ id: 2, name: 'Beta', value: -2 },
{ id: 1, name: 'Alpha', value: 3 },
];
const getRowKey = (row: TestRow) => row.id;
describe('Table', () => {
it('должен отображать заголовки, строки и содержимое ячеек в переданном порядке', () => {
const { container } = render(<Table columns={columns} rows={rows} getRowKey={getRowKey} />);
const headers = screen.getAllByRole('columnheader');
const bodyRows = screen.getAllByRole('row').slice(1);
expect(headers.map((header) => header.textContent)).toEqual(['Name', 'Value']);
expect(headers[0].getAttribute('scope')).toBe('col');
expect(headers[0].classList.contains(styles.table_cell_left)).toBe(true);
expect(headers[1].classList.contains(styles.table_cell_right)).toBe(true);
expect(headers[1].classList.contains('amount')).toBe(true);
expect(bodyRows).toHaveLength(2);
expect(within(bodyRows[0]).getByRole('rowheader').textContent).toBe('Beta');
expect(within(bodyRows[0]).getByRole('rowheader').getAttribute('scope')).toBe('row');
expect(within(bodyRows[0]).getByRole('cell').classList.contains('negative')).toBe(true);
expect(within(bodyRows[0]).getByText('-2').tagName).toBe('STRONG');
expect(within(bodyRows[1]).getByRole('rowheader').textContent).toBe('Alpha');
expect(within(bodyRows[1]).getByRole('cell').classList.contains('negative')).toBe(false);
expect(container.firstElementChild?.classList.contains(styles.table_stickyHeader)).toBe(true);
expect(container.firstElementChild?.classList.contains(styles.table_stickyFirstColumn)).toBe(false);
});
it('должен применять настройки закрепления, выравнивания и дополнительный класс', () => {
const { container } = render(
<Table
columns={[
{
id: 'name',
header: 'Name',
align: 'center',
renderCell: (row: TestRow) => row.name,
},
]}
rows={rows}
getRowKey={getRowKey}
className="custom-table"
stickyHeader={false}
stickyFirstColumn
/>,
);
expect(container.firstElementChild?.classList.contains('custom-table')).toBe(true);
expect(container.firstElementChild?.classList.contains(styles.table_stickyHeader)).toBe(false);
expect(container.firstElementChild?.classList.contains(styles.table_stickyFirstColumn)).toBe(true);
expect(screen.getByRole('columnheader').classList.contains(styles.table_cell_center)).toBe(true);
expect(screen.getAllByRole('cell')[0].classList.contains(styles.table_cell_center)).toBe(true);
});
it('должен отображать прочерк на всю ширину пустой таблицы', () => {
render(<Table columns={columns} rows={[]} getRowKey={getRowKey} />);
const cell = screen.getByRole('cell');
expect(cell.textContent).toBe('—');
expect(cell.getAttribute('colspan')).toBe('2');
expect(cell.classList.contains(styles.table_empty)).toBe(true);
expect(screen.queryByRole('rowheader')).toBeNull();
});
it('должен отображать переданное содержимое пустого состояния', () => {
render(
<Table
columns={columns}
rows={[]}
getRowKey={getRowKey}
emptyContent={<span>No rows</span>}
/>,
);
expect(within(screen.getByRole('cell')).getByText('No rows')).toBeDefined();
});
it('должен сохранять ячейки строк по ключам при изменении порядка и данных', () => {
const { rerender } = render(<Table columns={columns} rows={rows} getRowKey={getRowKey} />);
const alpha = screen.getByRole('rowheader', { name: 'Alpha' });
rerender(
<Table
columns={columns}
rows={[{ ...rows[1], name: 'Updated alpha' }, rows[0]]}
getRowKey={getRowKey}
/>,
);
expect(screen.getAllByRole('rowheader').map((cell) => cell.textContent)).toEqual(['Updated alpha', 'Beta']);
expect(screen.getByRole('rowheader', { name: 'Updated alpha' })).toBe(alpha);
});
});
import { act, render, screen, within } from '@testing-library/react';
import { PriceScaleMode } from 'lightweight-charts';
import { BehaviorSubject, Subject } from 'rxjs';
import { SeasonalityTable } from '@src/components/SeasonalityTable';
import { createInitialSeasonalityState } from '@src/constants';
import { SeasonalityViewType } from '@src/types';
import { setPricePrecision } from '@src/utils/precision';
import styles from '@src/components/SeasonalityTable/index.module.scss';
import tableStyles from '@src/components/Table/index.module.scss';
import type { SeasonalityState, SeasonalityTableRow } from '@src/types';
jest.mock('@src/components/SeasonalityTable/index.module.scss', () => ({
__esModule: true,
default: {
seasonality: 'seasonality',
positive: 'positive',
negative: 'negative',
total: 'total',
},
}));
jest.mock('@src/components/Table/index.module.scss', () => ({
__esModule: true,
default: {
table: 'table',
table_content: 'table_content',
table_stickyHeader: 'table_stickyHeader',
table_stickyFirstColumn: 'table_stickyFirstColumn',
table_cell: 'table_cell',
table_cell_left: 'table_cell_left',
table_cell_center: 'table_cell_center',
table_cell_right: 'table_cell_right',
table_empty: 'table_empty',
},
}));
const mocks = {
locale: 'en',
};
jest.mock('@src/translations', () => ({
t: (key: string) => key,
getLocale: () => mocks.locale,
}));
function createRow(year = 2024): SeasonalityTableRow {
return {
year,
months: [
{ absolute: 12.3456, percentage: 1.23456 },
{ absolute: -2.3456, percentage: -0.87654 },
{ absolute: 0, percentage: 0 },
{ absolute: 5, percentage: null },
...Array(8).fill(null),
],
total: {
absolute: 15.1234,
percentage: 2.3456,
},
};
}
function createTestContext(overrides: Partial<SeasonalityState> = {}) {
const stateObs = new BehaviorSubject<SeasonalityState>({
...createInitialSeasonalityState(),
status: 'ready',
displayMode: SeasonalityViewType.Table,
tableRows: [createRow()],
...overrides,
});
return { stateObs };
}
function getYearCells(year = 2024) {
const row = screen.getByRole('rowheader', { name: String(year) }).closest('tr') as HTMLTableRowElement;
return within(row).getAllByRole('cell');
}
beforeEach(() => {
setPricePrecision(3);
mocks.locale = 'en';
});
afterEach(() => {
setPricePrecision();
jest.clearAllMocks();
});
describe('SeasonalityTable', () => {
it('не должен отображаться до получения состояния, при idle и в режиме графика', () => {
const stateObs = new Subject<SeasonalityState>();
const { container } = render(<SeasonalityTable stateObs={stateObs} />);
expect(container.children).toHaveLength(0);
act(() => stateObs.next(createInitialSeasonalityState()));
expect(container.children).toHaveLength(0);
act(() =>
stateObs.next({
...createInitialSeasonalityState(),
status: 'ready',
}),
);
expect(container.children).toHaveLength(0);
act(() =>
stateObs.next({
...createInitialSeasonalityState(),
displayMode: SeasonalityViewType.Table,
}),
);
expect(container.children).toHaveLength(0);
});
it('должен отображать все месяцы, годовые итоги и строки в переданном порядке', () => {
const props = createTestContext({
tableRows: [createRow(2025), createRow(2024)],
});
const { container } = render(<SeasonalityTable {...props} />);
expect(screen.getAllByRole('columnheader').map((cell) => cell.textContent)).toEqual([
'Date',
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
'Year',
]);
expect(screen.getAllByRole('rowheader').map((cell) => cell.textContent)).toEqual(['2025', '2024']);
expect(container.firstElementChild?.classList.contains(tableStyles.table_stickyFirstColumn)).toBe(true);
const cells = getYearCells(2025);
expect(cells).toHaveLength(13);
expect(cells[0].textContent).toBe('12.346');
expect(cells[1].textContent).toBe('-2.346');
expect(cells[2].textContent).toBe('0.000');
expect(cells[3].textContent).toBe('5.000');
expect(cells[4].textContent).toBe('—');
expect(cells[12].textContent).toBe('15.123');
expect(cells[0].classList.contains(styles.positive)).toBe(true);
expect(cells[1].classList.contains(styles.negative)).toBe(true);
expect(cells[2].classList.contains(styles.positive)).toBe(false);
expect(cells[2].classList.contains(styles.negative)).toBe(false);
expect(cells[12].classList.contains(styles.total)).toBe(true);
});
it('должен переключать значения, точность и цвета при изменении шкалы', () => {
const props = createTestContext();
render(<SeasonalityTable {...props} />);
expect(getYearCells()[3].classList.contains(styles.positive)).toBe(true);
act(() =>
props.stateObs.next({
...props.stateObs.value,
scale: PriceScaleMode.Percentage,
}),
);
const cells = getYearCells();
expect(cells[0].textContent).toBe('1.23%');
expect(cells[1].textContent).toBe('-0.88%');
expect(cells[2].textContent).toBe('0.00%');
expect(cells[3].textContent).toBe('—');
expect(cells[3].classList.contains(styles.positive)).toBe(false);
expect(cells[3].classList.contains(styles.negative)).toBe(false);
expect(cells[12].textContent).toBe('2.35%');
});
it.each([0, 6])('должен использовать общую точность цены %s', (precision) => {
setPricePrecision(precision);
render(<SeasonalityTable {...createTestContext()} />);
expect(getYearCells()[0].textContent).toBe(precision === 0 ? '12' : '12.345600');
});
it('должен использовать текущую локаль при форматировании процентов', () => {
mocks.locale = 'ru';
render(
<SeasonalityTable
{...createTestContext({
scale: PriceScaleMode.Percentage,
})}
/>,
);
expect(getYearCells()[0].textContent).toBe('1,23%');
expect(getYearCells()[12].textContent).toBe('2,35%');
});
it.each([
['loading', 'Loading seasonality'],
['error', 'Unable to load seasonality'],
['empty', 'No seasonality data'],
['ready', 'No seasonality data'],
] as const)('должен отображать пустое состояние при статусе %s', (status, message) => {
render(
<SeasonalityTable
{...createTestContext({
status,
tableRows: [],
})}
/>,
);
expect(screen.getByRole('cell').textContent).toBe(message);
expect(screen.getByRole('cell').getAttribute('colspan')).toBe('14');
});
it('должен отображать прочерк при отсутствии годового итога', () => {
render(
<SeasonalityTable
{...createTestContext({
tableRows: [{ ...createRow(), total: null }],
})}
/>,
);
expect(getYearCells()[12].textContent).toBe('—');
expect(getYearCells()[12].classList.contains(styles.positive)).toBe(false);
expect(getYearCells()[12].classList.contains(styles.negative)).toBe(false);
});
});