347 lines
16 KiB
JavaScript
347 lines
16 KiB
JavaScript
import assert from 'node:assert/strict';
|
||
import test from 'node:test';
|
||
import {
|
||
EMPTY_FACET_MAP,
|
||
PAGE_CACHE_LIMIT,
|
||
buildSvod2PageParams,
|
||
buildSvod2QueryParams,
|
||
buildWindowRows,
|
||
createSheetPagingState,
|
||
facetsToMaps,
|
||
getCurrentPages,
|
||
getMissingPages,
|
||
getPageCount,
|
||
getSkeletonWidth,
|
||
getSvod2RowId,
|
||
getVirtualizerPadding,
|
||
getVisiblePage,
|
||
getWindowPages,
|
||
getWindowRange,
|
||
isPlaceholderRow,
|
||
shouldFetchFirstPageFallback,
|
||
shouldRefine,
|
||
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);
|
||
});
|
||
|
||
test('begin(load) оставляет прежние pages/total/facets до первого ответа нового поколения', () => {
|
||
let s = begin(createSheetPagingState(), 1, 'load');
|
||
s = page(s, 1, 0, ['a'], 5);
|
||
s = svod2PagingReducer(s, { type: 'facets', generation: 1, facets: { total: 5 } });
|
||
s = begin(s, 2, 'load', 'k2');
|
||
assert.equal(s.loading, true);
|
||
assert.deepEqual(s.pages.get(0), ['a']);
|
||
assert.equal(s.total, 5);
|
||
assert.deepEqual(s.facets, { total: 5 });
|
||
s = page(s, 2, 0, ['z'], 1);
|
||
assert.deepEqual([...s.pages.keys()], [0]);
|
||
assert.deepEqual(s.pages.get(0), ['z']);
|
||
});
|
||
|
||
test('refine поверх load: loading снимает settled нового поколения, старого - игнорируется', () => {
|
||
let s = begin(createSheetPagingState(), 1, 'load');
|
||
s = begin(s, 2, 'refine');
|
||
assert.equal(s.loading, true);
|
||
assert.equal(svod2PagingReducer(s, { type: 'settled', generation: 1 }), s);
|
||
s = svod2PagingReducer(s, { type: 'settled', generation: 2 });
|
||
assert.equal(s.loading, false);
|
||
});
|
||
|
||
test('«Найдено» не показывается рядом с ошибкой', () => {
|
||
assert.equal(shouldShowFound({ hasCurrentData: true, loading: false, error: 'x', facets: { total: 5, search_total: 1 } }), false);
|
||
});
|
||
test('shouldRefine: на загруженных данных при смене условий', () => {
|
||
const base = { hasCurrentData: true, loading: false, pendingQueryKey: 'q', queryKey: 'q', dataRefineKey: 'a', refineKey: 'b' };
|
||
assert.equal(shouldRefine(base), true);
|
||
assert.equal(shouldRefine({ ...base, refineKey: 'a' }), false);
|
||
});
|
||
test('shouldRefine: поиск изменён во время первой загрузки (T12.7)', () => {
|
||
const base = { hasCurrentData: false, loading: true, pendingQueryKey: 'q', queryKey: 'q', dataRefineKey: 'a', refineKey: 'b' };
|
||
assert.equal(shouldRefine(base), true);
|
||
assert.equal(shouldRefine({ ...base, pendingQueryKey: 'old' }), false);
|
||
assert.equal(shouldRefine({ ...base, loading: false }), false);
|
||
});
|
||
test('shouldRefine: ключ A -> B -> A при изменённых фильтрах', () => {
|
||
// данные ключа A построены на фильтре 'a'; на B фильтры сброшены (''), возврат на A — данные устарели
|
||
const onA = { hasCurrentData: true, loading: false, pendingQueryKey: 'A', queryKey: 'A', dataRefineKey: 'a', refineKey: '' };
|
||
assert.equal(shouldRefine(onA), true);
|
||
const onB = { ...onA, hasCurrentData: false, queryKey: 'B' };
|
||
assert.equal(shouldRefine(onB), false);
|
||
});
|
||
test('shouldRefine: первое монтаж и смена вкладки без расхождения не запускают refine', () => {
|
||
const base = { hasCurrentData: false, loading: false, pendingQueryKey: '', queryKey: 'q', refineKey: 'a' };
|
||
assert.equal(shouldRefine({ ...base, dataRefineKey: undefined }), false);
|
||
assert.equal(shouldRefine({ ...base, hasCurrentData: true, dataRefineKey: 'a' }), false);
|
||
});
|
||
|
||
test('полосы загрузки: ширина детерминирована и в пределах колонки', () => {
|
||
for (const size of [60, 150, 400]) {
|
||
for (let i = 0; i < 20; i++) {
|
||
const w = getSkeletonWidth(size, i, 3);
|
||
assert.equal(w, getSkeletonWidth(size, i, 3));
|
||
assert.ok(w >= Math.round(size / 3) && w <= size, `${size}:${i}:${w}`);
|
||
}
|
||
}
|
||
assert.notEqual(getSkeletonWidth(400, 1, 0), getSkeletonWidth(400, 2, 0));
|
||
});
|
||
|
||
test('первая страница при смене условий: запасной offset=0, если запрошенная страница за концом нового списка', () => {
|
||
assert.equal(shouldFetchFirstPageFallback({ candidate: 0, total: 43 }), false);
|
||
assert.equal(shouldFetchFirstPageFallback({ candidate: 37, total: 43 }), true);
|
||
assert.equal(shouldFetchFirstPageFallback({ candidate: 37, total: 0 }), true);
|
||
assert.equal(shouldFetchFirstPageFallback({ candidate: 37, total: 22419 }), false);
|
||
assert.equal(shouldFetchFirstPageFallback({ candidate: 75, total: 22419 }), true);
|
||
});
|
||
|
||
test('смена поколения условий: пока нет ответа нового поколения, старые страницы не показываются (все строки окна — заглушки)', () => {
|
||
const pages = new Map([[0, [{ data: 1 }]]]);
|
||
assert.equal(getCurrentPages({ pages, dataGeneration: 3, generation: 3 }), pages);
|
||
const stale = getCurrentPages({ pages, dataGeneration: 3, generation: 4 });
|
||
assert.equal(stale.size, 0);
|
||
const rows = buildWindowRows([0], stale, 3);
|
||
assert.deepEqual(
|
||
rows.map((r) => isPlaceholderRow(r)),
|
||
[true, true, true],
|
||
);
|
||
});
|
||
test('до первой загрузки (поколение 0) страниц нет и показывать нечего', () => {
|
||
assert.equal(getCurrentPages({ pages: new Map(), dataGeneration: 0, generation: 0 }).size, 0);
|
||
});
|