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