158 lines
6.1 KiB
JavaScript
158 lines
6.1 KiB
JavaScript
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/);
|
|
});
|