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