- ColumnResizer: handleMouseMove завершает перетаскивание, если левая кнопка не зажата - cellMemoReset.test: убран дубль теста columnSizing, добавлены характеристики закрепления второй колонки и скрытия закреплённой - комментарии к columnFilters (триггер React.memo(TableHead)) и ссылки на архивную спеку Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
189 lines
9.3 KiB
JavaScript
189 lines
9.3 KiB
JavaScript
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'));
|
||
});
|