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