Some checks failed
Deploy / deploy (push) Has been cancelled
Full-stack web application for Telegram management - Frontend: Vue 3 + Vben Admin - Backend: NestJS - Features: User management, group broadcast, statistics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
385 lines
11 KiB
TypeScript
385 lines
11 KiB
TypeScript
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<MenuStats> {
|
||
// 等待菜单容器加载
|
||
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;
|
||
}
|