Files
creator-hub/web/tests/monitoring-pagination.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

207 lines
8.1 KiB
JavaScript

const assert = require('node:assert/strict');
const { readFileSync } = require('node:fs');
const path = require('node:path');
const Module = require('node:module');
const test = require('node:test');
const ts = require('typescript');
const { loadPageState } = require('./helpers/page-state.cjs');
const listFile = path.join(__dirname, '../src/components/accounts/MonitoringAccountList.tsx');
const detailFile = path.join(__dirname, '../src/pages/accounts/monitoring/$id/index.tsx');
function harness(initialURL = '/accounts/monitoring', storage = new Map()) {
const entries = [initialURL];
let position = 0;
let pageActions = [];
const location = () => new URL(entries[position], 'http://localhost');
const history = {
push(url) {
entries.splice(position + 1);
entries.push(url);
position += 1;
},
replace(url) { entries[position] = url; },
back() { position -= 1; },
};
const element = (type, props, ...children) => ({ type, props: props || {}, children });
let slots = [], cursor = 0, activeComponent, dirty = false;
const effects = [];
const react = {
createElement: element,
Fragment: 'Fragment',
useMemo: factory => factory(),
useState(value) {
const index = cursor++, currentSlots = slots;
if (!(index in slots)) slots[index] = typeof value === 'function' ? value() : value;
return [slots[index], next => {
const value = typeof next === 'function' ? next(currentSlots[index]) : next;
if (!Object.is(value, currentSlots[index])) {
currentSlots[index] = value;
if (slots === currentSlots) dirty = true;
}
}];
},
useEffect(callback, deps) {
const index = cursor++;
if (!slots[index] || deps.some((value, i) => !Object.is(value, slots[index][i]))) {
slots[index] = deps;
effects.push(callback);
}
},
useCallback: (callback) => callback,
};
const components = new Proxy({
App: { useApp: () => ({ message: { success() {}, error() {} } }) },
Typography: { Text: 'Text', Link: 'Typography.Link' },
Form: Object.assign('Form', { useForm: () => [{}], Item: 'Form.Item' }),
Grid: { useBreakpoint: () => ({ xl: true }) },
}, { get: (target, key) => target[key] ?? String(key) });
const jsx = (type, props) => element(type, props, props?.children);
const mocks = {
react,
'react/jsx-runtime': { jsx, jsxs: jsx, Fragment: 'Fragment' },
antd: components,
'@ant-design/icons': new Proxy({}, { get: (_, key) => String(key) }),
'@ant-design/pro-components': { PageContainer: 'PageContainer' },
'@umijs/max': {
history,
Link: 'Link',
useParams: () => ({ id: '42' }),
useLocation: location,
useSearchParams: () => [new URLSearchParams(location().search), (params, options) => {
const url = `${location().pathname}?${params}`;
if (options?.replace) history.replace(url);
else history.push(url);
}],
},
'@/services/api': { getList: async () => [], getOne: async () => undefined, creatorGet: async () => ({}) },
'@/hooks/usePageState': loadPageState(react, { storage, pathname: () => location().pathname }),
'@/components/PageActions': { usePageActions: (actions) => { pageActions = actions; } },
'@/utils/helpers': { conflictMessage: () => '', dateTime: () => '' },
'@/utils/table': {
TABLE_PAGE_SIZE: 20,
tablePagination: () => ({ pageSize: 20, hideOnSinglePage: true }),
fixedLeft: () => ({}), fixedRight: () => ({}), useTableSticky: () => undefined,
},
'@/components/creator/WorksGallery': { default: 'WorksGallery' },
};
function load(file) {
const compiled = new Module(file, module);
compiled.filename = file;
compiled.paths = module.paths;
compiled.require = (name) => {
assert.ok(Object.hasOwn(mocks, name), `Unmocked dependency: ${name}`);
return mocks[name];
};
compiled._compile(ts.transpileModule(readFileSync(file, 'utf8'), {
compilerOptions: { module: ts.ModuleKind.CommonJS, jsx: ts.JsxEmit.ReactJSX, target: ts.ScriptTarget.ES2020 },
}).outputText, file);
return compiled.exports.default;
}
function find(node, type) {
if (!node || typeof node !== 'object') return undefined;
if (node.type === type) return node;
return node.children?.flat(Infinity).map((child) => find(child, type)).find(Boolean);
}
function render(Component) {
if (activeComponent !== Component) { slots = []; activeComponent = Component; }
for (let i = 0; i < 10; i++) {
cursor = 0;
dirty = false;
const tree = Component();
while (effects.length) effects.shift()();
if (!dirty) return tree;
}
throw new Error('Pagination render did not settle');
}
const List = load(listFile);
const Detail = load(detailFile);
return {
table: () => find(render(List), 'Table').props,
enterDetail() {
const column = find(render(List), 'Table').props.columns.find((column) => column.dataIndex === 'nickname');
find(column.render('', { id: '42' }), 'Typography.Link').props.onClick();
},
returnFromDetail() {
render(Detail);
const button = find(pageActions, 'Button');
button.props.onClick();
},
history,
url: () => entries[position],
entryCount: () => entries.length,
};
}
test('returning through the menu restores saved monitoring pagination without query parameters', () => {
const storage = new Map();
const h = harness('/accounts/monitoring', storage);
h.table().pagination.onChange(1, 50);
h.table().pagination.onChange(4, 50);
h.table();
const returned = harness('/accounts/monitoring', storage);
assert.equal(returned.table().pagination.current, 4);
assert.equal(returned.table().pagination.pageSize, 50);
});
test('an explicit monitoring URL overrides saved pagination and becomes the new saved position', () => {
const storage = new Map();
harness('/accounts/monitoring?page=4&pageSize=50', storage).table();
const explicit = harness('/accounts/monitoring?page=2&pageSize=20', storage);
assert.equal(explicit.table().pagination.current, 2);
assert.equal(explicit.table().pagination.pageSize, 20);
assert.equal(harness('/accounts/monitoring', storage).table().pagination.current, 2);
});
test('monitoring list starts at page one with the existing page size', () => {
const h = harness();
assert.equal(h.table().pagination.current, 1);
assert.equal(h.table().pagination.pageSize, 20);
});
test('detail return preserves the selected page and page size', () => {
const h = harness();
h.table().pagination.onChange(2, 20);
h.enterDetail();
h.returnFromDetail();
assert.equal(h.table().pagination.current, 2);
assert.equal(h.table().pagination.pageSize, 20);
assert.equal(new URL(h.url(), 'http://localhost').pathname, '/accounts/monitoring');
});
test('browser back restores pagination without stepping through previous page changes', () => {
const h = harness();
h.table().pagination.onChange(2, 20);
h.table().pagination.onChange(3, 20);
assert.equal(h.entryCount(), 1);
h.enterDetail();
h.history.back();
assert.equal(h.table().pagination.current, 3);
});
test('changing page size resets to page one and keeps that size after returning', () => {
const h = harness('/accounts/monitoring?page=3&pageSize=20');
h.table().pagination.onChange(3, 50);
assert.equal(h.table().pagination.current, 1);
h.table().pagination.onChange(2, 50);
h.enterDetail();
h.returnFromDetail();
assert.equal(h.table().pagination.current, 2);
assert.equal(h.table().pagination.pageSize, 50);
});
test('reload and directly opened detail retain the pagination in their URL', () => {
const h = harness('/accounts/monitoring/42?page=4&pageSize=50');
h.returnFromDetail();
assert.equal(h.table().pagination.current, 4);
assert.equal(h.table().pagination.pageSize, 50);
});
for (const value of ['invalid', '-1', '0', '1.5', 'Infinity', '9007199254740992']) {
test(`invalid pagination value ${value} uses valid defaults`, () => {
const h = harness(`/accounts/monitoring?page=${value}&pageSize=${value}`);
assert.equal(h.table().pagination.current, 1);
assert.equal(h.table().pagination.pageSize, 20);
});
}