Files
creator-hub/web/tests/page-state.test.cjs
T
rogee 51091cf0c5
douyin-release-gate / verify (push) Failing after 18m33s
fix: preserve pagination and filters across list navigation
2026-10-07 14:41:11 +08:00

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