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();