const assert = require('node:assert/strict'); const { readFileSync } = require('node:fs'); const { createRequire, Module } = require('node:module'); const { resolve } = require('node:path'); const test = require('node:test'); const vm = require('node:vm'); const ts = require('typescript'); const requireWeb = createRequire(resolve(__dirname, '../package.json')); function compile(relativePath, imports = {}) { const path = resolve(__dirname, relativePath); const loaded = new Module(path, module); loaded.require = id => imports[id] || requireWeb(id); loaded._compile(ts.transpileModule(readFileSync(path, 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText, path); return loaded.exports; } const helpers = compile('../src/utils/helpers.ts'); const actions = compile('../src/components/accounts/account-actions.ts', { '@/services/api': {}, '@/utils/account-collection-status': compile('../src/utils/account-collection-status.ts'), }); const account = { id: '1004291301', runtime_status: 'active', login_status: 'logged_in', environment: { alias: 'environment', schedule_block_reason: 'runtime_missing' }, }; const stopped = { alias: 'environment', state: 'missing', status: '未记录运行实例', gateway_reachable: true }; const source = ts.createSourceFile('AccountManagementList.tsx', readFileSync(resolve(__dirname, '../src/components/accounts/AccountManagementList.tsx'), 'utf8'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); let renderer; function visit(node) { if (ts.isObjectLiteralExpression(node) && node.properties.some(property => ts.isPropertyAssignment(property) && property.name.getText(source) === 'title' && property.initializer.text === '操作')) { renderer = node.properties.find(property => ts.isPropertyAssignment(property) && property.name.getText(source) === 'render').initializer.getText(source); } ts.forEachChild(node, visit); } visit(source); assert.ok(renderer, '必须存在账号操作列'); function renderActions(row = account, browser = stopped, actionBusy = '') { const calls = []; const code = ts.transpileModule(`exports.render = ${renderer};`, { compilerOptions: { jsx: ts.JsxEmit.ReactJSX, module: ts.ModuleKind.CommonJS }, }).outputText; const context = { exports: {}, require: requireWeb, ...requireWeb('antd'), DownOutlined: requireWeb('@ant-design/icons').DownOutlined, ...helpers, ...actions, browsers: browser ? [browser] : [], verifiedLogin: {}, actionBusy, performPrimary: (row, intent) => calls.push([row.id, intent]), moreAction() {}, setEnvironmentTarget() {}, }; vm.runInNewContext(code, context); const cell = context.exports.render(undefined, row); const [tooltip, dropdown] = cell.props.children; return { button: tooltip.props.children, tooltip, menu: dropdown.props.menu, calls }; } test('未启动只阻止采集调度,不阻止启动环境', () => { const readiness = helpers.accountReadiness(account, account.environment); assert.equal(readiness.ready, false); assert.equal(readiness.reason, 'runtime_missing'); assert.equal(readiness.label, '运行环境未启动'); assert.equal(readiness.canStart, true); }); test('可调度账号仍可启动,未知阻塞原因不能绕过', () => { assert.equal(helpers.accountReadiness(account, {}).canStart, true); assert.equal(helpers.accountReadiness(account, { schedule_block_reason: 'unknown_block' }).canStart, false); }); for (const [name, row, binding, error] of [ ['未绑定', account, null], ['状态读取失败', account, account.environment, new Error('unavailable')], ['停止待确认', account, { ...account.environment, runtime_cleanup_pending: true }], ['固定出口异常', account, { ...account.environment, network_exit_id: 'proxy', network_exit_health: 'unhealthy' }], ['账号暂停', { ...account, runtime_status: 'paused' }, account.environment], ['暂停账号仍在停止', { ...account, runtime_status: 'paused' }, { ...account.environment, runtime_id: 'runtime' }], ]) { test(`${name}不能通过启动环境绕过限制`, () => { assert.equal(helpers.accountReadiness(row, binding, error).canStart, false); }); } test('1004291301 未启动时主按钮和更多菜单均允许启动', () => { const { button, tooltip, menu, calls } = renderActions(); assert.equal(button.props.children, '启动环境'); assert.equal(button.props.disabled, false); assert.equal(tooltip.props.title, undefined); assert.equal(menu.items.find(item => item.key === 'start').disabled, false); button.props.onClick(); assert.deepEqual(calls, [['1004291301', 'start']]); }); test('需要重新登录的未启动账号可点击重新登录', () => { const { button, calls } = renderActions({ ...account, login_status: 'manual_login' }); assert.equal(button.props.children, '重新登录'); assert.equal(button.props.disabled, false); button.props.onClick(); assert.deepEqual(calls, [['1004291301', 'login']]); }); for (const environment of [ { ...account.environment, runtime_cleanup_pending: true }, { ...account.environment, network_exit_id: 'proxy', network_exit_health: 'unhealthy' }, { ...account.environment, schedule_block_reason: 'unknown_block' }, ]) { test(`列表启动入口仍遵守限制 ${JSON.stringify(environment)}`, () => { const { button, tooltip, menu } = renderActions({ ...account, environment }); assert.equal(button.props.disabled, true); assert.ok(tooltip.props.title); assert.equal(menu.items.find(item => item.key === 'start').disabled, true); }); } test('暂停账号保留恢复运行入口,不能从更多菜单直接启动', () => { const paused = { ...account, runtime_status: 'paused' }; const { button, menu } = renderActions(paused); assert.equal(button.props.children, '恢复运行'); assert.equal(button.props.disabled, false); assert.equal(menu.items.find(item => item.key === 'start').disabled, true); }); test('操作执行中仍禁用主按钮,运行中不重复启动', () => { assert.equal(renderActions(account, stopped, account.id).button.props.disabled, true); const running = { ...stopped, state: 'running', status: 'ready' }; const { button, menu } = renderActions({ ...account, environment: { alias: 'environment' } }, running); assert.equal(button.props.children, '同步资料'); assert.equal(button.props.disabled, false); assert.equal(menu.items.find(item => item.key === 'start').disabled, true); });