Skip to content
Merged
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
123 changes: 123 additions & 0 deletions src/lib/components/tablev2/MultiSelectableContent.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Table, TableProps } from './Tablev2.component';
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { ThemeProvider } from 'styled-components';
import { coreUIAvailableThemes } from '../../style/theme';

jest.mock('./TableUtils', () => ({
...jest.requireActual('./TableUtils'),
// since convertRemToPixels rely on getComputedStyle(document.documentElement) which is not available in jest
// we mock it
convertRemToPixels: () => 12,
}));

jest.mock('react-virtualized-auto-sizer', () => ({ children }) => {
return children({
height: 600,
width: 600,
});
});

const data = [
{ firstName: 'Sotiria', lastName: 'Agathangelou', age: 90 },
{ firstName: 'Stefania', lastName: 'Evgenios', age: 27 },
{ firstName: 'Yohann', lastName: 'Rodolph', age: 27 },
{ firstName: 'Ninette', lastName: 'Caroline', age: 31 },
];

const columns: TableProps['columns'] = [
{ Header: 'First Name', accessor: 'firstName' },
{ Header: 'Last Name', accessor: 'lastName' },
{ Header: 'Age', accessor: 'age' },
];

const renderMultiSelectTable = (
props: {
onMultiSelectionChanged?: jest.Mock;
onSingleRowSelected?: jest.Mock;
} = {},
) =>
render(
<ThemeProvider theme={coreUIAvailableThemes.artescaLight}>
<Table columns={columns} data={data} defaultSortingKey="firstName">
<Table.MultiSelectableContent
rowHeight="h40"
separationLineVariant="backgroundLevel3"
{...props}
/>
</Table>
</ThemeProvider>,
);

describe('MultiSelectableContent', () => {
it('reports only checkbox-selected rows in onMultiSelectionChanged', async () => {
const onMultiSelectionChanged = jest.fn();
renderMultiSelectTable({ onMultiSelectionChanged });

await waitFor(() => screen.queryAllByRole('img', { hidden: true }));

const rows = screen.getAllByRole('row');
const targetRow = rows[1];
const checkbox = within(targetRow).getByRole('checkbox');

fireEvent.click(checkbox);

expect(onMultiSelectionChanged).toHaveBeenCalled();
const lastCallRows = onMultiSelectionChanged.mock.calls.at(-1)![0];
expect(lastCallRows).toHaveLength(1);
expect(lastCallRows[0].original).toEqual(data[3]); // Ninette (firstName-sorted index 0)
});

it('does not include a previously single-clicked row in subsequent multi-selection (ARTESCA-8467)', async () => {
const onSingleRowSelected = jest.fn();
const onMultiSelectionChanged = jest.fn();
renderMultiSelectTable({ onSingleRowSelected, onMultiSelectionChanged });

await waitFor(() => screen.queryAllByRole('img', { hidden: true }));

// Skip the header (rows[0]) — data rows are rows[1..4] sorted by firstName:
// Ninette, Sotiria, Stefania, Yohann.

// Simulate clicking the row body (not the checkbox) to trigger the
// "view details" path. Click a data cell within the row.
fireEvent.click(screen.getByText('Ninette'));

expect(onSingleRowSelected).toHaveBeenCalledTimes(1);
expect(onSingleRowSelected.mock.calls[0][0].original).toEqual(data[3]);

// Re-fetch the rows because RenderRow is memoized inside the parent and
// remounts on every parent render. The viewed row's checkbox must stay
// unchecked — this is the contract that prevents stale rows from leaking
// into multi-selection.
const viewedRow = screen.getAllByRole('row')[1];
expect(within(viewedRow).getByRole('checkbox')).not.toBeChecked();

// Now check a different row's checkbox.
const checkboxRow = screen.getAllByRole('row')[3]; // Stefania
fireEvent.click(within(checkboxRow).getByRole('checkbox'));

expect(onMultiSelectionChanged).toHaveBeenCalled();
const lastCallRows = onMultiSelectionChanged.mock.calls.at(-1)![0];
expect(lastCallRows).toHaveLength(1);
expect(lastCallRows[0].original).toEqual(data[1]); // Stefania
});

it('keeps the active row checkbox unchecked across subsequent checkbox clicks', async () => {
const onSingleRowSelected = jest.fn();
const onMultiSelectionChanged = jest.fn();
renderMultiSelectTable({ onSingleRowSelected, onMultiSelectionChanged });

await waitFor(() => screen.queryAllByRole('img', { hidden: true }));

fireEvent.click(screen.getByText('Ninette'));

const otherCheckboxRow = screen.getAllByRole('row')[3];
fireEvent.click(within(otherCheckboxRow).getByRole('checkbox'));

// The active row's checkbox stays unchecked even after another row's
// checkbox click — visual highlight on the active row is driven by the
// `isSelected` prop on the styled-component (covered in storybook).
const activeRow = screen.getAllByRole('row')[1];
expect(within(activeRow).getByRole('checkbox')).not.toBeChecked();
expect(within(screen.getAllByRole('row')[3]).getByRole('checkbox')).toBeChecked();
});
});
11 changes: 8 additions & 3 deletions src/lib/components/tablev2/MultiSelectableContent.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, memo, CSSProperties } from 'react';
import { useEffect, useState, memo, CSSProperties } from 'react';
import { Row } from 'react-table';
import { areEqual } from 'react-window';
import { useTableContext } from './Tablev2.component';
Expand Down Expand Up @@ -86,6 +86,11 @@ export const MultiSelectableContent = <
});
}, [setHiddenColumns]);

// Tracks the row most recently activated via `onSingleRowSelected` (e.g. for
// a detail panel). Kept separate from react-table's `selectedRowIds` so that
// viewing a row does not mark it as checkbox-selected for bulk operations.
const [activeRowId, setActiveRowId] = useState<string | null>(null);

const handleMultipleSelectedRows = (
selectedRowIds,
rows,
Expand Down Expand Up @@ -136,15 +141,15 @@ export const MultiSelectableContent = <
? () => {
onSingleRowSelected(row);
toggleAllRowsSelected(false);
row.toggleRowSelected(true);
setActiveRowId(row.id);
}
: () => handleMultipleSelectedRows(selectedRowIds, rows, row, index),
};

return (
<TableRowMultiSelectable
{...rowProps}
isSelected={row.isSelected}
isSelected={row.isSelected || activeRowId === row.id}
separationLineVariant={separationLineVariant}
className="tr"
>
Expand Down
Loading