DFiP_Budget_planing/web/src/pages/Svod2Page/svod2Paging.test.js
Andrey Epifancev 78e06583f4 perf(web): свод 2 — состояние вкладки с поколениями запросов
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 22:21:04 +04:00

259 lines
11 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 {
EMPTY_FACET_MAP,
PAGE_CACHE_LIMIT,
buildSvod2PageParams,
buildSvod2QueryParams,
buildWindowRows,
createSheetPagingState,
facetsToMaps,
getMissingPages,
getPageCount,
getSvod2RowId,
getVirtualizerPadding,
getVisiblePage,
getWindowPages,
getWindowRange,
isPlaceholderRow,
shouldShowFound,
svod2PagingReducer,
touchPage,
} from './svod2Paging.js';
test('окно в начале — страницы 0 и 1 (решение #15)', () => assert.deepEqual(getWindowPages(0, 5563), [0, 1]));
test('окно в середине и в конце', () => {
assert.deepEqual(getWindowPages(5, 5563), [4, 5, 6]);
assert.deepEqual(getWindowPages(18, 5563), [17, 18]);
});
test('total = 0 — пустое окно и пустой срез', () => {
assert.deepEqual(getWindowPages(0, 0), []);
assert.deepEqual(buildWindowRows([], new Map(), 0), []);
assert.equal(getPageCount(0), 0);
assert.deepEqual(getWindowRange([], 0), { windowStart: 0, windowEnd: 0 });
});
test('видимая страница прижимается к последней при уменьшении total', () =>
assert.equal(getVisiblePage({ scrollTop: 200000 * 37, rowHeight: 37, total: 700 }), 2));
test('видимая страница: обычный скролл и total = 0', () => {
assert.equal(getVisiblePage({ scrollTop: 0, rowHeight: 37, total: 5563 }), 0);
assert.equal(getVisiblePage({ scrollTop: 300 * 37, rowHeight: 37, total: 5563 }), 1);
assert.equal(getVisiblePage({ scrollTop: 300 * 37 - 1, rowHeight: 37, total: 5563 }), 0);
assert.equal(getVisiblePage({ scrollTop: 5000, rowHeight: 37, total: 0 }), 0);
});
test('срез: неполная последняя страница и заглушки', () => {
const rows = buildWindowRows([17, 18], new Map([[18, Array.from({ length: 163 }, (_, i) => ({ id: i }))]]), 5563);
assert.equal(rows.length, 300 + 163);
assert.deepEqual(rows[0], { __placeholder: true, index: 5100 });
assert.equal(rows[300].id, 0);
});
test('страница короче ожидаемой дополняется заглушками, длиннее — обрезается', () => {
// total изменился между запросами: страница 0 пришла с 250 строками, страница 1 — с 400
const short = Array.from({ length: 250 }, (_, i) => ({ id: i }));
const long = Array.from({ length: 400 }, (_, i) => ({ id: 1000 + i }));
const rows = buildWindowRows(
[0, 1],
new Map([
[0, short],
[1, long],
]),
600,
);
assert.equal(rows.length, 600);
assert.equal(rows[249].id, 249);
assert.deepEqual(rows[250], { __placeholder: true, index: 250 });
assert.deepEqual(rows[299], { __placeholder: true, index: 299 });
assert.equal(rows[300].id, 1000);
assert.equal(rows[599].id, 1299);
});
test('сумма отступов и строк окна = total * rowH', () => {
const { windowStart, windowEnd } = getWindowRange([4, 5, 6], 5563);
assert.equal(windowStart, 1200);
assert.equal(windowEnd, 2100);
const { paddingStart, paddingEnd } = getVirtualizerPadding({ windowStart, windowEnd, total: 5563, rowHeight: 37 });
assert.equal(paddingStart + (windowEnd - windowStart) * 37 + paddingEnd, 5563 * 37);
});
test('окно с последней неполной страницей: windowEnd = total, нижний отступ 0', () => {
const { windowStart, windowEnd } = getWindowRange([17, 18], 5563);
assert.equal(windowEnd, 5563);
assert.deepEqual(getVirtualizerPadding({ windowStart, windowEnd, total: 5563, rowHeight: 37 }).paddingEnd, 0);
});
test('LRU: не больше 5 страниц, страницы окна не вытесняются', () => {
let cache = new Map();
for (let p = 0; p < 7; p++) cache = touchPage(cache, p, [{ id: p }], [0, 1]);
assert.equal(PAGE_CACHE_LIMIT, 5);
assert.equal(cache.size, 5);
assert.ok(cache.has(0));
assert.ok(cache.has(1));
assert.deepEqual(
[...cache.keys()].sort((a, b) => a - b),
[0, 1, 4, 5, 6],
);
});
test('LRU: повторное обращение делает страницу самой свежей, исходный Map не мутируется', () => {
const base = new Map([
[1, []],
[2, []],
]);
const next = touchPage(base, 1, [{ id: 'x' }]);
assert.notEqual(next, base);
assert.deepEqual([...base.keys()], [1, 2]);
assert.deepEqual([...next.keys()], [2, 1]);
assert.deepEqual(next.get(1), [{ id: 'x' }]);
});
test('getMissingPages исключает загруженные и летящие', () =>
assert.deepEqual(getMissingPages([4, 5, 6], new Map([[5, []]]), new Set([6])), [4]));
test('параметры страницы: compact, пустые не передаются, search как введён', () => {
assert.deepEqual(buildSvod2PageParams({ orgIds: [], search: '', columnFilters: [], offset: 300 }), {
view: 'compact',
offset: 300,
limit: 300,
});
assert.deepEqual(
buildSvod2QueryParams({
orgIds: [3, 5],
search: ' аренда ',
columnFilters: [
{ id: 'org_name', value: 'Филиал Ёлки' },
{ id: 'name', value: 'x' },
],
}),
{ org_ids: '3,5', search: ' аренда ', org_name: 'Филиал Ёлки' },
);
assert.deepEqual(
buildSvod2QueryParams({
columnFilters: [
{ id: 'smeta_type', value: '' },
{ id: 'smeta_direction', value: null },
],
}),
{},
);
assert.equal(buildSvod2PageParams({ offset: 0, limit: 50, search: 'a' }).limit, 50);
});
test('facetsToMaps', () => {
const result = { smeta_type: [{ value: 'КВ', count: 3 }], smeta_direction: [], org_name: [] };
const maps = facetsToMaps(result);
assert.equal(maps.get('smeta_type').get('КВ'), 3);
assert.equal(maps.get('org_name').size, 0);
assert.notEqual(facetsToMaps(result), maps);
assert.notEqual(facetsToMaps(result).get('smeta_type'), maps.get('smeta_type'));
assert.equal(facetsToMaps(null).get('org_name').size, 0);
assert.ok(Object.isFrozen(EMPTY_FACET_MAP));
});
test('«Найдено» — только поиск', () => {
assert.equal(shouldShowFound({ hasCurrentData: true, loading: false, facets: { total: 5563, search_total: 300 } }), true);
assert.equal(shouldShowFound({ hasCurrentData: true, loading: false, facets: { total: 5563, search_total: 5563 } }), false);
assert.equal(shouldShowFound({ hasCurrentData: true, loading: true, facets: { total: 5563, search_total: 300 } }), false);
assert.equal(shouldShowFound({ hasCurrentData: false, loading: false, facets: { total: 5563, search_total: 300 } }), false);
assert.equal(shouldShowFound({ hasCurrentData: true, loading: false, facets: null }), false);
});
test('getSvod2RowId', () => {
assert.equal(getSvod2RowId({ __placeholder: true, index: 7 }), 'ph-7');
assert.equal(getSvod2RowId({ data: { header: { source_form: 'f1', source_id: 2 }, line_id: 3 } }), 'f1-2-3');
assert.equal(isPlaceholderRow({ __placeholder: true, index: 1 }), true);
assert.equal(isPlaceholderRow({ data: {} }), false);
assert.equal(isPlaceholderRow(undefined), false);
});
const begin = (s, generation, mode, queryKey = 'k') => svod2PagingReducer(s, { type: 'begin', generation, mode, queryKey });
const page = (s, generation, pageIndex, rows, total) =>
svod2PagingReducer(s, { type: 'page', generation, pageIndex, rows, total, keepPages: [pageIndex] });
test('начальное состояние вкладки', () => {
const s = createSheetPagingState();
assert.equal(s.generation, 0);
assert.equal(s.dataGeneration, 0);
assert.equal(s.total, 0);
assert.equal(s.facets, null);
assert.equal(s.loading, false);
assert.equal(s.error, '');
assert.equal(s.pages.size, 0);
});
test('устаревшие page/facets/fail игнорируются', () => {
let s = begin(createSheetPagingState(), 1, 'load');
s = begin(s, 2, 'refine');
for (const a of [
{ type: 'page', generation: 1, pageIndex: 0, rows: [], total: 9 },
{ type: 'facets', generation: 1, facets: {} },
{ type: 'fail', generation: 1, mode: 'load', message: 'x' },
{ type: 'settled', generation: 1 },
])
assert.equal(svod2PagingReducer(s, a), s);
});
test('load ставит loading и pendingQueryKey, сбрасывает error', () => {
const s = begin(createSheetPagingState(), 1, 'load', 'q1');
assert.equal(s.loading, true);
assert.equal(s.generation, 1);
assert.equal(s.pendingQueryKey, 'q1');
assert.equal(s.error, '');
});
test('refine оставляет прежние строки до первого ответа нового поколения', () => {
let s = begin(createSheetPagingState(), 1, 'load');
s = page(s, 1, 0, ['a', 'b'], 2);
s = svod2PagingReducer(s, { type: 'facets', generation: 1, facets: { total: 2 } });
s = svod2PagingReducer(s, { type: 'settled', generation: 1 });
s = begin(s, 2, 'refine', 'k2');
assert.deepEqual(s.pages.get(0), ['a', 'b']);
assert.equal(s.total, 2);
assert.deepEqual(s.facets, { total: 2 });
assert.equal(s.loading, false);
assert.equal(s.pendingQueryKey, 'k2');
s = page(s, 2, 0, ['c'], 1);
assert.deepEqual([...s.pages.keys()], [0]);
assert.deepEqual(s.pages.get(0), ['c']);
assert.equal(s.dataGeneration, 2);
});
test('первый ответ поколения ставит loadedQueryKey', () => {
let s = begin(createSheetPagingState(), 1, 'load', 'k');
assert.equal(s.loadedQueryKey, '');
s = page(s, 1, 0, ['a'], 1);
assert.equal(s.loadedQueryKey, 'k');
assert.equal(s.dataGeneration, 1);
});
test('ошибка догрузки (scroll) не меняет состояние, страница снова среди недостающих', () => {
let s = begin(createSheetPagingState(), 1, 'load');
s = page(s, 1, 0, ['a'], 600);
assert.equal(svod2PagingReducer(s, { type: 'fail', generation: s.generation, mode: 'scroll', message: 'x' }), s);
assert.deepEqual(getMissingPages([0, 1], s.pages, new Set()), [1]);
});
test('ошибка load очищает строки и ставит error', () => {
let s = begin(createSheetPagingState(), 1, 'load');
s = page(s, 1, 0, ['a'], 5);
s = svod2PagingReducer(s, { type: 'fail', generation: 1, mode: 'load', message: 'x' });
assert.equal(s.error, 'x');
assert.equal(s.pages.size, 0);
assert.equal(s.total, 0);
});
test('ошибка refine очищает строки и ставит error', () => {
let s = begin(createSheetPagingState(), 1, 'load');
s = page(s, 1, 0, ['a'], 5);
s = begin(s, 2, 'refine');
s = svod2PagingReducer(s, { type: 'fail', generation: 2, mode: 'refine', message: 'y' });
assert.equal(s.error, 'y');
assert.equal(s.pages.size, 0);
assert.equal(s.total, 0);
});
test('последний total поколения побеждает', () => {
let s = begin(createSheetPagingState(), 1, 'load');
s = page(s, 1, 0, ['a'], 5563);
s = page(s, 1, 1, ['b'], 5560);
assert.equal(s.total, 5560);
assert.equal(s.pages.size, 2);
});
test('facets и settled текущего поколения', () => {
let s = begin(createSheetPagingState(), 1, 'load');
s = svod2PagingReducer(s, { type: 'facets', generation: 1, facets: { total: 3 } });
assert.deepEqual(s.facets, { total: 3 });
s = svod2PagingReducer(s, { type: 'settled', generation: 1 });
assert.equal(s.loading, false);
});