inputs and data views keyed: search, select, tabs, state blocks, data table
The DataTable's pagination carries the only interpolated strings in this area
('Showing page {{current}} of {{total}}', '{{size}} / page'). No plural forms:
nothing here is a count-varying noun, so inventing one would only add a key
nobody uses.
DataTable.test.tsx now renders inside an I18nProvider with the real en-GB
catalog, so it still asserts the English a user sees rather than a dot-path.
Left/right utilities became logical (ms-/me-/ps-/pe-/start-) per D3.
This commit is contained in:
@@ -1,7 +1,21 @@
|
||||
import type { Column } from './DataTable';
|
||||
import type { ReactNode } from 'react';
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { render, screen, fireEvent, within } from '@testing-library/react';
|
||||
import { I18nProvider } from 'i18n';
|
||||
import { DataTable, useDataControl } from './DataTable';
|
||||
import enGB from '../locales/en-GB.json';
|
||||
|
||||
/**
|
||||
* The table's own chrome ("Next", "No Data Found.") comes from the `ui.*` catalog now, so these
|
||||
* assertions only hold inside a provider — the same one the app mounts. Asserting the English a user
|
||||
* actually sees, rather than a key, is the point of wrapping rather than loosening the expectations.
|
||||
*/
|
||||
const withI18n = (ui: ReactNode) => (
|
||||
<I18nProvider translations={{ 'en-GB': { ui: enGB } }} locale="en-GB" fallbackLocale="en-GB">
|
||||
{ui}
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
type TestItem = {
|
||||
id: number;
|
||||
@@ -55,7 +69,7 @@ const TestWrapper = ({
|
||||
describe('DataTable', () => {
|
||||
describe('rendering', () => {
|
||||
test('renders table with data', () => {
|
||||
render(<TestWrapper />);
|
||||
render(withI18n(<TestWrapper />));
|
||||
|
||||
expect(screen.getByRole('table')).toBeDefined();
|
||||
expect(screen.getByText('Alpha')).toBeDefined();
|
||||
@@ -63,7 +77,7 @@ describe('DataTable', () => {
|
||||
});
|
||||
|
||||
test('renders column headers', () => {
|
||||
render(<TestWrapper />);
|
||||
render(withI18n(<TestWrapper />));
|
||||
|
||||
// Component renders both desktop and mobile versions (hidden via CSS)
|
||||
// so we use getAllByText and verify at least one exists
|
||||
@@ -74,7 +88,7 @@ describe('DataTable', () => {
|
||||
});
|
||||
|
||||
test('renders correct number of rows based on page size', () => {
|
||||
render(<TestWrapper pageSize={3} />);
|
||||
render(withI18n(<TestWrapper pageSize={3} />));
|
||||
|
||||
const rows = screen.getAllByRole('row');
|
||||
// 1 header row + 3 data rows
|
||||
@@ -82,20 +96,20 @@ describe('DataTable', () => {
|
||||
});
|
||||
|
||||
test('shows no data message when data is empty', () => {
|
||||
render(<TestWrapper data={[]} noDataMessage="No items found" />);
|
||||
render(withI18n(<TestWrapper data={[]} noDataMessage="No items found" />));
|
||||
|
||||
expect(screen.getByText('No items found')).toBeDefined();
|
||||
expect(screen.queryByRole('table')).toBeNull();
|
||||
});
|
||||
|
||||
test('shows default no data message', () => {
|
||||
render(<TestWrapper data={[]} />);
|
||||
render(withI18n(<TestWrapper data={[]} />));
|
||||
|
||||
expect(screen.getByText('No Data Found.')).toBeDefined();
|
||||
});
|
||||
|
||||
test('renders noDataCTA when provided', () => {
|
||||
render(<TestWrapper data={[]} noDataCTA={<button>Add Item</button>} />);
|
||||
render(withI18n(<TestWrapper data={[]} noDataCTA={<button>Add Item</button>} />));
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Add Item' })).toBeDefined();
|
||||
});
|
||||
@@ -106,7 +120,7 @@ describe('DataTable', () => {
|
||||
return <DataTable dataController={controller} columns={columns} />;
|
||||
};
|
||||
|
||||
const { container } = render(<NullWrapper />);
|
||||
const { container } = render(withI18n(<NullWrapper />));
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -118,7 +132,7 @@ describe('DataTable', () => {
|
||||
return <DataTable dataController={controller} />;
|
||||
};
|
||||
|
||||
render(<AutoColumnsWrapper />);
|
||||
render(withI18n(<AutoColumnsWrapper />));
|
||||
|
||||
// Should generate columns from object keys (rendered for both mobile/desktop)
|
||||
expect(screen.getAllByText('Id').length).toBeGreaterThan(0);
|
||||
@@ -130,7 +144,7 @@ describe('DataTable', () => {
|
||||
|
||||
describe('sorting', () => {
|
||||
test('clicking sortable column header triggers sort', () => {
|
||||
render(<TestWrapper pageSize={10} />);
|
||||
render(withI18n(<TestWrapper pageSize={10} />));
|
||||
|
||||
// Get the first "Name" element (desktop version)
|
||||
const nameHeaders = screen.getAllByText('Name');
|
||||
@@ -143,7 +157,7 @@ describe('DataTable', () => {
|
||||
});
|
||||
|
||||
test('clicking same column twice reverses sort order', () => {
|
||||
render(<TestWrapper pageSize={10} />);
|
||||
render(withI18n(<TestWrapper pageSize={10} />));
|
||||
|
||||
const nameHeaders = screen.getAllByText('Name');
|
||||
fireEvent.click(nameHeaders[0]!); // asc
|
||||
@@ -156,7 +170,7 @@ describe('DataTable', () => {
|
||||
});
|
||||
|
||||
test('sort icon is displayed for sortable columns', () => {
|
||||
render(<TestWrapper />);
|
||||
render(withI18n(<TestWrapper />));
|
||||
|
||||
// Each sortable column should have the ArrowUpDown icon
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
@@ -169,19 +183,19 @@ describe('DataTable', () => {
|
||||
|
||||
describe('pagination', () => {
|
||||
test('shows pagination bar when data exceeds page size', () => {
|
||||
render(<TestWrapper pageSize={5} />);
|
||||
render(withI18n(<TestWrapper pageSize={5} />));
|
||||
|
||||
expect(screen.getByText(/Showing page 1 of 2/)).toBeDefined();
|
||||
});
|
||||
|
||||
test('does not show pagination when all data fits on one page', () => {
|
||||
render(<TestWrapper pageSize={10} />);
|
||||
render(withI18n(<TestWrapper pageSize={10} />));
|
||||
|
||||
expect(screen.queryByText(/Showing page/)).toBeNull();
|
||||
});
|
||||
|
||||
test('next button navigates to next page', () => {
|
||||
render(<TestWrapper pageSize={5} />);
|
||||
render(withI18n(<TestWrapper pageSize={5} />));
|
||||
|
||||
const nextButton = screen.getByRole('button', { name: 'Next' });
|
||||
fireEvent.click(nextButton);
|
||||
@@ -192,7 +206,7 @@ describe('DataTable', () => {
|
||||
});
|
||||
|
||||
test('previous button navigates to previous page', () => {
|
||||
render(<TestWrapper pageSize={5} />);
|
||||
render(withI18n(<TestWrapper pageSize={5} />));
|
||||
|
||||
const nextButton = screen.getByRole('button', { name: 'Next' });
|
||||
fireEvent.click(nextButton);
|
||||
@@ -205,7 +219,7 @@ describe('DataTable', () => {
|
||||
});
|
||||
|
||||
test('first button goes to first page', () => {
|
||||
render(<TestWrapper pageSize={5} />);
|
||||
render(withI18n(<TestWrapper pageSize={5} />));
|
||||
|
||||
const nextButton = screen.getByRole('button', { name: 'Next' });
|
||||
fireEvent.click(nextButton);
|
||||
@@ -217,7 +231,7 @@ describe('DataTable', () => {
|
||||
});
|
||||
|
||||
test('last button goes to last page', () => {
|
||||
render(<TestWrapper pageSize={5} />);
|
||||
render(withI18n(<TestWrapper pageSize={5} />));
|
||||
|
||||
const lastButton = screen.getByRole('button', { name: 'Last' });
|
||||
fireEvent.click(lastButton);
|
||||
@@ -226,7 +240,7 @@ describe('DataTable', () => {
|
||||
});
|
||||
|
||||
test('navigation buttons are disabled at boundaries', () => {
|
||||
render(<TestWrapper pageSize={5} />);
|
||||
render(withI18n(<TestWrapper pageSize={5} />));
|
||||
|
||||
const firstBtn = screen.getByRole('button', { name: 'First' });
|
||||
const prevBtn = screen.getByRole('button', { name: 'Prev' });
|
||||
@@ -258,7 +272,7 @@ describe('DataTable', () => {
|
||||
{ field: 'status', label: 'Status', condition: false },
|
||||
];
|
||||
|
||||
render(<TestWrapper cols={conditionalColumns} />);
|
||||
render(withI18n(<TestWrapper cols={conditionalColumns} />));
|
||||
|
||||
expect(screen.getAllByText('ID').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('Name').length).toBeGreaterThan(0);
|
||||
@@ -271,7 +285,7 @@ describe('DataTable', () => {
|
||||
{ field: 'status', label: 'Status', condition: true },
|
||||
];
|
||||
|
||||
render(<TestWrapper cols={conditionalColumns} />);
|
||||
render(withI18n(<TestWrapper cols={conditionalColumns} />));
|
||||
|
||||
expect(screen.getAllByText('Status').length).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -283,11 +297,11 @@ describe('DataTable', () => {
|
||||
{ field: 'status', label: 'Status', condition: () => showStatus },
|
||||
];
|
||||
|
||||
const { rerender } = render(<TestWrapper cols={conditionalColumns} />);
|
||||
const { rerender } = render(withI18n(<TestWrapper cols={conditionalColumns} />));
|
||||
expect(screen.queryByText('Status')).toBeNull();
|
||||
|
||||
showStatus = true;
|
||||
rerender(<TestWrapper cols={conditionalColumns} />);
|
||||
rerender(withI18n(<TestWrapper cols={conditionalColumns} />));
|
||||
expect(screen.getAllByText('Status').length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -303,7 +317,7 @@ describe('DataTable', () => {
|
||||
},
|
||||
];
|
||||
|
||||
render(<TestWrapper cols={formattedColumns} />);
|
||||
render(withI18n(<TestWrapper cols={formattedColumns} />));
|
||||
|
||||
const formattedNames = screen.getAllByTestId('formatted-name');
|
||||
expect(formattedNames.length).toBeGreaterThan(0);
|
||||
@@ -319,7 +333,7 @@ describe('DataTable', () => {
|
||||
},
|
||||
];
|
||||
|
||||
render(<TestWrapper cols={formattedColumns} />);
|
||||
render(withI18n(<TestWrapper cols={formattedColumns} />));
|
||||
|
||||
// Each row should have a unique test ID based on item.id
|
||||
expect(screen.getByTestId('item-1')).toBeDefined();
|
||||
@@ -337,7 +351,7 @@ describe('DataTable', () => {
|
||||
},
|
||||
];
|
||||
|
||||
render(<TestWrapper cols={actionColumns} />);
|
||||
render(withI18n(<TestWrapper cols={actionColumns} />));
|
||||
|
||||
expect(screen.getByTestId('action-1')).toBeDefined();
|
||||
expect(screen.getByText('Edit Alpha')).toBeDefined();
|
||||
@@ -351,7 +365,7 @@ describe('DataTable', () => {
|
||||
{ field: 'name', label: 'Name' },
|
||||
];
|
||||
|
||||
render(<TestWrapper cols={tooltipColumns} />);
|
||||
render(withI18n(<TestWrapper cols={tooltipColumns} />));
|
||||
|
||||
// The tooltip indicator (ⓘ) should be present in the ID column headers
|
||||
const idHeaders = screen.getAllByText(/ID/);
|
||||
@@ -367,7 +381,7 @@ describe('DataTable', () => {
|
||||
{ field: 'name', label: 'Full Name', labelMobile: 'Name' },
|
||||
];
|
||||
|
||||
render(<TestWrapper cols={mobileColumns} />);
|
||||
render(withI18n(<TestWrapper cols={mobileColumns} />));
|
||||
|
||||
// Both desktop and mobile labels should be in the DOM (hidden/shown via CSS)
|
||||
expect(screen.getAllByText('Identifier').length).toBeGreaterThan(0);
|
||||
@@ -396,7 +410,7 @@ describe('DataTable', () => {
|
||||
);
|
||||
};
|
||||
|
||||
render(<SearchableTable />);
|
||||
render(withI18n(<SearchableTable />));
|
||||
|
||||
const searchInput = screen.getByTestId('search-input');
|
||||
fireEvent.change(searchInput, { target: { value: 'Alpha' } });
|
||||
@@ -420,7 +434,7 @@ describe('DataTable', () => {
|
||||
);
|
||||
};
|
||||
|
||||
render(<ExternalSortTable />);
|
||||
render(withI18n(<ExternalSortTable />));
|
||||
|
||||
fireEvent.click(screen.getByTestId('sort-btn'));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user