import { test, expect, type Page } from '@playwright/test'; interface MenuStats { totalCount: number; menuItems: string[]; categories: string[]; timestamp: string; } interface MenuComparison { backend: MenuStats; mock: MenuStats; differences: { missingInMock: string[]; onlyInMock: string[]; totalDifference: number; }; } /** * 菜单对比测试 * 测试前后端菜单显示差异,找出不一致的地方 */ test.describe('菜单显示对比测试', () => { let comparison: MenuComparison; test.beforeAll(async () => { console.log('🚀 开始菜单对比测试'); }); test('1️⃣ 后端启动状态下的菜单测试', async ({ page }) => { console.log('📊 测试后端启动状态下的菜单'); // 访问应用 await page.goto('http://localhost:5174'); await page.waitForTimeout(2000); // 检查是否在登录页面 const isLoginPage = await page .locator('input[placeholder*="用户名"], input[placeholder*="admin"]') .isVisible(); if (isLoginPage) { console.log('🔐 检测到登录页面,开始登录'); // 填写登录信息 await page.fill( 'input[placeholder*="用户名"], input[placeholder*="admin"]', 'admin', ); await page.fill( 'input[placeholder*="密码"], input[type="password"]', '111111', ); // 点击登录按钮 await page.click('button[type="submit"], button:has-text("登录")'); await page.waitForTimeout(3000); } // 等待菜单加载 await page.waitForSelector( 'aside[class*="sidebar"], nav[class*="menu"], .ant-menu', { timeout: 10000 }, ); await page.waitForTimeout(2000); // 获取所有菜单项 const menuStats = await extractMenuItems(page); // 截图保存 await page.screenshot({ path: 'test-results/screenshots/backend-menu-full.png', fullPage: true, }); console.log(`✅ 后端模式菜单统计:`); console.log(` - 总菜单项: ${menuStats.totalCount}`); console.log(` - 主要分类: ${menuStats.categories.length}`); console.log( ` - 菜单列表: ${menuStats.menuItems.slice(0, 10).join(', ')}${menuStats.menuItems.length > 10 ? '...' : ''}`, ); comparison = { backend: menuStats, mock: { totalCount: 0, menuItems: [], categories: [], timestamp: '' }, differences: { missingInMock: [], onlyInMock: [], totalDifference: 0 }, }; }); test('2️⃣ Mock模式下的菜单测试', async ({ page, browser }) => { console.log('🎭 测试Mock模式下的菜单'); // 创建新的浏览器上下文来模拟无后端状态 const context = await browser.newContext(); const mockPage = await context.newPage(); // 拦截所有API请求,模拟后端不可用 await mockPage.route('**/api/**', (route) => { route.abort('failed'); }); await mockPage.route('**/auth/**', (route) => { route.abort('failed'); }); await mockPage.route('**/system/**', (route) => { route.abort('failed'); }); // 访问应用 await mockPage.goto('http://localhost:5174'); await mockPage.waitForTimeout(3000); // 尝试登录(使用Mock数据) try { const isLoginPage = await mockPage .locator('input[placeholder*="用户名"], input[placeholder*="admin"]') .isVisible(); if (isLoginPage) { console.log('🔐 Mock模式登录测试'); // 使用Mock用户登录 await mockPage.fill( 'input[placeholder*="用户名"], input[placeholder*="admin"]', 'admin', ); await mockPage.fill( 'input[placeholder*="密码"], input[type="password"]', '123456', ); await mockPage.click('button[type="submit"], button:has-text("登录")'); await mockPage.waitForTimeout(3000); } } catch (error) { console.log('⚠️ Mock模式登录失败,尝试直接访问主页'); await mockPage.goto('http://localhost:5174/dashboard'); await mockPage.waitForTimeout(3000); } // 等待菜单加载或超时 try { await mockPage.waitForSelector( 'aside[class*="sidebar"], nav[class*="menu"], .ant-menu', { timeout: 8000 }, ); await mockPage.waitForTimeout(2000); } catch (error) { console.log('⚠️ Mock模式菜单加载超时'); } // 获取Mock模式菜单项 const mockMenuStats = await extractMenuItems(mockPage); // 截图保存 await mockPage.screenshot({ path: 'test-results/screenshots/mock-menu-full.png', fullPage: true, }); console.log(`🎭 Mock模式菜单统计:`); console.log(` - 总菜单项: ${mockMenuStats.totalCount}`); console.log(` - 主要分类: ${mockMenuStats.categories.length}`); console.log( ` - 菜单列表: ${mockMenuStats.menuItems.slice(0, 10).join(', ')}${mockMenuStats.menuItems.length > 10 ? '...' : ''}`, ); // 更新对比数据 comparison.mock = mockMenuStats; await context.close(); }); test('3️⃣ 生成菜单对比报告', async () => { console.log('📋 生成详细对比报告'); // 计算差异 const backendMenus = new Set(comparison.backend.menuItems); const mockMenus = new Set(comparison.mock.menuItems); const missingInMock = Array.from(backendMenus).filter( (item) => !mockMenus.has(item), ); const onlyInMock = Array.from(mockMenus).filter( (item) => !backendMenus.has(item), ); comparison.differences = { missingInMock, onlyInMock, totalDifference: Math.abs( comparison.backend.totalCount - comparison.mock.totalCount, ), }; // 生成报告 const report = generateComparisonReport(comparison); // 保存报告到文件 const fs = await import('fs'); await fs.promises.writeFile( 'test-results/menu-comparison-report.json', JSON.stringify(comparison, null, 2), ); // 输出控制台报告 console.log('\n' + '='.repeat(80)); console.log('🔍 菜单对比分析报告'); console.log('='.repeat(80)); console.log(report); console.log('='.repeat(80)); // 断言检查 expect(comparison.backend.totalCount).toBeGreaterThan(0); // 注意:Mock模式可能完全没有菜单,这是正常的测试结果 console.log( `⚠️ 发现重大差异: Mock模式缺失 ${comparison.differences.totalDifference} 个菜单项`, ); }); }); /** * 提取页面菜单项信息 */ async function extractMenuItems(page: Page): Promise { // 等待菜单容器加载 await page.waitForTimeout(2000); try { // 尝试多种菜单选择器 const menuSelectors = [ '.ant-menu-item', '.ant-menu-submenu-title', '[role="menuitem"]', 'li[class*="menu"]', 'a[class*="menu"]', '.sidebar-menu-item', '.nav-item', ]; let allMenuItems: string[] = []; let categories: string[] = []; for (const selector of menuSelectors) { try { const elements = await page.locator(selector).all(); for (const element of elements) { const text = await element.textContent(); if (text && text.trim()) { const cleanText = text.trim(); if (!allMenuItems.includes(cleanText)) { allMenuItems.push(cleanText); } } } } catch (error) { // 忽略选择器错误,继续下一个 } } // 尝试获取主要分类 try { const categorySelectors = [ '.ant-menu-submenu-title', '.menu-group-title', '[class*="category"]', '.sidebar-title', ]; for (const selector of categorySelectors) { try { const elements = await page.locator(selector).all(); for (const element of elements) { const text = await element.textContent(); if (text && text.trim()) { const cleanText = text.trim(); if (!categories.includes(cleanText)) { categories.push(cleanText); } } } } catch (error) { // 忽略选择器错误 } } } catch (error) { console.log('⚠️ 获取分类失败:', error.message); } // 如果没有找到菜单项,尝试其他方法 if (allMenuItems.length === 0) { console.log('⚠️ 使用备用方法获取菜单'); const fallbackSelectors = [ 'span:has-text("仪表板")', 'span:has-text("账号管理")', 'span:has-text("群组管理")', 'span:has-text("私信群发")', 'span:has-text("系统管理")', 'a[href*="/dashboard"]', 'a[href*="/account"]', 'a[href*="/system"]', ]; for (const selector of fallbackSelectors) { try { const elements = await page.locator(selector).all(); for (const element of elements) { const text = await element.textContent(); if (text && text.trim()) { const cleanText = text.trim(); if (!allMenuItems.includes(cleanText)) { allMenuItems.push(cleanText); } } } } catch (error) { // 忽略错误 } } } return { totalCount: allMenuItems.length, menuItems: allMenuItems, categories: categories, timestamp: new Date().toISOString(), }; } catch (error) { console.log('❌ 菜单提取失败:', error.message); return { totalCount: 0, menuItems: [], categories: [], timestamp: new Date().toISOString(), }; } } /** * 生成对比报告 */ function generateComparisonReport(comparison: MenuComparison): string { const { backend, mock, differences } = comparison; let report = ''; report += `📊 菜单统计对比:\n`; report += ` 后端模式: ${backend.totalCount} 个菜单项\n`; report += ` Mock模式: ${mock.totalCount} 个菜单项\n`; report += ` 差异数量: ${differences.totalDifference} 个\n\n`; if (differences.missingInMock.length > 0) { report += `❌ Mock模式中缺失的菜单项 (${differences.missingInMock.length}个):\n`; differences.missingInMock.forEach((item) => { report += ` - ${item}\n`; }); report += '\n'; } if (differences.onlyInMock.length > 0) { report += `➕ 仅在Mock模式中存在的菜单项 (${differences.onlyInMock.length}个):\n`; differences.onlyInMock.forEach((item) => { report += ` - ${item}\n`; }); report += '\n'; } report += `🔧 建议修复方案:\n`; if (differences.missingInMock.length > 0) { report += ` 1. 在Mock数据中添加缺失的 ${differences.missingInMock.length} 个菜单项\n`; report += ` 2. 检查菜单权限配置是否一致\n`; report += ` 3. 验证路由配置完整性\n`; } if (differences.totalDifference === 0) { report += `✅ 前后端菜单完全一致,无需修复\n`; } return report; }