const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const Module = require('node:module'); const test = require('node:test'); const ts = require('typescript'); const { loadTable } = require('./helpers/page-state.cjs'); const filename = path.join(__dirname, '../src/hooks/usePageState.ts'); function harness(pathname = '/accounts', storage = new Map()) { const slots = []; let cursor = 0; let currentPath = pathname; const effects = []; const useMemo = (factory, deps) => { const index = cursor++; if (!slots[index] || deps.some((value, i) => !Object.is(value, slots[index].deps[i]))) { slots[index] = { value: factory(), deps }; } return slots[index].value; }; const react = { useMemo, useCallback: (callback, deps) => useMemo(() => callback, deps), useState: (initial) => { const index = cursor++; if (!(index in slots)) slots[index] = typeof initial === 'function' ? initial() : initial; return [slots[index], (next) => { slots[index] = typeof next === 'function' ? next(slots[index]) : next; }]; }, useEffect: (callback, deps) => { const index = cursor++; if (!slots[index] || deps.some((value, i) => !Object.is(value, slots[index].deps[i]))) { slots[index] = { deps }; effects.push(callback); } }, }; const mocks = { react, '@umijs/max': { useLocation: () => ({ pathname: currentPath }) } }; const loaded = new Module(filename); loaded.filename = filename; loaded.require = (name) => { assert.ok(name in mocks, `Unexpected import: ${name}`); return mocks[name]; }; loaded._compile(ts.transpileModule(fs.readFileSync(filename, 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, }).outputText, filename); const table = loadTable(react, loaded.exports); return { storage, pathname(value) { currentPath = value; }, render(name = 'pagination', initial = { current: 1, pageSize: 20 }, isTable = false) { global.sessionStorage = { getItem: (key) => storage.get(key) ?? null, setItem: (key, value) => storage.set(key, value), }; cursor = 0; const result = isTable ? table.useTablePagination() : loaded.exports.usePageState(name, initial); while (effects.length) effects.shift()(); return result; }, renderTablePagination() { return this.render(undefined, undefined, true); }, }; } test.afterEach(() => { delete global.sessionStorage; }); for (const pathname of ['/accounts/managed', '/accounts/import', '/gateways', '/network-exits']) { test(`${pathname}: pagination restores after navigation and page-size changes reset to one`, () => { const h = harness(pathname); h.renderTablePagination().onChange(3, 20); h.renderTablePagination(); const restored = harness(pathname, h.storage); assert.equal(restored.renderTablePagination().current, 3); restored.renderTablePagination().onChange(3, 50); assert.equal(restored.renderTablePagination().current, 1); restored.renderTablePagination().onChange(2, 50); restored.renderTablePagination(); const reloaded = harness(pathname, h.storage).renderTablePagination(); assert.equal(reloaded.current, 2); assert.equal(reloaded.pageSize, 50); assert.equal(harness(`${pathname}/edit`, h.storage).renderTablePagination().current, 1); }); } test('page state restores pagination after unmount and refresh', () => { const first = harness(); const [, set] = first.render(); set({ current: 3, pageSize: 50 }); first.render(); assert.deepEqual(harness('/accounts', first.storage).render()[0], { current: 3, pageSize: 50 }); }); test('functional updates use the latest value and retain page size', () => { const h = harness(); const [, set] = h.render(); set((value) => ({ ...value, current: value.current + 1 })); set((value) => ({ ...value, current: value.current + 1 })); assert.equal(h.render()[0].current, 3); }); test('pages and browser tabs never share pagination', () => { const h = harness('/accounts'); const [, set] = h.render(); set({ current: 4, pageSize: 50 }); h.render(); assert.equal(harness('/gateways', h.storage).render()[0].current, 1); assert.equal(harness('/accounts').render()[0].current, 1); }); test('switching account routes restores the correct state on the first render', () => { const h = harness('/accounts/one'); const [, set] = h.render(); set({ current: 3, pageSize: 20 }); h.render(); h.pathname('/accounts/two'); const [second, setSecond] = h.render(); assert.equal(second.current, 1); setSecond((value) => ({ ...value, current: 2 })); h.render(); h.pathname('/accounts/one'); assert.equal(h.render()[0].current, 3); }); test('owned and competitor tabs independently retain filters and pagination', () => { const h = harness('/creator/comments'); const [, set] = h.render('owned', { hours: 24, page: 1 }); set({ hours: 6, page: 3 }); h.render('owned', { hours: 24, page: 1 }); const [other, setOther] = h.render('competitor', { hours: 24, page: 1 }); assert.deepEqual(other, { hours: 24, page: 1 }); setOther({ hours: 12, page: 2 }); h.render('competitor', { hours: 24, page: 1 }); assert.deepEqual(h.render('owned', { hours: 24, page: 1 })[0], { hours: 6, page: 3 }); }); test('zero, false, empty filters, and date strings restore without being replaced by defaults', () => { for (const value of [0, false, {}, { dates: ['2026-07-01', '2026-07-03'] }]) { const h = harness(); const [, set] = h.render('filters', { hours: 24 }); set(value); h.render('filters', { hours: 24 }); assert.deepEqual(harness('/accounts', h.storage).render('filters', { hours: 24 })[0], value); } }); test('corrupt storage and storage write errors are not silently ignored', () => { const h = harness(); h.render(); const key = [...h.storage.keys()][0]; h.storage.set(key, '{invalid'); assert.throws(() => harness('/accounts', h.storage).render(), SyntaxError); const failing = new Map(); failing.set = () => { throw new Error('storage unavailable'); }; assert.throws(() => harness('/accounts', failing).render(), /storage unavailable/); });