304 lines
14 KiB
JavaScript
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();
|