Files
creator-hub/web/tests/account-management-unified-list.test.cjs
T

219 lines
14 KiB
JavaScript

const assert = require('node:assert/strict');
const { readFileSync } = require('node:fs');
const { Module, createRequire } = require('node:module');
const { resolve } = require('node:path');
const { test } = require('node:test');
const ts = require('typescript');
const requireWeb = createRequire(resolve(__dirname, '../package.json'));
const file = resolve(__dirname, '../src/components/accounts/AccountManagementList.tsx');
function setup({ accounts = [{ id: 'account-1', name: '真实昵称', business_status: 'normal', tags: ['主账号'] }], environments = [{ alias: 'env-1', gateway: 'gw-a' }], loadError, actionError } = {}) {
const states = [], calls = [], messages = [], paths = [];
let cursor = 0, started = false;
const marker = displayName => Object.assign(() => null, { displayName });
const form = { setFieldsValue(values) { form.values = values; }, async validateFields() { return form.values; } };
const antd = {};
for (const name of ['Alert', 'Avatar', 'Button', 'Card', 'Flex', 'Modal', 'Popconfirm', 'Select', 'Space', 'Table', 'Tag']) antd[name] = marker(name);
antd.Typography = { Text: marker('Text'), Link: marker('Link') };
antd.Form = Object.assign(marker('Form'), { Item: marker('Form.Item'), useForm: () => [form] });
antd.App = { useApp: () => ({ message: { success: text => messages.push(text), error: text => messages.push(text) } }) };
const loaded = new Module(file, module);
const options = { accounts, environments, loadError, actionError };
loaded.require = id => {
if (id === 'react') return {
useState(value) { const index = cursor++; if (!(index in states)) states[index] = value; return [states[index], next => { states[index] = typeof next === 'function' ? next(states[index]) : next; }]; },
useCallback: callback => callback,
useEffect(effect) { if (!started) { started = true; effect(); } },
};
if (id === 'antd') return antd;
if (id === '@umijs/max') return { history: { push: path => paths.push(path) } };
if (id === '@/services/api') return {
async creatorGet(path) { calls.push({ path }); if (options.loadError) throw options.loadError; return path === '/creator/environments' ? options.environments : options.accounts; },
async creatorAction(path) { calls.push({ path }); if (options.actionError) throw options.actionError; },
async creatorUpdate(path, data) { calls.push({ path, data }); if (options.actionError) throw options.actionError; },
async remove(resource, id) { calls.push({ resource, id }); if (options.actionError) throw options.actionError; },
};
if (id === '@/utils/helpers') return { accountReadiness: () => ({ ready: true, label: '可运行' }), dateTime: value => value, conflictMessage: (error, fallback) => error.message || fallback };
if (id === '@/utils/table') return { fixedLeft: column => ({ ...column, fixed: 'left' }), fixedRight: column => ({ ...column, fixed: 'right' }), tablePagination: () => ({ pageSize: 20, hideOnSinglePage: true }), useTableSticky: () => ({ offsetHeader: 56 }) };
if (id.startsWith('@/components/accounts/')) return { default: marker(id.split('/').at(-1)) };
return requireWeb(id);
};
loaded._compile(ts.transpileModule(readFileSync(file, 'utf8'), { fileName: file, compilerOptions: { module: ts.ModuleKind.CommonJS, jsx: ts.JsxEmit.ReactJSX, inlineSourceMap: true, inlineSources: true } }).outputText, file);
let tree;
const render = () => { cursor = 0; tree = loaded.exports.default(); return tree; };
const findAll = (name, node = tree) => {
if (!node || typeof node !== 'object') return [];
return [...(node.type?.displayName === name ? [node] : []), ...[node.props?.children].flat(Infinity).filter(child => child != null).flatMap(child => findAll(name, child))];
};
const find = (name, node) => findAll(name, node)[0];
const column = title => find('Table').props.columns.find(column => column.title === title);
const cell = (title, row) => { const col = column(title); return col.render(row[col.dataIndex], row); };
render();
return { render, find, findAll, cell, calls, messages, paths, form, options };
}
const flush = async () => { for (let i = 0; i < 10; i++) await Promise.resolve(); };
const text = node => node == null ? '' : typeof node === 'object' ? [node.props?.children].flat(Infinity).map(text).join('') : String(node);
const ready = async options => { const s = setup(options); await flush(); s.render(); return s; };
test('待登录环境与已登录账号共用唯一列表、计数及分页', async () => {
const s = await ready();
assert.equal(s.findAll('Table').length, 1);
assert.equal(s.findAll('Card').length, 1);
const rows = s.find('Table').props.dataSource;
assert.equal(rows.length, 2);
assert.equal(rows[0].name, '真实昵称');
assert.match(text(s.find('Text')), /共 2 项/);
assert.equal(s.find('Table').props.loading, false);
assert.notEqual(rows[0].id, rows[1].id);
});
for (const rowCount of [1, 20, 100]) {
test(`账号与操作列不挤压,${rowCount} 条数据使用页面纵向滚动`, async () => {
const s = await ready({ accounts: Array.from({ length: rowCount }, (_, i) => ({ id: `account-${i}`, name: `账号 ${i}` })), environments: [] });
const { columns, scroll, dataSource, sticky, pagination } = s.find('Table').props;
assert.equal(dataSource.length, rowCount);
assert.deepEqual(sticky, { offsetHeader: 56 });
assert.deepEqual(pagination, { pageSize: 20, hideOnSinglePage: true });
assert.equal(columns[0].title, '账号');
assert.equal(columns[0].fixed, 'left');
assert.equal(columns[0].width, 240);
assert.equal(columns.at(-1).title, '操作');
assert.equal(columns.at(-1).fixed, 'right');
assert.equal(columns.at(-1).width, 300);
assert.ok(columns.slice(1, -1).every(column => !column.fixed));
assert.equal(scroll.x, 'max-content', '宽屏也应按内容宽度滚动,而不是挤压两侧列');
assert.equal(Object.hasOwn(scroll, 'y'), false, '不应启用表格内部纵向滚动');
const row = dataSource.find(row => !row.pending_environment);
assert.notEqual(s.find('Space', s.cell('操作', row)).props.wrap, true, '操作按钮不得换行折叠');
});
}
test('按共用环境数据库ID倒序排列,新建环境与账号在前,登录绑定不改变位置', async () => {
const accounts = [
{ id: 'account-z', name: '旧账号', environment: { id: 2, alias: 'env-z' } },
{ id: 'account-a', name: '新账号', environment: { id: 12, alias: 'env-a' } },
];
const environments = [{ id: 9, alias: 'env-9' }, { id: 20, alias: 'env-20' }];
const s = await ready({ accounts, environments });
assert.deepEqual(s.find('Table').props.dataSource.map(row => row.id), ['environment:env-20', 'account-a', 'environment:env-9', 'account-z']);
assert.deepEqual(accounts.map(row => row.id), ['account-z', 'account-a'], '排序不修改服务返回的原数据');
s.find('Button', s.cell('操作', s.find('Table').props.dataSource[0])).props.onClick(); s.render();
s.options.accounts = [...accounts, { id: 'account-bound', name: '已登录', environment: { id: 20, alias: 'env-20' } }];
s.options.environments = [{ id: 9, alias: 'env-9' }];
s.find('AccountLoginModal').props.onSuccess(); await flush(); s.render();
assert.deepEqual(s.find('Table').props.dataSource.map(row => row.id), ['account-bound', 'account-a', 'environment:env-9', 'account-z']);
});
test('待登录行缺失资料显示默认值,不伪造账号、业务状态或详情入口', async () => {
const s = await ready({ accounts: [] });
const row = s.find('Table').props.dataSource[0];
assert.ok(row, '环境应成为主列表中的一行');
assert.equal(row.name, '');
const name = s.cell('账号', row);
assert.equal(s.find('Avatar', name).props.src, undefined);
assert.match(text(name), /待登录/);
assert.equal(s.find('Link', name), undefined);
for (const title of ['粉丝', '关注', '获赞', '互关', '评论']) assert.equal(s.cell(title, row), '0');
assert.equal(text(s.cell('作品数据', row)), '登录后显示作品数据');
assert.equal(text(s.cell('标签', row)), '未设置');
assert.equal(text(s.cell('业务状态', row)), '—');
assert.equal(text(s.cell('状态', row)), '待登录');
assert.match(text(s.cell('运行环境', row)), /env-1未启动网关 gw-a机器直连/);
const actions = s.cell('操作', row);
assert.equal(s.findAll('Button', actions).length, 1);
assert.equal(text(actions), '登录并同步账号');
assert.equal(s.find('Popconfirm', actions), undefined);
assert.equal(s.paths.length, 0);
assert.deepEqual(s.options.environments, [{ alias: 'env-1', gateway: 'gw-a' }], '默认值不能写回环境数据');
});
test('登录使用真实环境标识,成功后同一列表刷新为真实账号,不保留重复环境', async () => {
const s = await ready({ accounts: [] });
const row = s.find('Table').props.dataSource[0];
assert.ok(row);
s.find('Button', s.cell('操作', row)).props.onClick(); s.render();
let modal = s.find('AccountLoginModal');
assert.equal(modal.props.accountID, 'env-1');
assert.equal(modal.props.resource, 'environments');
modal.props.onClose(); s.render(); assert.equal(s.find('AccountLoginModal'), undefined);
s.find('Button', s.cell('操作', row)).props.onClick(); s.render();
modal = s.find('AccountLoginModal');
s.options.accounts = [{ id: 'account-new', name: '新昵称', business_status: 'normal' }];
s.options.environments = [];
modal.props.onSuccess(); await flush(); s.render();
assert.equal(s.find('AccountLoginModal'), undefined);
assert.deepEqual(s.find('Table').props.dataSource.map(row => row.id), ['account-new']);
});
test('待登录环境真实名称、网关、运行与出口信息仍被保留', async () => {
const s = await ready({ accounts: [], environments: [{ alias: 'env-2', name: '环境二', gateway: 'gw-b', runtime_id: 'runtime-2', runtime_cleanup_pending: true, network_exit: { id: 'exit-2', health_status: 'healthy' } }] });
const row = s.find('Table').props.dataSource[0]; assert.ok(row);
assert.match(text(s.cell('运行环境', row)), /环境二停止待确认网关 gw-b出口 exit-2 · 健康/);
});
test('空列表不生成占位行,环境加载失败不冒充成功并支持重试', async () => {
const empty = await ready({ accounts: [], environments: [] });
assert.equal(empty.findAll('Table').length, 1);
assert.deepEqual(empty.find('Table').props.dataSource, []);
const s = await ready({ loadError: new Error('环境加载失败') });
assert.equal(s.find('Alert').props.title, '环境加载失败');
assert.equal(s.find('Table').props.loading, false);
s.options.loadError = undefined;
await s.find('Alert').props.action.props.onClick(); s.render();
assert.equal(s.find('Alert'), undefined);
assert.equal(s.find('Table').props.dataSource.length, 2);
});
test('已登录账号仍可访问详情、编辑、启动、删除和保存标签', async () => {
const s = await ready({ environments: [] });
let row = s.find('Table').props.dataSource[0];
s.find('Link', s.cell('账号', row)).props.onClick();
assert.equal(s.paths.at(-1), '/accounts/account-1');
const actions = s.cell('操作', row);
s.findAll('Button', actions).find(button => text(button) === '编辑账号').props.onClick();
assert.equal(s.paths.at(-1), '/accounts/account-1/edit');
await s.findAll('Button', actions).find(button => text(button) === '启动').props.onClick(); s.render();
assert.ok(s.calls.some(call => call.path === '/phase-a/accounts/account-1/start'));
let login = s.find('AccountLoginModal'); assert.equal(login.props.accountID, 'account-1');
login.props.onSuccess(); await flush(); s.render();
login = s.find('AccountLoginModal'); login.props.onClose(); s.render();
s.findAll('Button', actions).find(button => text(button) === '编辑标签').props.onClick(); s.render();
assert.deepEqual(s.form.values.tags, ['主账号']);
s.form.values = { tags: ['新标签'] };
await s.find('Modal').props.onOk(); s.render();
row = s.find('Table').props.dataSource[0]; assert.deepEqual(row.tags, ['新标签']);
assert.ok(s.calls.some(call => call.path === '/creator/accounts/account-1/tags'));
await s.find('Popconfirm', actions).props.onConfirm();
assert.ok(s.calls.some(call => call.resource === 'accounts' && call.id === 'account-1'));
});
test('已登录账号缺失业务状态有明确占位,真实指标和标签保持原样', async () => {
const s = await ready({ environments: [], accounts: [{ id: 'account-2', name: '', follower_count: 20000, aweme_count: 100, work_count: 7, latest_published_at: '2026-01-01', environment: { alias: 'env-2', runtime_id: 'runtime-2', network_exit_id: 'exit-2', network_exit_health: 'unchecked' } }] });
const row = s.find('Table').props.dataSource[0];
assert.equal(s.cell('粉丝', row), '2.0w');
assert.equal(s.cell('关注', row), '—');
assert.equal(text(s.cell('业务状态', row)), '—');
assert.match(text(s.cell('作品数据', row)), /100 个作品已采集 7 \/ 总计 100最近发布 2026-01-01/);
assert.match(text(s.cell('运行环境', row)), /env-2运行中网关 —出口 exit-2 · unchecked/);
assert.equal(text(s.cell('状态', row)), '可运行');
});
test('账号操作失败显示实际错误,标签取消及新增关闭刷新保持可用', async () => {
const s = await ready({ actionError: new Error('操作冲突') });
const row = s.find('Table').props.dataSource[0];
const actions = s.cell('操作', row);
const button = label => s.findAll('Button', actions).find(button => text(button) === label);
await button('启动').props.onClick(); s.render(); assert.equal(s.find('AccountLoginModal'), undefined);
await s.find('Popconfirm', actions).props.onConfirm();
button('编辑标签').props.onClick(); s.render(); await s.find('Modal').props.onOk(); s.render();
assert.equal(s.messages.filter(message => message === '操作冲突').length, 3);
assert.equal(s.find('Modal').props.open, true);
s.find('Modal').props.onCancel(); s.render(); assert.equal(s.find('Modal').props.open, false);
await s.find('Modal').props.onOk();
const create = () => s.findAll('Button').find(button => text(button) === '新增账号').props.onClick();
create(); s.render(); s.find('AccountCreateModal').props.onClose(); s.render(); assert.equal(s.find('AccountCreateModal'), undefined);
create(); s.render(); s.find('AccountCreateModal').props.onSuccess(); await flush(); s.render();
assert.equal(s.find('AccountCreateModal'), undefined);
assert.equal(s.find('Table').props.dataSource.length, 2);
});