Files
gerenjizhang/test-browser-validation.js
T

304 lines
14 KiB
JavaScript

const { chromium } = require('playwright');
const path = require('path');
const fs = require('fs');
const SCREENSHOT_DIR = path.join(__dirname, 'test-screenshots');
const FRONTEND_URL = 'http://localhost:5173';
const API_BASE = 'http://localhost:3001';
// 确保截图目录存在
if (!fs.existsSync(SCREENSHOT_DIR)) {
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
}
async function apiGet(urlPath) {
const http = require('http');
return new Promise((resolve, reject) => {
http.get(API_BASE + urlPath, res => {
let data = '';
res.on('data', c => data += c);
res.on('end', () => resolve(JSON.parse(data)));
}).on('error', reject);
});
}
async function main() {
console.log('[BROWSER TEST] 启动浏览器自动化测试...');
console.log(`[BROWSER TEST] 截图目录: ${SCREENSHOT_DIR}\n`);
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await context.newPage();
// 截图计数器
let screenshotNum = 0;
async function screenshot(name) {
screenshotNum++;
const fileName = `${String(screenshotNum).padStart(2, '0')}-${name}.png`;
const filePath = path.join(SCREENSHOT_DIR, fileName);
await page.screenshot({ path: filePath, fullPage: true });
console.log(`[SCREENSHOT] ${fileName}`);
return filePath;
}
const results = {
tests: [],
pages: []
};
function recordTest(page, name, status, detail) {
results.tests.push({ page, name, status, detail });
const icon = status === 'PASS' ? '✓' : status === 'FAIL' ? '✗' : '!';
console.log(` [${icon}] ${name}${detail ? ' - ' + detail : ''}`);
}
try {
// ========== 1. 首页 Dashboard ==========
console.log('\n========== 页面1: 首页 Dashboard ==========');
results.pages.push('Dashboard');
await page.goto(FRONTEND_URL, { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForTimeout(2000);
await screenshot('01-dashboard');
// 验证余额卡片
const balanceText = await page.locator('.balance-amount').first().innerText().catch(() => null);
recordTest('Dashboard', balanceText && balanceText.includes('¥') ? 'PASS' : 'FAIL',
'余额显示', balanceText);
// 验证本月收入/支出显示
const incomeText = await page.locator('.stat-value.income').first().innerText().catch(() => null);
recordTest('Dashboard', incomeText && incomeText.length > 0 ? 'PASS' : 'FAIL',
'本月收入显示', incomeText);
const expenseText = await page.locator('.stat-value.expense').first().innerText().catch(() => null);
recordTest('Dashboard', expenseText && expenseText.length > 0 ? 'PASS' : 'FAIL',
'本月支出显示', expenseText);
// 验证快捷操作按钮
const quickActions = await page.locator('.quick-actions .btn').count().catch(() => 0);
recordTest('Dashboard', quickActions >= 2 ? 'PASS' : 'FAIL',
'快捷操作按钮', `${quickActions}个按钮`);
// 验证最近记录
const recentRecords = await page.locator('.records-card .record-item').count().catch(() => 0);
recordTest('Dashboard', recentRecords > 0 ? 'PASS' : 'WARN',
'最近记录', `${recentRecords}条记录`);
// 验证预算进度区域
const budgetSection = await page.locator('.budget-card').count().catch(() => 0);
recordTest('Dashboard', budgetSection > 0 ? 'PASS' : 'WARN',
'预算进度区域', budgetSection > 0 ? '存在' : '不存在或无预算数据');
// ========== 2. 账单明细页 ==========
console.log('\n========== 页面2: 账单明细 Record ==========');
results.pages.push('Record');
// 点击导航栏的"账单"链接
await page.locator('a[href="/record"], a:has-text("账单")').first().click().catch(async () => {
// 如果找不到导航,直接导航到URL
await page.goto(`${FRONTEND_URL}/record`, { waitUntil: 'networkidle', timeout: 15000 });
});
await page.waitForTimeout(2000);
await screenshot('02-record-all');
// 验证页面标题
const pageTitle = await page.locator('.page-title').innerText().catch(() => '');
recordTest('Record', pageTitle.includes('明细') || pageTitle.includes('账单') ? 'PASS' : 'FAIL',
'页面标题', pageTitle);
// 验证筛选按钮
const filterBtns = await page.locator('.filter-btn').count().catch(() => 0);
recordTest('Record', filterBtns >= 3 ? 'PASS' : 'FAIL',
'筛选按钮', `${filterBtns}个筛选按钮`);
// 验证记录列表
const recordItems = await page.locator('.record-item').count().catch(() => 0);
recordTest('Record', recordItems > 0 ? 'PASS' : 'FAIL',
'记录列表', `${recordItems}条记录`);
// 验证支出记录(红色)
const expenseRecords = await page.locator('.record-amount.expense').count().catch(() => 0);
recordTest('Record', expenseRecords > 0 ? 'PASS' : 'WARN',
'支出记录样式(红色)', `${expenseRecords}`);
// 验证收入记录(绿色)
const incomeRecords = await page.locator('.record-amount.income').count().catch(() => 0);
recordTest('Record', incomeRecords > 0 ? 'PASS' : 'WARN',
'收入记录样式(绿色)', `${incomeRecords}`);
// 测试支出筛选
await page.locator('.filter-btn[data-filter="expense"]').click().catch(() => {});
await page.waitForTimeout(1000);
await screenshot('03-record-expense');
const filteredExpenses = await page.locator('.record-item').count().catch(() => 0);
recordTest('Record', filteredExpenses > 0 ? 'PASS' : 'FAIL',
'支出筛选结果', `${filteredExpenses}`);
// 测试收入筛选
await page.locator('.filter-btn[data-filter="income"]').click().catch(() => {});
await page.waitForTimeout(1000);
await screenshot('04-record-income');
const filteredIncomes = await page.locator('.record-item').count().catch(() => 0);
recordTest('Record', filteredIncomes > 0 ? 'PASS' : 'FAIL',
'收入筛选结果', `${filteredIncomes}`);
// 回到全部
await page.locator('.filter-btn[data-filter="all"]').click().catch(() => {});
await page.waitForTimeout(1000);
// 验证新增按钮
const fabBtn = await page.locator('.fab').count().catch(() => 0);
recordTest('Record', fabBtn > 0 ? 'PASS' : 'FAIL',
'新增浮动按钮', fabBtn > 0 ? '存在' : '不存在');
// ========== 3. 预算管理页 ==========
console.log('\n========== 页面3: 预算管理 Budget ==========');
results.pages.push('Budget');
await page.locator('a[href="/budget"], a:has-text("预算")').first().click().catch(async () => {
await page.goto(`${FRONTEND_URL}/budget`, { waitUntil: 'networkidle', timeout: 15000 });
});
await page.waitForTimeout(2000);
await screenshot('05-budget');
// 验证页面标题
const budgetTitle = await page.locator('.page-title').innerText().catch(() => '');
recordTest('Budget', budgetTitle.includes('预算') ? 'PASS' : 'FAIL',
'页面标题', budgetTitle);
// 验证预算概览
const budgetSummary = await page.locator('.budget-summary').count().catch(() => 0);
recordTest('Budget', budgetSummary > 0 ? 'PASS' : 'FAIL',
'预算概览区域', budgetSummary > 0 ? '存在' : '不存在');
// 验证分类卡片数量
const categoryCards = await page.locator('.category-card').count().catch(() => 0);
recordTest('Budget', categoryCards >= 6 ? 'PASS' : 'WARN',
'分类预算卡片', `${categoryCards}个 (预期6个)`);
// 验证设置预算按钮
const setBudgetBtn = await page.locator('.btn-primary:has-text("设置预算")').count().catch(() => 0);
recordTest('Budget', setBudgetBtn > 0 ? 'PASS' : 'FAIL',
'设置预算按钮', setBudgetBtn > 0 ? '存在' : '不存在');
// ========== 4. 统计报表页 ==========
console.log('\n========== 页面4: 统计报表 Statistics ==========');
results.pages.push('Statistics');
await page.locator('a[href="/statistics"], a:has-text("统计")').first().click().catch(async () => {
await page.goto(`${FRONTEND_URL}/statistics`, { waitUntil: 'networkidle', timeout: 15000 });
});
await page.waitForTimeout(3000); // ECharts需要更多渲染时间
await screenshot('06-statistics-line');
// 验证页面标题
const statsTitle = await page.locator('.page-title').innerText().catch(() => '');
recordTest('Statistics', statsTitle.includes('统计') ? 'PASS' : 'FAIL',
'页面标题', statsTitle);
// 验证图表类型切换
const chartTypeBtns = await page.locator('.chart-type-btn').count().catch(() => 0);
recordTest('Statistics', chartTypeBtns >= 3 ? 'PASS' : 'FAIL',
'图表类型切换按钮', `${chartTypeBtns}个 (折线/柱状/饼图)`);
// 验证趋势图表容器
const trendChart = await page.locator('.echarts-container').first().count().catch(() => 0);
recordTest('Statistics', trendChart > 0 ? 'PASS' : 'FAIL',
'趋势图表', trendChart > 0 ? '已渲染' : '未渲染');
// 切换到饼图
await page.locator('.chart-type-btn[data-type="pie"]').click().catch(() => {});
await page.waitForTimeout(2000);
await screenshot('07-statistics-pie');
// 验证饼图渲染
const pieChart = await page.locator('.pie-chart-container').count().catch(() => 0);
recordTest('Statistics', pieChart > 0 ? 'PASS' : 'FAIL',
'饼图渲染', pieChart > 0 ? '已渲染' : '未渲染');
// 验证图例
const pieLegend = await page.locator('.legend-item').count().catch(() => 0);
recordTest('Statistics', pieLegend > 0 ? 'PASS' : 'WARN',
'饼图图例', `${pieLegend}个分类`);
// 验证支出构成金额显示
const pieCenterText = await page.locator('.pie-chart-wrapper').innerText().catch(() => '');
recordTest('Statistics', pieCenterText.includes('总支出') ? 'PASS' : 'WARN',
'饼图中心文本', pieCenterText.includes('总支出') ? '显示总支出' : '未显示总支出');
// 验证月度对比图表
await page.locator('.chart-type-btn[data-type="line"]').click().catch(() => {});
await page.waitForTimeout(1000);
const compareChart = await page.locator('.echarts-container').count().catch(() => 0);
recordTest('Statistics', compareChart >= 2 ? 'PASS' : 'FAIL',
'月度对比图表', `${compareChart}个图表容器`);
// ========== 5. API数据一致性验证 ==========
console.log('\n========== 数据一致性验证 ==========');
// 获取API数据
const recordsAPI = await apiGet('/api/records?userId=1');
const dashboardAPI = await apiGet('/api/dashboard/summary?userId=1');
const statsAPI = await apiGet('/api/statistics/monthly?userId=1&month=2026-04');
if (recordsAPI.success && dashboardAPI.success && statsAPI.success) {
// 验证记录总数
const totalRecords = recordsAPI.data.length;
recordTest('API', totalRecords > 0 ? 'PASS' : 'FAIL',
'API记录总数', `${totalRecords}`);
// 验证支出类别覆盖
const expenseCategories = [...new Set(recordsAPI.data.filter(r => r.type === 'expense').map(r => r.category))];
const expectedExpenseCategories = ['餐饮', '交通', '购物', '娱乐', '医疗', '其他'];
const missingExpCats = expectedExpenseCategories.filter(c => !expenseCategories.includes(c));
recordTest('API', missingExpCats.length === 0 ? 'PASS' : 'WARN',
'支出类别覆盖', missingExpCats.length === 0 ? '全部6个类别都有数据' : `缺少: ${missingExpCats.join(', ')}`);
// 验证收入类别覆盖
const incomeCategories = [...new Set(recordsAPI.data.filter(r => r.type === 'income').map(r => r.category))];
const expectedIncomeCategories = ['工资', '奖金', '投资', '兼职', '理财', '其他'];
const missingIncCats = expectedIncomeCategories.filter(c => !incomeCategories.includes(c));
recordTest('API', missingIncCats.length === 0 ? 'PASS' : 'WARN',
'收入类别覆盖', missingIncCats.length === 0 ? '全部6个类别都有数据' : `缺少: ${missingIncCats.join(', ')}`);
// 验证Dashboard与Stats一致性
const dashIncome = dashboardAPI.data.monthIncome;
const statsIncome = statsAPI.data.totalIncome;
recordTest('API', Math.abs(dashIncome - statsIncome) < 0.01 ? 'PASS' : 'FAIL',
'Dashboard/Stat收入一致性', `Dashboard: ¥${dashIncome}, Stats: ¥${statsIncome}`);
const dashExpense = dashboardAPI.data.monthExpense;
const statsExpense = statsAPI.data.totalExpense;
recordTest('API', Math.abs(dashExpense - statsExpense) < 0.01 ? 'PASS' : 'FAIL',
'Dashboard/Stat支出一致性', `Dashboard: ¥${dashExpense}, Stats: ¥${statsExpense}`);
}
// ========== 生成汇总 ==========
console.log('\n===========================================');
const totalTests = results.tests.length;
const passedTests = results.tests.filter(t => t.status === 'PASS').length;
const failedTests = results.tests.filter(t => t.status === 'FAIL').length;
const warnTests = results.tests.filter(t => t.status === 'WARN').length;
console.log(`页面验证测试: ${totalTests}项 | 通过: ${passedTests} | 失败: ${failedTests} | 警告: ${warnTests}`);
console.log(`截图数量: ${screenshotNum}`);
console.log(`截图目录: ${SCREENSHOT_DIR}`);
console.log('===========================================');
// 保存JSON结果
const jsonPath = path.join(SCREENSHOT_DIR, 'browser-test-results.json');
fs.writeFileSync(jsonPath, JSON.stringify(results, null, 2));
console.log(`\n结果保存至: ${jsonPath}`);
} catch (err) {
console.error('[BROWSER TEST ERROR]', err.message);
await screenshot('error-state');
} finally {
await browser.close();
}
}
main();