const assert = require('assert');
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const appSource = fs.readFileSync(path.join(__dirname, '..', 'public', 'app.js'), 'utf8');
const htmlSource = fs.readFileSync(path.join(__dirname, '..', 'public', 'index.html'), 'utf8');
const styleSource = fs.readFileSync(path.join(__dirname, '..', 'public', 'style.css'), 'utf8');
const taskRunnerSource = fs.readFileSync(path.join(__dirname, '..', 'src', 'workers', 'task_runner.js'), 'utf8');
const context = vm.createContext({
console,
Set,
Map,
Date,
JSON,
Math,
String,
Number,
Object,
Array,
RegExp,
Promise,
Uint8Array,
setTimeout,
clearTimeout,
});
vm.runInContext(`${appSource}\nthis.__geminiApp = geminiApp;`, context, { filename: 'public/app.js' });
(async () => {
const app = context.__geminiApp();
app.$nextTick = callback => {
if (callback) callback();
return Promise.resolve();
};
app.accounts = Array.from({ length: 1000 }, (_, index) => ({
email: `user${String(index).padStart(4, '0')}@example.com`,
password: `password-${index}`,
secret_key: 'JBSWY3DPEHPK3PXP',
status: '未登录',
}));
app._accountByEmail = new Map(app.accounts.map(account => [account.email, account]));
app._rebuildDashboardStats();
app.accounts[0].status = 'ChatGPT成功';
app.accounts[1].status = 'ChatGPT失败';
app.accounts[2].status = '失败';
app.accounts[3].status = '未注册成功已改密和2FA';
assert.strictEqual(app.tierCounts.level1['已登录'], 1, 'ChatGPT成功应归入已登录');
assert.strictEqual(app.tierCounts.level1['登录失败'], 3, 'ChatGPT失败、失败和新状态都不能归入已登录');
app.tierFilter.level1 = '已登录';
assert.strictEqual(app.filteredAccounts.length, 1, '已登录筛选不应包含 ChatGPT失败');
app.tierFilter.level1 = '登录失败';
assert.strictEqual(app.filteredAccounts.length, 3, '登录失败筛选应包含新状态和其他任务失败状态');
assert.strictEqual(app.statusClass('未注册成功已改密和2FA'), 'status-ChatGPT失败');
app.tierFilter.level1 = '全部';
app.accounts[0].status = '未登录';
app.accounts[1].status = '未登录';
app.accounts[2].status = '未登录';
app.accounts[3].status = '已充值提取';
assert.strictEqual(app.statusCounts['已充值提取'], 1, '已充值提取应计入独立状态统计');
assert.strictEqual(app.statusClass('已充值提取'), 'status-已充值提取');
app.accounts[3].status = '未登录';
const nowIso = new Date().toISOString();
const oldIso = new Date(Date.now() - (10 * 24 * 60 * 60 * 1000)).toISOString();
app.accounts[4].password_changed = 1;
app.accounts[4].password_changed_at = nowIso;
app.accounts[5].password_changed = 1;
app.accounts[5].password_changed_at = oldIso;
app.accounts[6].status = '改密成功';
assert.strictEqual(app._hasPasswordChanged(app.accounts[6]), true, '实时状态已改密成功时不能继续归入未改密');
app.setTierFilter(3, '未改密');
assert(!app.filteredAccounts.some(account => ['user0004@example.com', 'user0005@example.com', 'user0006@example.com'].includes(account.email)), '未改密筛选应排除所有已改密账号');
app.setTierFilter(3, '改密<7天');
assert.deepStrictEqual(Array.from(app.filteredAccounts, account => account.email), ['user0004@example.com'], '改密不足7天应有独立筛选结果');
app.setTierFilter(3, '改密≥7天');
assert.deepStrictEqual(Array.from(app.filteredAccounts, account => account.email), ['user0005@example.com'], '改密满7天应有独立筛选结果');
app.setTierFilter(3, '全部');
app.accounts[6].status = '未登录';
app.accounts[0].restriction_status = 'restricted';
app.accounts[0].status = '登录成功';
assert.strictEqual(app._isGcpRestricted(app.accounts[0]), true, '限制检查结果应计入 GCP 被限制');
assert.strictEqual(app._failureLabelForCode('GCP_RESTRICTED', ''), 'GCP被限制');
assert.strictEqual(app.tierCounts.level2['GCP被限制'], 1, '组合任务旧数据应出现在 GCP 仪表盘');
app.tierFilter.level2 = 'GCP被限制';
assert.strictEqual(app._tier2Accounts.length, 1, 'GCP 仪表盘应能筛选组合任务结果');
app.tierFilter.level2 = '全部';
app._showTaskStartFeedback('change_proxy', app.accounts);
assert.strictEqual(app.taskProgress.total, 1000, '任务启动时应立即显示总进度');
const etaNow = Date.now();
app.taskProgress = { current: 25, total: 100, startedAt: etaNow - 25000 };
app.taskClockNow = etaNow;
assert.strictEqual(app.taskProgressPercent, 25, '应实时计算任务完成百分比');
assert(app.taskEtaText.includes('预计结束'), '完成首个账号后应显示预计结束时间');
app.taskProgress = { current: 2, total: 100, startedAt: etaNow - 25000 };
assert(app.taskEtaText.includes('采样中'), '并发任务前几个结果不应产生失真的预计时间');
app._showTaskStartFeedback('change_proxy', app.accounts);
app._activeTaskType = 'change_proxy';
app._setActiveTaskEmails(app.accounts.map(account => account.email));
app.isRunning = true;
assert.strictEqual(app.getLiveMessage(app.accounts[0]), '等待切换 IP...', '未开始账号应按需显示等待状态');
app._queueAccountUpdate({
email: 'user0000@example.com',
message: '✅ 切换 IP 成功,浏览器代理已更新',
});
app._flushAccountUpdates();
assert.strictEqual(app.accountLogs['user0000@example.com'].message, '✅ 切换 IP 成功,浏览器代理已更新');
assert.strictEqual(app.accountLogs['user0000@example.com'].type, 'success');
assert.strictEqual(app.accountTotalPages, 10);
assert.strictEqual(app.pagedAccounts.length, 100, '默认只应渲染 100 个账号');
assert.strictEqual(app.pagedAccounts[0].email, 'user0000@example.com');
app.accountPage = 10;
assert.strictEqual(app.pagedAccounts.length, 100);
assert.strictEqual(app.pagedAccounts[0].email, 'user0900@example.com');
app.selectedEmails = new Set(['user0000@example.com']);
app.accountPage = 2;
app.selectCurrentPage();
assert.strictEqual(app.selectedCount, 101, '选中当前页应保留已有选择并加入本页账号');
assert(app.selectedEmails.has('user0100@example.com'));
assert(app.selectedEmails.has('user0199@example.com'));
assert(!app.selectedEmails.has('user0200@example.com'));
app.deselectCurrentPage();
assert.strictEqual(app.selectedCount, 1, '取消当前页选择应保留其他页已选账号');
assert(app.selectedEmails.has('user0000@example.com'));
assert(!app.selectedEmails.has('user0100@example.com'));
assert(!app.selectedEmails.has('user0199@example.com'));
let computedTotpCount = 0;
app._computeTotp = async () => {
computedTotpCount++;
return '123456';
};
await app.updateTotpCodes();
assert.strictEqual(computedTotpCount, 100, 'TOTP 只应计算当前页账号');
assert.strictEqual(Object.keys(app.totpCodes).length, 100);
app.searchText = 'user0999';
app.resetAccountPage();
assert.strictEqual(app.filteredAccounts.length, 1);
assert.strictEqual(app.accountCurrentPage, 1);
assert.strictEqual(app.pagedAccounts[0].email, 'user0999@example.com');
app.searchText = '';
app.accounts[10].browser_id = 'browser-live';
app.accounts[10].browser_seq = null;
context.fetch = async () => ({
json: async () => ({ success: true, data: [{ id: 'browser-live', name: 'Browser Live', seq: 88 }] }),
});
await app.loadBrowsers();
assert.strictEqual(app.accounts[10].browser_seq, '88', '浏览器列表同步完成后应立即补齐当前页面的显示编号');
app._browserById = new Map([['browser-1', { id: 'browser-1', seq: 42, name: 'Browser 42' }]]);
assert.strictEqual(app.getBrowserSeq('browser-1'), '42');
assert.strictEqual(app.getBrowserSeq('missing-browser', '77'), '77', '实时列表缺失时应显示数据库保存的浏览器编号');
app._pendingAccountUpdates = new Map([[
'user0500@example.com',
{
email: 'user0500@example.com', status: '登录成功', message: '完成',
password_changed: 1, password_changed_at: nowIso,
},
]]);
app._flushAccountUpdates();
assert.strictEqual(app._accountByEmail.get('user0500@example.com').status, '登录成功');
assert.strictEqual(app._accountByEmail.get('user0500@example.com').password_changed, 1, '改密标记应通过实时更新进入页面');
assert.strictEqual(
app.dashboardStats.tierCounts.level1['已登录'],
app.tierCounts.level1['已登录'],
'实时更新后应刷新一次仪表盘快照',
);
const accountLogsIdentity = app.accountLogs;
app.addLog('[user0500@example.com] ✅ 完成');
assert.strictEqual(app.accountLogs, accountLogsIdentity, '高频账号日志不应复制整个日志对象');
assert.strictEqual(app.accountLogs['user0500@example.com'].message, '✅ 完成');
for (let index = 0; index < 600; index++) app.addLog(`batch log ${index}`);
app._flushLogs();
assert.strictEqual(app.logs.length, 500, '日志 DOM 数据应保持固定上限');
assert(htmlSource.includes('x-for="account in pagedAccounts"'), '账号表格必须使用分页数据源');
assert(htmlSource.includes('@click="selectCurrentPage()"'), '分页栏应提供选中当前页按钮');
assert(htmlSource.includes('@click="deselectCurrentPage()"'), '分页栏应提供取消当前页选择按钮');
assert(htmlSource.includes('>取消当前页选择'), '分页栏应明确显示取消当前页选择');
assert(htmlSource.includes('task-progress-strip'), '运行任务时必须渲染统一进度条');
assert(!htmlSource.includes('x-for="account in filteredAccounts"'), '账号表格不应再一次渲染全部筛选结果');
assert(htmlSource.includes('dashboardStats.tierCounts'), '仪表盘卡片应读取共享统计快照');
assert(!/x-text="tierCounts\./.test(htmlSource), '每个仪表盘卡片不应各自重新扫描账号列表');
assert(htmlSource.includes('@click="runDeleteBrowsers()"'), '操作栏应显示独立的删除浏览器按钮');
assert(taskRunnerSource.includes('successfullyDeletedIds.has(acc.browser_id)'), '只有窗口删除成功后才能清空浏览器编号');
assert.strictEqual((htmlSource.match(/