Files
creator-hub/web/tests/account-create-modal.test.cjs
T

149 lines
8.1 KiB
JavaScript

const assert = require('node:assert/strict');
const { readFileSync, existsSync } = require('node:fs');
const { test } = require('node:test');
const { Module, createRequire } = require('node:module');
const { resolve } = require('node:path');
const ts = require('typescript');
const requireWeb = createRequire(resolve(__dirname, '../package.json'));
const file = resolve(__dirname, '../src/components/accounts/AccountCreateModal.tsx');
function setup({ gateways = [{ name: 'gw-a' }, { name: 'gw-b' }], loadError, result = { alias: 'env-new' }, createError } = {}) {
const states = [], effects = [], calls = [], successes = [], closes = [];
let cursor = 0, effectCursor = 0;
const marker = name => Object.assign(() => null, { displayName: name });
const Form = Object.assign(marker('Form'), { Item: marker('Form.Item'), useForm: () => [{ submit() { calls.push('submit'); } }] });
const antd = { Form };
for (const name of ['Modal', 'Alert', 'Button', 'Collapse', 'Select']) antd[name] = marker(name);
antd.Typography = { Paragraph: marker('Paragraph'), Link: marker('Link') };
const fingerprint = new Module(resolve(__dirname, '../src/components/accounts/FingerprintFields.tsx'), module);
fingerprint.require = id => id === 'antd' ? antd : requireWeb(id);
fingerprint._compile(ts.transpileModule(readFileSync(fingerprint.id, 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, jsx: ts.JsxEmit.ReactJSX } }).outputText, fingerprint.id);
const loaded = new Module(file, module);
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; }]; },
useEffect(effect, deps) {
const index = effectCursor++;
if (!effects[index] || deps.some((value, i) => value !== effects[index].deps[i])) {
effects[index]?.cleanup();
effects[index] = { deps, effect };
}
},
};
if (id === 'antd') return antd;
if (id === '@/services/api') return {
async creatorGet(path) { calls.push({ load: path }); if (loadError) throw loadError; return gateways; },
async creatorCreate(path, data) { calls.push({ path, data }); if (createError) throw createError; return result; },
};
if (id === '@/utils/helpers') return { conflictMessage: (error, fallback) => error.message || fallback };
if (id === '@/components/accounts/FingerprintFields') return fingerprint.exports;
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);
const render = () => {
cursor = 0; effectCursor = 0;
const node = loaded.exports.default({ onClose: () => closes.push(true), onSuccess: () => successes.push(true) });
for (const effect of effects) if (!effect.cleanup) effect.cleanup = effect.effect();
return node;
};
let tree = render();
const cleanup = () => effects.forEach(effect => effect.cleanup());
const find = (name, node = tree) => {
if (!node || typeof node !== 'object') return;
if (node.type?.displayName === name) return node;
for (const child of [node.props?.children].flat(Infinity)) { if (!child) continue; const found = find(name, child); if (found) return found; }
};
return { calls, successes, closes, cleanup, find, render() { tree = render(); return tree; } };
}
const flush = async () => { for (let i = 0; i < 8; i++) await Promise.resolve(); };
test('网关必选、指纹默认折叠;创建只提交网关和可选指纹', async () => {
const s = setup(); await flush(); s.render();
assert.deepEqual(s.calls[0], { load: '/gateways' });
assert.deepEqual(s.find('Select').props.options.map(x => x.value), ['gw-a', 'gw-b']);
assert.equal(s.find('Form.Item').props.name, 'gateway');
assert.ok(s.find('Form.Item').props.rules.some(rule => rule.required));
assert.equal(s.find('Collapse').props.defaultActiveKey, undefined);
s.find('Modal').props.onOk();
assert.equal(s.calls.at(-1), 'submit');
await s.find('Form').props.onFinish({ gateway: 'gw-b', fingerprint: { timezone: ' Asia/Shanghai ', disable_spoofing: ['font', 'audio'] } });
assert.deepEqual(s.calls.at(-1), { path: '/creator/environments', data: { gateway: 'gw-b', fingerprint: { timezone: 'Asia/Shanghai', disable_spoofing: 'font,audio' } } });
assert.equal(s.successes.length, 1);
assert.equal(s.closes.length, 0);
});
test('不填写指纹时使用默认配置;提交期间不能关闭', async () => {
const s = setup(); await flush(); s.render();
const save = s.find('Form').props.onFinish({ gateway: 'gw-a' });
const busy = s.render();
assert.equal(busy.props.confirmLoading, true);
assert.equal(busy.props.closable, false);
assert.equal(busy.props.keyboard, false);
assert.equal(busy.props.mask.closable, false);
assert.equal(busy.props.cancelButtonProps.disabled, true);
assert.equal(s.find('Select').props.disabled, true);
busy.props.onCancel();
assert.equal(s.closes.length, 0);
await save;
assert.deepEqual(s.calls.at(-1).data, { gateway: 'gw-a', fingerprint: {} });
});
test('创建失败显示原因,不关闭弹窗也不触发列表刷新', async () => {
const s = setup({ createError: new Error('网关不存在') }); await flush(); s.render();
await s.find('Form').props.onFinish({ gateway: 'gw-a' });
s.render();
assert.equal(s.find('Alert').props.title, '网关不存在');
assert.equal(s.successes.length, 0);
assert.equal(s.closes.length, 0);
assert.equal(s.find('Modal').props.confirmLoading, false);
s.find('Modal').props.onCancel();
assert.equal(s.closes.length, 1);
});
test('缺失环境标识不能当作创建成功', async () => {
const s = setup({ result: {} }); await flush(); s.render();
await s.find('Form').props.onFinish({ gateway: 'gw-a' }); s.render();
assert.match(s.find('Alert').props.title, /环境标识/);
assert.equal(s.successes.length, 0);
});
test('网关列表加载失败显示错误并允许重试,不冒充空列表', async () => {
const s = setup({ loadError: new Error('网关加载失败') }); await flush(); s.render();
const alert = s.find('Alert');
assert.equal(alert.props.type, 'error');
assert.match(alert.props.title, /网关加载失败/);
alert.props.action.props.onClick(); s.render(); await flush(); s.render();
assert.equal(s.calls.length, 2);
assert.equal(s.find('Modal').props.okButtonProps.disabled, true);
});
test('无网关时提示配置网关且不能创建;卸载后忽略晚到的加载结果', async () => {
const s = setup({ gateways: [] }); await flush(); s.render();
assert.equal(s.find('Alert').props.type, 'info');
assert.equal(s.find('Modal').props.okButtonProps.disabled, true);
const pending = setup(); pending.cleanup(); await flush();
assert.equal(pending.render().props.okButtonProps.disabled, true);
});
test('账号列表新增打开弹窗,创建成功关闭并刷新;独立创建页面已移除', () => {
const list = readFileSync(resolve(__dirname, '../src/components/accounts/AccountManagementList.tsx'), 'utf8');
assert.match(list, /onClick=\{\(\) => setCreateOpen\(true\)\}/);
assert.match(list, /onSuccess=\{\(\) => \{ setCreateOpen\(false\); void load\(\); \}\}/);
assert.doesNotMatch(list, /\/accounts\/new/);
assert.equal(existsSync(resolve(__dirname, '../src/pages/accounts/new.tsx')), false);
const source = ts.createSourceFile('AccountManagementList.tsx', list, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
let callback;
function visit(node) {
if (ts.isJsxSelfClosingElement(node) && node.tagName.getText(source) === 'AccountCreateModal') {
callback = node.attributes.properties.find(prop => prop.name?.getText(source) === 'onSuccess').initializer.expression.getText(source);
}
ts.forEachChild(node, visit);
}
visit(source);
let open = true, refreshes = 0;
const success = new Function('setCreateOpen', 'load', `return (${callback});`)(value => { open = value; }, () => { refreshes++; });
success();
assert.equal(open, false);
assert.equal(refreshes, 1);
});