Andrey Epifancev e0e460b6c2 fix(table): ресайз завершается при потерянном mouseup; тест-характеристики table-core; комментарии
- ColumnResizer: handleMouseMove завершает перетаскивание, если левая кнопка не зажата
- cellMemoReset.test: убран дубль теста columnSizing, добавлены характеристики закрепления второй колонки и скрытия закреплённой
- комментарии к columnFilters (триггер React.memo(TableHead)) и ссылки на архивную спеку

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 21:49:03 +04:00

189 lines
9.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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'));
});