167 lines
6.4 KiB
JavaScript
167 lines
6.4 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const vm = require('vm');
|
|
|
|
(async () => {
|
|
const appSource = fs.readFileSync(path.join(__dirname, '..', 'public', 'app.js'), 'utf8');
|
|
const indexSource = fs.readFileSync(path.join(__dirname, '..', 'public', 'index.html'), 'utf8');
|
|
const objectUrls = new Map();
|
|
const downloads = [];
|
|
let objectUrlIndex = 0;
|
|
let clipboardText = '';
|
|
|
|
class TestBlob {
|
|
constructor(parts, options = {}) {
|
|
this.parts = parts;
|
|
this.type = options.type || '';
|
|
}
|
|
}
|
|
|
|
const context = vm.createContext({
|
|
console, Set, Map, Date, JSON, Math, String, Number, Object, Array, RegExp, Promise, Uint8Array,
|
|
setTimeout, clearTimeout,
|
|
Blob: TestBlob,
|
|
URL: {
|
|
createObjectURL(blob) {
|
|
const url = `blob:test/${++objectUrlIndex}`;
|
|
objectUrls.set(url, blob);
|
|
return url;
|
|
},
|
|
revokeObjectURL() {},
|
|
},
|
|
navigator: {
|
|
clipboard: {
|
|
async writeText(text) { clipboardText = text; },
|
|
},
|
|
},
|
|
document: {
|
|
createElement(tag) {
|
|
if (tag === 'a') {
|
|
return {
|
|
href: '',
|
|
download: '',
|
|
click() {
|
|
downloads.push({ filename: this.download, blob: objectUrls.get(this.href) });
|
|
},
|
|
};
|
|
}
|
|
return { value: '', select() {} };
|
|
},
|
|
body: { appendChild() {}, removeChild() {} },
|
|
execCommand() { return true; },
|
|
},
|
|
});
|
|
vm.runInContext(`${appSource}\nthis.__geminiApp = geminiApp;`, context, { filename: 'public/app.js' });
|
|
|
|
const app = context.__geminiApp();
|
|
assert.strictEqual(app.exportMode.csv, false, 'CSV导出默认应关闭');
|
|
assert.strictEqual(app.exportFields.recharged_at, false, '充值时间导出默认应关闭');
|
|
assert.strictEqual(app.exportFields.account_refund_status, false, '账号退款导出默认应关闭');
|
|
assert.match(indexSource, /x-model="exportMode\.csv"[^>]*>\s*导出到 CSV/, '导出弹窗应显示CSV勾选项');
|
|
assert.match(indexSource, /x-model="exportFields\.recharged_at"[^>]*>\s*充值时间/, '导出弹窗应显示充值时间勾选项');
|
|
assert.match(indexSource, /x-model="exportFields\.account_refund_status"[^>]*>\s*账号退款/, '导出弹窗应显示账号退款勾选项');
|
|
app.accounts = [{ email: 'reset@example.com' }];
|
|
app.selectedEmails = new Set(['reset@example.com']);
|
|
app.exportMode.csv = true;
|
|
app.exportSelectedAccounts();
|
|
assert.strictEqual(app.exportMode.csv, false, '每次重新打开导出弹窗都应恢复CSV默认状态');
|
|
assert.strictEqual(app.exportMode.clipboard, true);
|
|
|
|
const directCsv = app.buildExportCsv([
|
|
{ email: 'user@example.com', customer_name: 'A,客户', message: '第一行\n"第二行"' },
|
|
], ['email', 'customer_name', 'message']);
|
|
assert.strictEqual(
|
|
directCsv,
|
|
'"邮箱","客户名","消息"\r\n"user@example.com","A,客户","第一行\n""第二行"""',
|
|
'CSV应包含中文表头,并正确转义逗号、引号和换行',
|
|
);
|
|
const refundCsv = app.buildExportCsv([
|
|
{ email: 'refund@example.com', account_refund_status: 'gpt警告' },
|
|
], ['email', 'account_refund_status']);
|
|
assert.strictEqual(
|
|
refundCsv,
|
|
'"邮箱","账号退款"\r\n"refund@example.com","gpt警告"',
|
|
'CSV勾选账号退款后应导出当前退款状态',
|
|
);
|
|
|
|
const rawRechargeTime = '2026-08-22 05:51:48';
|
|
const beijingRechargeTime = app.formatDate(rawRechargeTime);
|
|
assert.match(beijingRechargeTime, /13:51:48/, '充值时间应转换为北京时间');
|
|
const rechargeCsv = app.buildExportCsv([
|
|
{ email: 'paid@example.com', recharged_at: rawRechargeTime },
|
|
], ['email', 'recharged_at']);
|
|
assert.strictEqual(
|
|
rechargeCsv,
|
|
`"邮箱","充值时间"\r\n"paid@example.com","${beijingRechargeTime}"`,
|
|
'CSV勾选充值时间后应导出北京时间',
|
|
);
|
|
app.stagedBatchDetail = {
|
|
batch: { selected_fields: ['email', 'recharged_at'] },
|
|
accounts: [{ email: 'paid@example.com', recharged_at: rawRechargeTime }],
|
|
};
|
|
assert.strictEqual(
|
|
app.stagedBatchText,
|
|
`paid@example.com----${beijingRechargeTime}`,
|
|
'暂存箱再次复制或下载时也应导出北京时间',
|
|
);
|
|
app.stagedBatchDetail = null;
|
|
|
|
app._exportList = [{
|
|
email: 'user@example.com',
|
|
customer_name: 'A,客户',
|
|
message: '第一行\n"第二行"',
|
|
}];
|
|
Object.keys(app.exportFields).forEach(key => { app.exportFields[key] = false; });
|
|
['email', 'customer_name', 'message', 'openai_auth_json'].forEach((key) => {
|
|
app.exportFields[key] = true;
|
|
});
|
|
app.exportMode = { file: false, csv: true, clipboard: false, staging: false };
|
|
app.showExportModal = true;
|
|
const logs = [];
|
|
app.addLog = (message, type) => logs.push({ message, type });
|
|
let authLoadCount = 0;
|
|
app.loadOpenAIAuthForExport = async (accounts) => {
|
|
authLoadCount++;
|
|
return {
|
|
accounts: accounts.map(account => ({ ...account, openai_auth_json: '{"token":"x,y"}' })),
|
|
missingCount: 0,
|
|
};
|
|
};
|
|
|
|
await app.confirmExport();
|
|
assert.strictEqual(authLoadCount, 1, '只选CSV时也必须加载授权JSON字段');
|
|
assert.strictEqual(downloads.length, 1, '只选CSV时应触发一次下载');
|
|
assert.match(downloads[0].filename, /^accounts_\d{4}-\d{2}-\d{2}\.csv$/);
|
|
assert.strictEqual(downloads[0].blob.type, 'text/csv;charset=utf-8');
|
|
assert.strictEqual(downloads[0].blob.parts[0], '\uFEFF', 'CSV应包含UTF-8 BOM');
|
|
const downloadedCsv = downloads[0].blob.parts.join('').slice(1);
|
|
assert.strictEqual(
|
|
downloadedCsv,
|
|
'"邮箱","客户名","消息","授权JSON"\r\n'
|
|
+ '"user@example.com","A,客户","第一行\n""第二行""","{""token"":""x,y""}"',
|
|
);
|
|
assert.strictEqual(clipboardText, '', '只选CSV时不应写入剪贴板');
|
|
assert.strictEqual(app.showExportModal, false);
|
|
assert.ok(logs.some(item => item.type === 'success' && /导出 1 个账号到 CSV/.test(item.message)));
|
|
|
|
downloads.length = 0;
|
|
authLoadCount = 0;
|
|
app.showExportModal = true;
|
|
app.exportMode = { file: true, csv: true, clipboard: false, staging: false };
|
|
await app.confirmExport();
|
|
assert.strictEqual(authLoadCount, 1, 'TXT和CSV同时导出时敏感字段只应加载一次');
|
|
assert.deepStrictEqual(
|
|
downloads.map(item => path.extname(item.filename)).sort(),
|
|
['.csv', '.txt'],
|
|
'TXT与CSV可以同时勾选并分别下载',
|
|
);
|
|
|
|
console.log('account_csv_export.test.js: PASS');
|
|
})().catch(error => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|