import assert from 'node:assert/strict'; import test from 'node:test'; import { createTable, getCoreRowModel } from '@tanstack/table-core'; import { getCellMemoResetKey, getPinnedLayoutKey, pickSettledSizing } from './cellMemoReset.js'; const base = { columnPinning: { left: ['mrt-row-expand', 'sort_order'], right: [] }, columnVisibility: {}, columnSizing: {}, globalFilter: '', columnFilters: [], }; const key = (patch = {}) => getCellMemoResetKey({ ...base, ...patch }); test('закрепление колонки слева и справа меняет ключ', () => { const k0 = key(); assert.notEqual(key({ columnPinning: { left: [...base.columnPinning.left, 'data.q1'], right: [] } }), k0); assert.notEqual(key({ columnPinning: { left: base.columnPinning.left, right: ['data.q4'] } }), k0); }); test('открепление колонки меняет ключ', () => { const pinned = { left: ['mrt-row-expand', 'sort_order', 'data.q1'], right: [] }; assert.notEqual(key({ columnPinning: pinned }), key({ columnPinning: base.columnPinning })); }); test('скрытие и показ закреплённой колонки меняют ключ', () => { const visible = key({ columnVisibility: { sort_order: true } }); const hidden = key({ columnVisibility: { sort_order: false } }); assert.notEqual(hidden, visible); assert.equal(key({ columnVisibility: { sort_order: true } }), visible); }); test('ширина закреплённой колонки меняет ключ', () => { assert.notEqual(key({ columnSizing: { sort_order: 120 } }), key({ columnSizing: { sort_order: 160 } })); assert.notEqual(key({ columnSizing: {} }), key({ columnSizing: { sort_order: 160 } })); }); test('ширина, скрытие и показ незакреплённой колонки не меняют ключ', () => { const k0 = key(); assert.equal(key({ columnSizing: { 'data.q1': 300 } }), k0); assert.equal(key({ columnVisibility: { 'data.q1': false } }), k0); assert.equal(key({ columnVisibility: { 'data.q1': true } }), k0); }); test('id без записи в columnVisibility считается видимой колонкой', () => { assert.equal(key({ columnVisibility: {} }), key({ columnVisibility: { sort_order: true, 'mrt-row-expand': true } })); }); test('данные не входят в ключ', () => { assert.equal(key({ data: [{ id: 1 }] }), key({ data: [{ id: 2 }] })); }); test('globalFilter: "" -> "abc" -> "abd" -> ""', () => { const [k0, k1, k2, k3] = ['', 'abc', 'abd', ''].map((globalFilter) => key({ globalFilter })); assert.notEqual(k1, k0); assert.notEqual(k2, k1); assert.equal(k3, k0); }); test('globalFilter null и undefined равны пустой строке', () => { assert.equal(key({ globalFilter: null }), key()); assert.equal(key({ globalFilter: undefined }), key()); }); test('фильтр колонки: значение, диапазон [min, max] и очистка меняют ключ', () => { const none = key(); const keys = [ none, key({ columnFilters: [{ id: 'name', value: 'ab' }] }), key({ columnFilters: [{ id: 'name', value: 'abc' }] }), key({ columnFilters: [{ id: 'data.q1', value: ['1', ''] }] }), key({ columnFilters: [{ id: 'data.q1', value: ['1', '5'] }] }), ]; assert.equal(new Set(keys).size, keys.length); assert.equal(key({ columnFilters: [] }), none); }); test('новый массив columnFilters с теми же значениями в другом порядке не меняет ключ', () => { const a = { id: 'name', value: 'ab' }; const b = { id: 'data.q1', value: ['1', '5'] }; assert.equal(key({ columnFilters: [a, b] }), key({ columnFilters: [{ ...b }, { ...a }] })); }); test('неполный columnPinning из localStorage не ломает ключ', () => { const layout = { columnVisibility: undefined, columnSizing: undefined }; assert.equal( getPinnedLayoutKey({ ...layout, columnPinning: { left: ['a'] } }), getPinnedLayoutKey({ ...layout, columnPinning: { left: ['a'], right: [] } }), ); assert.doesNotThrow(() => getPinnedLayoutKey({ ...layout, columnPinning: undefined })); assert.doesNotThrow(() => getCellMemoResetKey({})); }); test('пока ширину тянут, ключ прежний; после отпускания — по новым ширинам', () => { const s0 = { sort_order: 120 }; const s1 = { sort_order: 150 }; const s2 = { sort_order: 180 }; const settled = pickSettledSizing({ previous: s0, current: s0, isResizing: false }); const k0 = key({ columnSizing: settled }); const during = pickSettledSizing({ previous: settled, current: s1, isResizing: true }); assert.equal(during, s0); assert.equal(key({ columnSizing: during }), k0); const after = pickSettledSizing({ previous: during, current: s2, isResizing: false }); assert.equal(after, s2); assert.notEqual(key({ columnSizing: after }), k0); assert.equal(key({ columnSizing: after }), key({ columnSizing: s2 })); }); // Допущение @tanstack/table-core 8.20.5, на котором стоит явный сброс // (openspec/changes/aurora-1839-cell-memo/design.md, «Контекст», п. 3; D1). // Тесты ниже фиксируют характеристику table-core 8.20.5 (упадут при обновлении библиотеки): // новый массив columns даёт новые ячейки; смена columnSizing ячейки не пересоздаёт (поэтому ключ нужен для ширины // закреплённой колонки); смена columnPinning пересоздаёт ячейки закреплённых сторон, центральные те же. const createFixture = () => createTable({ data: [{ a: 1, b: 2, c: 3 }], columns: [ { accessorKey: 'a', size: 50 }, { accessorKey: 'b', size: 100 }, { accessorKey: 'c', size: 150 }, ], state: { columnPinning: { left: [], right: [] }, columnSizing: {}, columnOrder: [], columnVisibility: {} }, getCoreRowModel: getCoreRowModel(), }); const updateState = (table, state) => table.setOptions((options) => ({ ...options, state: { ...options.state, ...state } })); const cellsById = (table) => new Map( table .getRowModel() .rows[0].getVisibleCells() .map((cell) => [cell.id, cell]), ); test('table-core: новый массив columns с теми же определениями даёт новые ячейки', () => { const table = createFixture(); const before = cellsById(table); table.setOptions((options) => ({ ...options, columns: [...options.columns] })); const after = cellsById(table); assert.equal(after.size, before.size); for (const [id, cell] of after) assert.notEqual(cell, before.get(id)); }); // В table-core 8.20.5 getLeftVisibleCells/getRightVisibleCells (features/ColumnPinning.js) возвращают // поверхностные копии `{...cell, position}`, поэтому при смене закрепления пересоздаются ячейки закреплённых // колонок при прежнем массиве columns; центральные остаются теми же объектами. // Ключ сброса по закреплению нужен из-за ширины закреплённой колонки (смена columnSizing ячейки не пересоздаёт) // и как страховка на случай смены поведения библиотеки. test('table-core: смена columnPinning при прежнем массиве columns пересоздаёт ячейку закреплённой колонки, остальные те же', () => { const table = createFixture(); const before = cellsById(table); updateState(table, { columnPinning: { left: ['c'], right: [] } }); const after = cellsById(table); assert.equal(after.size, before.size); assert.notEqual(after.get('0_c'), before.get('0_c')); for (const id of ['0_a', '0_b']) assert.equal(after.get(id), before.get(id)); }); test('table-core: смена columnSizing при прежнем массиве columns оставляет те же ячейки', () => { const table = createFixture(); const before = cellsById(table); updateState(table, { columnSizing: { b: 120, c: 20 } }); const after = cellsById(table); assert.equal(after.size, before.size); for (const [id, cell] of after) assert.equal(cell, before.get(id)); }); test('table-core: закрепление второй колонки слева пересоздаёт все закреплённые ячейки, центральная та же', () => { const table = createFixture(); updateState(table, { columnPinning: { left: ['a'], right: [] } }); const before = cellsById(table); updateState(table, { columnPinning: { left: ['a', 'c'], right: [] } }); const after = cellsById(table); assert.notEqual(after.get('0_a'), before.get('0_a')); assert.notEqual(after.get('0_c'), before.get('0_c')); assert.equal(after.get('0_b'), before.get('0_b')); }); test('table-core: скрытие закреплённой колонки пересоздаёт ячейки оставшейся закреплённой стороны', () => { const table = createFixture(); updateState(table, { columnPinning: { left: ['a', 'c'], right: [] } }); const before = cellsById(table); updateState(table, { columnVisibility: { a: false } }); const after = cellsById(table); assert.equal(after.has('0_a'), false); assert.notEqual(after.get('0_c'), before.get('0_c')); });