207 lines
8.1 KiB
JavaScript
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);
|
|
});
|
|
}
|