douyin-release-gate / verify (push) Failing after 18m54s
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.
168 lines
9.3 KiB
JavaScript
168 lines
9.3 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', '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);
|
||
});
|