From 2860b8b4659ef0885e47d5c60ab39488c7e15a73 Mon Sep 17 00:00:00 2001 From: will short Date: Fri, 20 Dec 2024 18:02:59 -0600 Subject: [PATCH] chore: updated structure and added unit/impl tests for pinning (#5840) * feat: updated structure and added unit/impl tests for pinning * fix: cleanup test --- packages/table-core/tests/RowPinning.test.ts | 285 --------- .../tests/fixtures/data/generateColumns.ts | 17 + .../tests/fixtures/data/makeData.ts | 42 ++ .../table-core/tests/fixtures/data/types.ts | 15 + .../tests/{ => fixtures/setup}/test-setup.ts | 0 .../tests/helpers/createTestTable.ts | 54 ++ .../tests/helpers/rowPinningHelpers.ts | 59 ++ .../table-core/tests/helpers/testUtils.ts | 22 + .../row-pinning/rowPinningFeature.test.ts | 313 ++++++++++ packages/table-core/tests/makeTestData.ts | 8 +- .../rowPinningFeature.utils.test.ts | 591 ++++++++++++++++++ packages/table-core/vite.config.ts | 2 +- 12 files changed, 1118 insertions(+), 290 deletions(-) delete mode 100644 packages/table-core/tests/RowPinning.test.ts create mode 100644 packages/table-core/tests/fixtures/data/generateColumns.ts create mode 100644 packages/table-core/tests/fixtures/data/makeData.ts create mode 100644 packages/table-core/tests/fixtures/data/types.ts rename packages/table-core/tests/{ => fixtures/setup}/test-setup.ts (100%) create mode 100644 packages/table-core/tests/helpers/createTestTable.ts create mode 100644 packages/table-core/tests/helpers/rowPinningHelpers.ts create mode 100644 packages/table-core/tests/helpers/testUtils.ts create mode 100644 packages/table-core/tests/implementation/features/row-pinning/rowPinningFeature.test.ts create mode 100644 packages/table-core/tests/unit/features/row-pinning/rowPinningFeature.utils.test.ts diff --git a/packages/table-core/tests/RowPinning.test.ts b/packages/table-core/tests/RowPinning.test.ts deleted file mode 100644 index 5cd5bcb65a..0000000000 --- a/packages/table-core/tests/RowPinning.test.ts +++ /dev/null @@ -1,285 +0,0 @@ -import { beforeEach, describe, expect, it } from 'vitest' -import { - constructTable, - coreFeatures, - createColumnHelper, - createPaginatedRowModel, - rowPaginationFeature, - rowPinningFeature, -} from '../src' -import { makeData } from './makeTestData' -import type { ColumnDef } from '../src' -import type { Person } from './makeTestData' - -type personKeys = keyof Person -type PersonColumn = ColumnDef - -function generateColumns(people: Array): Array { - const columnHelper = createColumnHelper() - const person = people[0] - - if (!person) { - return [] - } - - return Object.keys(person).map((key) => { - const typedKey = key as personKeys - return columnHelper.accessor(typedKey, { id: typedKey }) - }) -} - -describe('rowPinningFeature', () => { - let data: Array - let columns: Array - beforeEach(() => { - data = makeData(10) - columns = generateColumns(data) - }) - - describe('constructTable', () => { - describe('getTopRows', () => { - it('should return pinned rows when keepPinnedRows is true rows are visible', () => { - const table = constructTable({ - _features: { - rowPinningFeature, - rowPaginationFeature, - ...coreFeatures, - }, - _rowModels: { - paginatedRowModel: createPaginatedRowModel(), - }, - enableRowPinning: true, - keepPinnedRows: true, - onStateChange() {}, - renderFallbackValue: '', - data, - state: { - pagination: { - pageSize: 5, - pageIndex: 0, // pinned rows will be on page 0 - }, - rowPinning: { - bottom: [], - top: ['0', '1'], - }, - }, - columns, - }) - - const result = table.getTopRows() - - expect(result.length).toBe(2) - expect(result[0]?.id).toBe('0') - expect(result[1]?.id).toBe('1') - }) - it('should return pinned rows when keepPinnedRows is true rows are not visible', () => { - const table = constructTable({ - _features: { - rowPinningFeature, - rowPaginationFeature, - ...coreFeatures, - }, - _rowModels: { - paginatedRowModel: createPaginatedRowModel(), - }, - enableRowPinning: true, - keepPinnedRows: true, - onStateChange() {}, - renderFallbackValue: '', - data, - state: { - pagination: { - pageSize: 5, - pageIndex: 1, // pinned rows will be on page 0 - }, - rowPinning: { - bottom: [], - top: ['0', '1'], - }, - }, - columns, - }) - - const result = table.getTopRows() - - expect(result.length).toBe(2) - expect(result[0]?.id).toBe('0') - expect(result[1]?.id).toBe('1') - }) - it('should return pinned rows when keepPinnedRows is false rows are visible', () => { - const table = constructTable({ - _features: { - rowPinningFeature, - rowPaginationFeature, - ...coreFeatures, - }, - _rowModels: { - paginatedRowModel: createPaginatedRowModel(), - }, - enableRowPinning: true, - keepPinnedRows: false, - onStateChange() {}, - renderFallbackValue: '', - data, - state: { - pagination: { - pageSize: 5, - pageIndex: 0, // pinned rows will be on page 0 - }, - rowPinning: { - bottom: [], - top: ['0', '1'], - }, - }, - columns, - }) - - const result = table.getTopRows() - - expect(result.length).toBe(2) - expect(result[0]?.id).toBe('0') - expect(result[1]?.id).toBe('1') - }) - it('should not return pinned rows when keepPinnedRows is false and rows are not visible', () => { - const table = constructTable({ - _features: { - rowPinningFeature, - rowPaginationFeature, - ...coreFeatures, - }, - _rowModels: { - paginatedRowModel: createPaginatedRowModel(), - }, - enableRowPinning: true, - keepPinnedRows: false, - onStateChange() {}, - renderFallbackValue: '', - data, - state: { - pagination: { - pageSize: 5, - pageIndex: 1, // pinned rows will be on page 0, but this is page 1 - }, - rowPinning: { - bottom: [], - top: ['0', '1'], - }, - }, - columns, - }) - - const result = table.getTopRows() - - expect(result.length).toBe(0) - }) - - it('should return correct top rows', () => { - const table = constructTable({ - _features: { - rowPinningFeature, - rowPaginationFeature, - ...coreFeatures, - }, - _rowModels: { - paginatedRowModel: createPaginatedRowModel(), - }, - enableRowPinning: true, - keepPinnedRows: true, - onStateChange() {}, - renderFallbackValue: '', - data, - state: { - pagination: { - pageSize: 5, - pageIndex: 0, // pinned rows will be on page 0 - }, - rowPinning: { - bottom: [], - top: ['1', '3'], - }, - }, - columns, - }) - - const result = table.getTopRows() - - expect(result.length).toBe(2) - expect(result[0]?.id).toBe('1') - expect(result[1]?.id).toBe('3') - }) - it('should return correct bottom rows', () => { - const table = constructTable({ - _features: { - rowPinningFeature, - rowPaginationFeature, - ...coreFeatures, - }, - _rowModels: { - paginatedRowModel: createPaginatedRowModel(), - }, - enableRowPinning: true, - keepPinnedRows: true, - onStateChange() {}, - renderFallbackValue: '', - data, - state: { - pagination: { - pageSize: 5, - pageIndex: 0, // pinned rows will be on page 0 - }, - rowPinning: { - bottom: ['1', '3'], - top: [], - }, - }, - columns, - }) - - const result = table.getBottomRows() - - expect(result.length).toBe(2) - expect(result[0]?.id).toBe('1') - expect(result[1]?.id).toBe('3') - }) - }) - describe('getCenterRows', () => { - it('should return all rows except any pinned rows', () => { - const data = makeData(6) - const columns = generateColumns(data) - - const table = constructTable({ - _features: { - rowPinningFeature, - rowPaginationFeature, - ...coreFeatures, - }, - _rowModels: { - paginatedRowModel: createPaginatedRowModel(), - }, - enableRowPinning: true, - keepPinnedRows: true, - onStateChange() {}, - renderFallbackValue: '', - data, - state: { - pagination: { - pageSize: 10, - pageIndex: 0, - }, - rowPinning: { - top: ['1', '3'], - bottom: ['2', '4'], - }, - }, - columns, - }) - - const result = table.getCenterRows() - - expect(result.length).toBe(2) - expect(result[0]?.id).toBe('0') // 0 and 5 are the only rows not pinned - expect(result[1]?.id).toBe('5') - }) - }) - }) -}) diff --git a/packages/table-core/tests/fixtures/data/generateColumns.ts b/packages/table-core/tests/fixtures/data/generateColumns.ts new file mode 100644 index 0000000000..591c53592a --- /dev/null +++ b/packages/table-core/tests/fixtures/data/generateColumns.ts @@ -0,0 +1,17 @@ +import { createColumnHelper } from '../../../src' +import type { Person, PersonColumn, PersonKeys } from './types' + +export function generateColumns(people: Array): Array { + const columnHelper = createColumnHelper() + const person = people[0] + + if (!person) { + return [] + } + + return Object.keys(person).map((key) => { + const typedKey = key as PersonKeys + + return columnHelper.accessor(typedKey, { id: typedKey }) + }) +} diff --git a/packages/table-core/tests/fixtures/data/makeData.ts b/packages/table-core/tests/fixtures/data/makeData.ts new file mode 100644 index 0000000000..7663cb056a --- /dev/null +++ b/packages/table-core/tests/fixtures/data/makeData.ts @@ -0,0 +1,42 @@ +import { faker } from '@faker-js/faker' +import { createArrayOfNumbers } from '../../helpers/testUtils' +import type { Person } from './types' + +function createPerson(): Person { + return { + id: faker.string.uuid(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.arrayElement([ + 'relationship', + 'complicated', + 'single', + ]), + } +} + +/** + * Creates a nested array of test Person objects + * @param lengths - An array of numbers where each number determines the length of Person arrays at that depth. + * e.g. makeData(3, 2) creates 3 parent rows with 2 sub-rows each + * @returns An array of Person objects with optional nested subRows based on the provided lengths + */ +export function makeData(...lengths: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lengths[depth] + + if (!len) return [] + + return createArrayOfNumbers(len).map(() => { + return { + ...createPerson(), + subRows: lengths[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/packages/table-core/tests/fixtures/data/types.ts b/packages/table-core/tests/fixtures/data/types.ts new file mode 100644 index 0000000000..3ba761eba4 --- /dev/null +++ b/packages/table-core/tests/fixtures/data/types.ts @@ -0,0 +1,15 @@ +import type { ColumnDef } from '../../../src' + +export type PersonKeys = keyof Person +export type PersonColumn = ColumnDef + +export type Person = { + id: string + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} diff --git a/packages/table-core/tests/test-setup.ts b/packages/table-core/tests/fixtures/setup/test-setup.ts similarity index 100% rename from packages/table-core/tests/test-setup.ts rename to packages/table-core/tests/fixtures/setup/test-setup.ts diff --git a/packages/table-core/tests/helpers/createTestTable.ts b/packages/table-core/tests/helpers/createTestTable.ts new file mode 100644 index 0000000000..3dd046c1ed --- /dev/null +++ b/packages/table-core/tests/helpers/createTestTable.ts @@ -0,0 +1,54 @@ +import { constructTable, coreFeatures } from '../../src' +import { makeData } from '../fixtures/data/makeData' +import { generateColumns } from '../fixtures/data/generateColumns' +import type { TableOptions, TableState } from '../../src' +import type { Person } from '../fixtures/data/types' + +export function createTestTableWithData( + lengths: Array | number = 10, + options?: Omit, 'data' | 'columns'>, +) { + const lengthsArray = Array.isArray(lengths) ? lengths : [lengths] + const data = makeData(...lengthsArray) + const columns = generateColumns(data) + + return constructTable({ + data, + columns, + getSubRows: (row) => row.subRows, + ...options, + _features: { + ...options?._features, + ...coreFeatures, + }, + }) +} + +export function createTestTableWithDataAndState( + lengths: Array | number = 10, + options?: Omit< + TableOptions, + 'data' | 'columns' | 'onStateChange' + >, +) { + let state = { ...options?.initialState } as TableState + + const table = createTestTableWithData(lengths, { + ...options, + _features: { + ...options?._features, + }, + state, + onStateChange: (updater) => { + if (typeof updater === 'function') { + state = updater(state) + } else { + state = updater + } + + table.options.state = state + }, + }) + + return table +} diff --git a/packages/table-core/tests/helpers/rowPinningHelpers.ts b/packages/table-core/tests/helpers/rowPinningHelpers.ts new file mode 100644 index 0000000000..3fd8ad65b3 --- /dev/null +++ b/packages/table-core/tests/helpers/rowPinningHelpers.ts @@ -0,0 +1,59 @@ +import { vi } from 'vitest' +import { getDefaultRowPinningState } from '../../src/features/row-pinning/rowPinningFeature.utils' + +import { rowPinningFeature } from '../../src' +import { + createTestTableWithData, + createTestTableWithDataAndState, +} from './createTestTable' +import type { RowPinningState, TableOptions } from '../../src' +import type { Person } from '../fixtures/data/types' + +export function createTableWithPinningState( + rowCount = 10, + pinningState?: RowPinningState, +) { + const table = createTestTableWithData(rowCount) + if (pinningState) { + table.options.state = { + rowPinning: pinningState, + } + } else { + table.options.state = { + rowPinning: getDefaultRowPinningState(), + } + } + return table +} + +export function createTableWithMockOnPinningChange(rowCount = 10) { + const onRowPinningChangeMock = vi.fn() + const table = createTestTableWithData(rowCount) + table.options.onRowPinningChange = onRowPinningChangeMock + return { table, onRowPinningChangeMock } +} + +export function createRowPinningTable( + options?: Omit< + TableOptions, + 'data' | 'columns' | 'onStateChange' + >, + lengths: Array | number = 10, +) { + const table = createTestTableWithDataAndState(lengths, { + enableRowPinning: true, + initialState: { + rowPinning: { + top: [], + bottom: [], + }, + }, + ...options, + _features: { + ...options?._features, + rowPinning: rowPinningFeature, + }, + }) + + return table +} diff --git a/packages/table-core/tests/helpers/testUtils.ts b/packages/table-core/tests/helpers/testUtils.ts new file mode 100644 index 0000000000..c55cdb3174 --- /dev/null +++ b/packages/table-core/tests/helpers/testUtils.ts @@ -0,0 +1,22 @@ +import type { vi } from 'vitest' +import type { RowPinningState } from '../../src' +import type { Person } from '../fixtures/data/types' + +export const createArrayOfNumbers = (length: number) => { + return Array.from({ length }, (_, i) => i) +} + +export const getPeopleIds = ( + people: Array, + usePersonId: boolean = false, +) => { + return people.map((person, index) => (usePersonId ? person.id : `${index}`)) +} + +export function getUpdaterResult( + mock: ReturnType, + input: RowPinningState, +) { + const updaterFn = mock.mock.calls[0]?.[0] + return updaterFn?.(input) +} diff --git a/packages/table-core/tests/implementation/features/row-pinning/rowPinningFeature.test.ts b/packages/table-core/tests/implementation/features/row-pinning/rowPinningFeature.test.ts new file mode 100644 index 0000000000..a9979864ba --- /dev/null +++ b/packages/table-core/tests/implementation/features/row-pinning/rowPinningFeature.test.ts @@ -0,0 +1,313 @@ +import { describe, expect, it } from 'vitest' +import { createPaginatedRowModel, rowPaginationFeature } from '../../../../src' +import { createRowPinningTable } from '../../../helpers/rowPinningHelpers' + +const ROW = { + 0: '0', + 1: '1', + 2: '2', +} as const + +const SUB_ROW = { + 0: '0.0', + 1: '0.1', +} + +const EMPTY_PINNING_STATE = { + top: [], + bottom: [], +} + +describe('table methods', () => { + describe('setRowPinning', () => { + it('should update pinning state', () => { + const table = createRowPinningTable() + const newState = { + top: [ROW[0]], + bottom: [ROW[1]], + } + + table.setRowPinning(newState) + + expect(table.getState().rowPinning).toEqual(newState) + }) + }) + + describe('resetRowPinning', () => { + it('should reset to default state when defaultState is true', () => { + const table = createRowPinningTable() + + table.setRowPinning({ + top: [ROW[0]], + bottom: [ROW[1]], + }) + + table.resetRowPinning(true) + + expect(table.getState().rowPinning).toEqual(EMPTY_PINNING_STATE) + }) + + it('should reset to initial state when defaultState is false', () => { + const initialState = { + top: [ROW[0]], + bottom: [ROW[1]], + } + const table = createRowPinningTable({ + _features: {}, + initialState: { + rowPinning: initialState, + }, + }) + + table.setRowPinning({ + top: [ROW[2]], + bottom: [], + }) + + table.resetRowPinning(false) + + expect(table.getState().rowPinning).toEqual(initialState) + }) + }) + + describe('getIsSomeRowsPinned', () => { + it('should return false when no rows are pinned', () => { + const table = createRowPinningTable() + expect(table.getIsSomeRowsPinned()).toBe(false) + expect(table.getIsSomeRowsPinned('top')).toBe(false) + expect(table.getIsSomeRowsPinned('bottom')).toBe(false) + }) + + it('should return true when rows are pinned', () => { + const table = createRowPinningTable() + table.setRowPinning({ + top: [ROW[0]], + bottom: [ROW[1]], + }) + + expect(table.getIsSomeRowsPinned()).toBe(true) + expect(table.getIsSomeRowsPinned('top')).toBe(true) + expect(table.getIsSomeRowsPinned('bottom')).toBe(true) + }) + }) + + describe('getTopRows/getBottomRows/getCenterRows', () => { + it('should return correct rows for each section', () => { + const table = createRowPinningTable({ + _features: {}, + initialState: { + rowPinning: { + top: [ROW[0]], + bottom: [ROW[2]], + }, + }, + }) + + const topRows = table.getTopRows() + const bottomRows = table.getBottomRows() + const centerRows = table.getCenterRows() + + expect(topRows).toHaveLength(1) + expect(topRows[0]?.id).toBe(ROW[0]) + + expect(bottomRows).toHaveLength(1) + expect(bottomRows[0]?.id).toBe(ROW[2]) + + expect(centerRows).toHaveLength(8) + expect( + centerRows.every((row) => row.id !== ROW[0] && row.id !== ROW[2]), + ).toBe(true) + }) + + it('should handle keepPinnedRows - false', () => { + const table = createRowPinningTable({ + _features: { + rowPaginationFeature, + }, + _rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + initialState: { + // Make first 2 rows visible + pagination: { + pageSize: 2, + pageIndex: 0, + }, + rowPinning: { + top: [ROW[0]], + bottom: [ROW[2]], + }, + }, + keepPinnedRows: false, + }) + + expect(table.getTopRows()).toHaveLength(1) + expect(table.getBottomRows()).toHaveLength(0) // Row 2 is not in visible rows + }) + }) + + it('should handle keepPinnedRows - true', () => { + const table = createRowPinningTable({ + _features: { + rowPaginationFeature, + }, + _rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + initialState: { + // Make first 2 rows visible + pagination: { + pageSize: 2, + pageIndex: 0, + }, + rowPinning: { + top: [ROW[0]], + bottom: [ROW[2]], + }, + }, + keepPinnedRows: true, + }) + + expect(table.getTopRows()).toHaveLength(1) + expect(table.getBottomRows()).toHaveLength(1) + }) +}) + +describe('row methods', () => { + describe('getCanPin', () => { + it('should return true by default', () => { + const table = createRowPinningTable() + const row = table.getRow(ROW[0]) + + expect(row.getCanPin()).toBe(true) + }) + + it('should return false when enableRowPinning is false', () => { + const table = createRowPinningTable({ + _features: {}, + enableRowPinning: false, + }) + const row = table.getRow(ROW[0]) + + expect(row.getCanPin()).toBe(false) + }) + + it('should use enableRowPinning function when provided', () => { + const table = createRowPinningTable({ + _features: {}, + enableRowPinning: (row) => row.id === ROW[1], + }) + + expect(table.getRow(ROW[0]).getCanPin()).toBe(false) + expect(table.getRow(ROW[1]).getCanPin()).toBe(true) + }) + }) + + describe('getIsPinned', () => { + it('should return false when row is not pinned', () => { + const table = createRowPinningTable() + const row = table.getRow(ROW[0]) + + expect(row.getIsPinned()).toBe(false) + }) + + it('should return correct position when row is pinned', () => { + const table = createRowPinningTable({ + _features: {}, + initialState: { + rowPinning: { + top: [ROW[0]], + bottom: [ROW[1]], + }, + }, + }) + + expect(table.getRow(ROW[0]).getIsPinned()).toBe('top') + expect(table.getRow(ROW[1]).getIsPinned()).toBe('bottom') + }) + }) + + describe('getPinnedIndex', () => { + it('should return -1 when row is not pinned', () => { + const table = createRowPinningTable() + const row = table.getRow(ROW[0]) + + expect(row.getPinnedIndex()).toBe(-1) + }) + + it('should return correct index for pinned rows', () => { + const table = createRowPinningTable({ + _features: {}, + initialState: { + rowPinning: { + top: [ROW[0], ROW[1]], + bottom: [ROW[2]], + }, + }, + }) + + expect(table.getRow(ROW[0]).getPinnedIndex()).toBe(0) + expect(table.getRow(ROW[1]).getPinnedIndex()).toBe(1) + expect(table.getRow(ROW[2]).getPinnedIndex()).toBe(0) + }) + }) + + describe('pin', () => { + it('should pin row to specified position', () => { + const table = createRowPinningTable() + const row = table.getRow(ROW[0]) + + row.pin('top') + expect(table.getState().rowPinning.top).toEqual([ROW[0]]) + + row.pin('bottom') + expect(table.getState().rowPinning.bottom).toEqual([ROW[0]]) + expect(table.getState().rowPinning.top).toEqual([]) + }) + + it('should unpin row when position is false', () => { + const table = createRowPinningTable({ + _features: {}, + initialState: { + rowPinning: { + top: [ROW[0]], + bottom: [], + }, + }, + }) + const row = table.getRow(ROW[0]) + + row.pin(false) + expect(table.getState().rowPinning).toEqual(EMPTY_PINNING_STATE) + }) + + it('should include leaf rows when includeLeafRows is true', () => { + const table = createRowPinningTable({ _features: {} }, [10, 2]) + const row = table.getRow(ROW[0]) + + // Mock leaf rows by pinning multiple rows + row.pin('top', true) + + // Verify the row was pinned + expect(table.getState().rowPinning.top).toContain(ROW[0]) + + // Verify the leaf rows were pinned + expect(table.getState().rowPinning.top).toContain(SUB_ROW[0]) + expect(table.getState().rowPinning.top).toContain(SUB_ROW[1]) + }) + + it('should include parent rows when includeParentRows is true', () => { + const table = createRowPinningTable({ _features: {} }, [10, 5]) + + const row = table.getRow(SUB_ROW[0]) + + row.pin('top', false, true) + + // Verify the row was pinned + expect(table.getState().rowPinning.top).toContain(SUB_ROW[0]) + + // Verify the parent row was pinned + expect(table.getState().rowPinning.top).toContain(ROW[0]) + }) + }) +}) diff --git a/packages/table-core/tests/makeTestData.ts b/packages/table-core/tests/makeTestData.ts index 331dd1eb19..b2db77cb6d 100644 --- a/packages/table-core/tests/makeTestData.ts +++ b/packages/table-core/tests/makeTestData.ts @@ -7,11 +7,11 @@ export type Person = { visits: number progress: number status: 'relationship' | 'complicated' | 'single' - subRows?: Person[] + subRows?: Array } const range = (len: number) => { - const arr: number[] = [] + const arr: Array = [] for (let i = 0; i < len; i++) { arr.push(i) } @@ -33,8 +33,8 @@ const newPerson = (): Person => { } } -export function makeData(...lens: number[]) { - const makeDataLevel = (depth = 0): Person[] => { +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { const len = lens[depth]! return range(len).map((d): Person => { return { diff --git a/packages/table-core/tests/unit/features/row-pinning/rowPinningFeature.utils.test.ts b/packages/table-core/tests/unit/features/row-pinning/rowPinningFeature.utils.test.ts new file mode 100644 index 0000000000..57b79040c0 --- /dev/null +++ b/packages/table-core/tests/unit/features/row-pinning/rowPinningFeature.utils.test.ts @@ -0,0 +1,591 @@ +import { describe, expect, it, vi } from 'vitest' +import { + getDefaultRowPinningState, + row_getCanPin, + row_getIsPinned, + row_getPinnedIndex, + row_pin, + table_getBottomRows, + table_getCenterRows, + table_getIsSomeRowsPinned, + table_getTopRows, + table_resetRowPinning, + table_setRowPinning, +} from '../../../../src/features/row-pinning/rowPinningFeature.utils' +import { createTestTableWithData } from '../../../helpers/createTestTable' +import { getUpdaterResult } from '../../../helpers/testUtils' +import { + createTableWithMockOnPinningChange, + createTableWithPinningState, +} from '../../../helpers/rowPinningHelpers' +import type { Row } from '../../../../src' +import type { Person } from '../../../fixtures/data/types' + +const DEFAULT_ROW_COUNT = 10 + +const ROW = { + 0: '0', + 1: '1', + 2: '2', + 8: '8', + 9: '9', +} as const + +const LEAF = { + 1: 'leaf1', + 2: 'leaf2', +} as const + +const PARENT = { + 1: 'parent1', + 2: 'parent2', +} as const + +const EMPTY_PINNING_STATE = { + top: [], + bottom: [], +} + +describe('getDefaultRowPinningState', () => { + it('should return default row pinning state with empty top and bottom arrays', () => { + const defaultState = getDefaultRowPinningState() + + expect(defaultState).toEqual({ + top: [], + bottom: [], + }) + + // Ensure we get a new object instance each time + const secondState = getDefaultRowPinningState() + expect(secondState).toEqual(defaultState) + expect(secondState).not.toBe(defaultState) + }) +}) + +describe('table_setRowPinning', () => { + it('should call onRowPinningChange with the updater function', () => { + const { table, onRowPinningChangeMock } = + createTableWithMockOnPinningChange() + + const newState = { + top: [ROW[1]], + bottom: [ROW[2]], + } + + table_setRowPinning(table, newState) + + expect(onRowPinningChangeMock).toHaveBeenCalledTimes(1) + expect(onRowPinningChangeMock).toHaveBeenCalledWith(newState) + }) + + it('should handle undefined onRowPinningChange without error', () => { + const table = createTestTableWithData(DEFAULT_ROW_COUNT) + + expect(() => { + table_setRowPinning(table, EMPTY_PINNING_STATE) + }).not.toThrow() + }) +}) + +describe('table_resetRowPinning', () => { + it('should reset to default state when defaultState is true', () => { + const { table, onRowPinningChangeMock } = + createTableWithMockOnPinningChange() + + table_resetRowPinning(table, true) + + expect(onRowPinningChangeMock).toHaveBeenCalledTimes(1) + expect(onRowPinningChangeMock).toHaveBeenCalledWith( + getDefaultRowPinningState(), + ) + }) + + it('should reset to initial state when defaultState is false', () => { + const { table, onRowPinningChangeMock } = + createTableWithMockOnPinningChange() + const initialState = { + top: [ROW[1]], + bottom: [ROW[2]], + } + table.initialState.rowPinning = initialState + + table_resetRowPinning(table, false) + + expect(onRowPinningChangeMock).toHaveBeenCalledTimes(1) + expect(onRowPinningChangeMock).toHaveBeenCalledWith(initialState) + }) + + it('should reset to default state when no initial state exists', () => { + const { table, onRowPinningChangeMock } = + createTableWithMockOnPinningChange() + + table_resetRowPinning(table, false) + + expect(onRowPinningChangeMock).toHaveBeenCalledTimes(1) + expect(onRowPinningChangeMock).toHaveBeenCalledWith( + getDefaultRowPinningState(), + ) + }) +}) + +describe('table_getIsSomeRowsPinned', () => { + it('should return false when no rows are pinned', () => { + const table = createTableWithPinningState() + + expect(table_getIsSomeRowsPinned(table)).toBe(false) + expect(table_getIsSomeRowsPinned(table, 'top')).toBe(false) + expect(table_getIsSomeRowsPinned(table, 'bottom')).toBe(false) + }) + + it('should return true when rows are pinned to top', () => { + const table = createTableWithPinningState(10, { + top: [ROW[0]], + bottom: [], + }) + + expect(table_getIsSomeRowsPinned(table)).toBe(true) + expect(table_getIsSomeRowsPinned(table, 'top')).toBe(true) + expect(table_getIsSomeRowsPinned(table, 'bottom')).toBe(false) + }) + + it('should return true when rows are pinned to bottom', () => { + const table = createTableWithPinningState(10, { + top: [], + bottom: [ROW[0]], + }) + + expect(table_getIsSomeRowsPinned(table)).toBe(true) + expect(table_getIsSomeRowsPinned(table, 'top')).toBe(false) + expect(table_getIsSomeRowsPinned(table, 'bottom')).toBe(true) + }) + + it('should handle undefined state', () => { + const table = createTableWithPinningState() + table.options.state = undefined + + expect(table_getIsSomeRowsPinned(table)).toBe(false) + expect(table_getIsSomeRowsPinned(table, 'top')).toBe(false) + expect(table_getIsSomeRowsPinned(table, 'bottom')).toBe(false) + }) +}) + +describe('table_getTopRows and table_getBottomRows', () => { + it('should return empty arrays when no rows are pinned', () => { + const table = createTestTableWithData(10) + table.options.state = { + rowPinning: getDefaultRowPinningState(), + } + + expect(table_getTopRows(table)).toEqual([]) + expect(table_getBottomRows(table)).toEqual([]) + }) + + it('should return pinned rows with position property', () => { + const table = createTestTableWithData(10) + const row0 = table.getRow('0', true) + const row1 = table.getRow('1', true) + + table.options.state = { + rowPinning: { + top: [ROW[0]], + bottom: [ROW[1]], + }, + } + + const topRows = table_getTopRows(table) + const bottomRows = table_getBottomRows(table) + + expect(topRows).toHaveLength(1) + expect(topRows[0]).toEqual({ ...row0, position: 'top' }) + + expect(bottomRows).toHaveLength(1) + expect(bottomRows[0]).toEqual({ ...row1, position: 'bottom' }) + }) + + it('should handle keepPinnedRows=false by only returning visible rows', () => { + const table = createTestTableWithData(10) + table.options.keepPinnedRows = false + + // Setup a row model with only some rows visible + const visibleRows = table.getRowModel().rows.slice(0, 5) + vi.spyOn(table, 'getRowModel').mockReturnValue({ + rows: visibleRows, + flatRows: [], + rowsById: {}, + }) + + table.options.state = { + rowPinning: { + top: [ROW[0], ROW[8]], // '0' is visible, '8' is not + bottom: [ROW[1], ROW[9]], // '1' is visible, '9' is not + }, + } + + const topRows = table_getTopRows(table) + const bottomRows = table_getBottomRows(table) + + expect(topRows).toHaveLength(1) + expect(topRows[0]?.id).toBe(ROW[0]) + + expect(bottomRows).toHaveLength(1) + expect(bottomRows[0]?.id).toBe(ROW[1]) + }) + + it('should handle keepPinnedRows=true by returning all pinned rows regardless of visibility', () => { + const table = createTestTableWithData(10) + table.options.keepPinnedRows = true + + // Setup a row model with only some rows visible + const visibleRows = table.getRowModel().rows.slice(0, 5) + vi.spyOn(table, 'getRowModel').mockReturnValue({ + rows: visibleRows, + flatRows: [], + rowsById: {}, + }) + + table.options.state = { + rowPinning: { + top: [ROW[0], ROW[8]], // '0' is visible, '8' is not + bottom: [ROW[1], ROW[9]], // '1' is visible, '9' is not + }, + } + + const topRows = table_getTopRows(table) + const bottomRows = table_getBottomRows(table) + + expect(topRows).toHaveLength(2) + expect(topRows.map((r) => r.id)).toEqual([ROW[0], ROW[8]]) + + expect(bottomRows).toHaveLength(2) + expect(bottomRows.map((r) => r.id)).toEqual([ROW[1], ROW[9]]) + }) + + it('should handle undefined state', () => { + const table = createTestTableWithData(10) + table.options.state = undefined + + expect(table_getTopRows(table)).toEqual([]) + expect(table_getBottomRows(table)).toEqual([]) + }) +}) + +describe('table_getCenterRows', () => { + it('should return all rows when no rows are pinned', () => { + const table = createTestTableWithData(10) + table.options.state = { + rowPinning: getDefaultRowPinningState(), + } + + const allRows = table.getRowModel().rows + const centerRows = table_getCenterRows(table) + + expect(centerRows).toEqual(allRows) + }) + + it('should return only unpinned rows when some rows are pinned', () => { + const table = createTestTableWithData(10) + const allRows = table.getRowModel().rows + + table.options.state = { + rowPinning: { + top: [ROW[0], ROW[1]], + bottom: [ROW[8], ROW[9]], + }, + } + + const centerRows = table_getCenterRows(table) + + expect(centerRows).toEqual(allRows.slice(2, 8)) + const rowIds = [ROW[0], ROW[1], ROW[8], ROW[9]] as Array + + expect(centerRows.every((row) => !rowIds.includes(row.id))).toBe(true) + }) + + it('should handle undefined state', () => { + const table = createTestTableWithData(10) + const allRows = table.getRowModel().rows + table.options.state = undefined + + const centerRows = table_getCenterRows(table) + + expect(centerRows).toEqual(allRows) + }) +}) + +describe('row_getCanPin', () => { + it('should return true when enableRowPinning is undefined', () => { + const table = createTestTableWithData(10) + const row = table.getRow('0') + + expect(row_getCanPin(row)).toBe(true) + }) + + it('should return false when enableRowPinning is false', () => { + const table = createTestTableWithData(10) + table.options.enableRowPinning = false + + const row = table.getRow('0') + + expect(row_getCanPin(row)).toBe(false) + }) + + it('should return true when enableRowPinning is true', () => { + const table = createTestTableWithData(10) + table.options.enableRowPinning = true + + const row = table.getRow('0') + + expect(row_getCanPin(row)).toBe(true) + }) + + it('should use enableRowPinning function when provided', () => { + const enableRowPinning = vi.fn((row) => row.id === '1') + const table = createTestTableWithData(10) + + table.options.enableRowPinning = enableRowPinning + + const row0 = table.getRow('0') + const row1 = table.getRow('1') + + expect(row_getCanPin(row0)).toBe(false) + expect(row_getCanPin(row1)).toBe(true) + expect(enableRowPinning).toHaveBeenCalledTimes(2) + }) +}) + +describe('row_getIsPinned', () => { + it('should return false when no rows are pinned', () => { + const table = createTestTableWithData(10) + table.options.state = { + rowPinning: getDefaultRowPinningState(), + } + + const row = table.getRow('0') + expect(row_getIsPinned(row)).toBe(false) + }) + + it('should return "top" when row is pinned to top', () => { + const table = createTestTableWithData(10) + table.options.state = { + rowPinning: { + top: [ROW[0]], + bottom: [], + }, + } + + const row = table.getRow('0') + expect(row_getIsPinned(row)).toBe('top') + }) + + it('should return "bottom" when row is pinned to bottom', () => { + const table = createTestTableWithData(10) + table.options.state = { + rowPinning: { + top: [], + bottom: [ROW[0]], + }, + } + + const row = table.getRow('0') + expect(row_getIsPinned(row)).toBe('bottom') + }) + + it('should handle undefined state', () => { + const table = createTestTableWithData(10) + table.options.state = undefined + + const row = table.getRow('0') + expect(row_getIsPinned(row)).toBe(false) + }) +}) + +describe('row_getPinnedIndex', () => { + it('should return -1 when row is not pinned', () => { + const table = createTestTableWithData(10) + table.options.state = { + rowPinning: getDefaultRowPinningState(), + } + + const row = table.getRow('0') + expect(row_getPinnedIndex(row)).toBe(-1) + }) + + it('should return correct index for top pinned rows', () => { + const table = createTestTableWithData(10) + table.options.state = { + rowPinning: { + top: [ROW[0], ROW[1], ROW[2]], + bottom: [], + }, + } + + expect(row_getPinnedIndex(table.getRow('0'))).toBe(0) + expect(row_getPinnedIndex(table.getRow('1'))).toBe(1) + expect(row_getPinnedIndex(table.getRow('2'))).toBe(2) + }) + + it('should return correct index for bottom pinned rows', () => { + const table = createTestTableWithData(10) + table.options.state = { + rowPinning: { + top: [], + bottom: [ROW[0], ROW[1], ROW[2]], + }, + } + + expect(row_getPinnedIndex(table.getRow('0'))).toBe(0) + expect(row_getPinnedIndex(table.getRow('1'))).toBe(1) + expect(row_getPinnedIndex(table.getRow('2'))).toBe(2) + }) + + it('should handle undefined state', () => { + const table = createTestTableWithData(10) + table.options.state = undefined + + const row = table.getRow('0') + expect(row_getPinnedIndex(row)).toBe(-1) + }) +}) + +describe('row_pin', () => { + it('should pin a row to top', () => { + const { table, onRowPinningChangeMock } = + createTableWithMockOnPinningChange() + const row = table.getRow('0') + + row_pin(row, 'top') + + expect(onRowPinningChangeMock).toHaveBeenCalledTimes(1) + expect( + getUpdaterResult(onRowPinningChangeMock, { top: [], bottom: [] }), + ).toEqual({ + top: [ROW[0]], + bottom: [], + }) + }) + + it('should pin a row to bottom', () => { + const { table, onRowPinningChangeMock } = + createTableWithMockOnPinningChange() + const row = table.getRow('0') + + row_pin(row, 'bottom') + + expect(onRowPinningChangeMock).toHaveBeenCalledTimes(1) + expect( + getUpdaterResult(onRowPinningChangeMock, { top: [], bottom: [] }), + ).toEqual({ + top: [], + bottom: [ROW[0]], + }) + }) + + it('should unpin a row when position is false', () => { + const { table, onRowPinningChangeMock } = + createTableWithMockOnPinningChange() + table.options.state = { + rowPinning: { + top: [ROW[0]], + bottom: [], + }, + } + const row = table.getRow('0') + + row_pin(row, false) + + expect(onRowPinningChangeMock).toHaveBeenCalledTimes(1) + expect( + getUpdaterResult(onRowPinningChangeMock, { top: [ROW[0]], bottom: [] }), + ).toEqual({ + top: [], + bottom: [], + }) + }) + + it('should include leaf rows when includeLeafRows is true', () => { + const { table, onRowPinningChangeMock } = + createTableWithMockOnPinningChange() + const row = table.getRow('0') + const leafRows = [{ id: LEAF[1] }, { id: LEAF[2] }] + vi.spyOn(row, 'getLeafRows').mockReturnValue( + leafRows as unknown as Array>, + ) + + row_pin(row, 'top', true) + + expect(onRowPinningChangeMock).toHaveBeenCalledTimes(1) + expect( + getUpdaterResult(onRowPinningChangeMock, { top: [], bottom: [] }), + ).toEqual({ + top: [ROW[0], LEAF[1], LEAF[2]], + bottom: [], + }) + }) + + it('should include parent rows when includeParentRows is true', () => { + const { table, onRowPinningChangeMock } = + createTableWithMockOnPinningChange() + const row = table.getRow('0') + const parentRows = [{ id: PARENT[1] }, { id: PARENT[2] }] + vi.spyOn(row, 'getParentRows').mockReturnValue( + parentRows as unknown as Array>, + ) + + row_pin(row, 'top', false, true) + + expect(onRowPinningChangeMock).toHaveBeenCalledTimes(1) + expect( + getUpdaterResult(onRowPinningChangeMock, { top: [], bottom: [] }), + ).toEqual({ + top: [PARENT[1], PARENT[2], ROW[0]], + bottom: [], + }) + }) + + it('should maintain existing pinned rows when pinning additional rows', () => { + const { table, onRowPinningChangeMock } = + createTableWithMockOnPinningChange() + table.options.state = { + rowPinning: { + top: [ROW[1]], + bottom: [ROW[2]], + }, + } + const row = table.getRow('0') + + row_pin(row, 'top') + + expect(onRowPinningChangeMock).toHaveBeenCalledTimes(1) + expect( + getUpdaterResult(onRowPinningChangeMock, { + top: [ROW[1]], + bottom: [ROW[2]], + }), + ).toEqual({ + top: [ROW[1], ROW[0]], + bottom: [ROW[2]], + }) + }) + + it('should remove row from other position when moving between top and bottom', () => { + const { table, onRowPinningChangeMock } = + createTableWithMockOnPinningChange() + table.options.state = { + rowPinning: { + top: [ROW[0]], + bottom: [], + }, + } + const row = table.getRow('0') + + row_pin(row, 'bottom') + + expect(onRowPinningChangeMock).toHaveBeenCalledTimes(1) + expect( + getUpdaterResult(onRowPinningChangeMock, { top: [ROW[0]], bottom: [] }), + ).toEqual({ + top: [], + bottom: [ROW[0]], + }) + }) +}) diff --git a/packages/table-core/vite.config.ts b/packages/table-core/vite.config.ts index 86da2b3f25..7f28c9d9c5 100644 --- a/packages/table-core/vite.config.ts +++ b/packages/table-core/vite.config.ts @@ -8,7 +8,7 @@ const config = defineConfig({ dir: './', watch: false, environment: 'jsdom', - setupFiles: ['./tests/test-setup.ts'], + setupFiles: ['./tests/fixtures/setup/test-setup.ts'], globals: true, }, })