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