Files
creator-hub/web/tests/account-create-modal.test.cjs
T
rogee e50bed9ad8
douyin-release-gate / verify (push) Failing after 18m54s
feat: add account events and private message management
Add Douyin notification polling, event details, and manual multi-account private messaging.

Refine environment memory settings, account operations, login collection recovery, and message UI; update tests and documentation.
2026-10-07 10:23:05 +08:00

168 lines
9.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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', 'InputNumber']) 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, predicate = () => true) => {
if (!node || typeof node !== 'object') return;
if (node.type?.displayName === name && predicate(node)) return node;
for (const child of [node.props?.children].flat(Infinity)) { if (!child) continue; const found = find(name, child, predicate); 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', memory_limit_mb: 4096, fingerprint: { timezone: ' Asia/Shanghai ', disable_spoofing: ['font', 'audio'] } });
assert.deepEqual(s.calls.at(-1), { path: '/creator/environments', data: { gateway: 'gw-b', memory_limit_mb: 4096, 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', memory_limit_mb: 2048 });
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', memory_limit_mb: 2048, fingerprint: {} });
});
test('内存设置在指纹区域外,默认2048 MB,校验整数范围并以256步进', async () => {
const s = setup(); await flush(); s.render();
const field = s.find('Form.Item', undefined, node => node.props.name === 'memory_limit_mb');
assert.ok(field, '必须提供独立内存字段');
assert.equal(field.props.initialValue, 2048);
assert.equal(field.props.label, '浏览器内存上限(MB)');
assert.match(field.props.extra, /默认2048 MB(2GB).*下次启动生效/);
const input = s.find('InputNumber');
assert.equal(input.props.min, 512);
assert.equal(input.props.max, 65536);
assert.equal(input.props.step, 256);
const Schema = requireWeb('@rc-component/async-validator').default;
const schema = new Schema({ memory_limit_mb: field.props.rules });
for (const value of [512, 2048, 4096, 65536]) await schema.validate({ memory_limit_mb: value });
for (const value of [undefined, null, 511, 65537, 2048.5, '2048']) {
await assert.rejects(schema.validate({ memory_limit_mb: value }));
}
});
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);
});