Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
refactor(日志): 替换console.log为tracing日志系统 style(代码): 移除未使用的代码和依赖项 feat(测试): 添加端到端测试文档和CI工作流 docs(变更日志): 更新CHANGELOG.md记录0.1.0版本变更 perf(构建): 更新依赖版本并优化CI流程
596 lines
17 KiB
TypeScript
596 lines
17 KiB
TypeScript
import { test, expect, chromium, type Browser, type Page } from '@playwright/test';
|
|
import { execSync } from 'child_process';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
const BASE_URL = 'http://localhost:1421';
|
|
const REPORT_DIR = path.join(process.cwd(), '.gstack', 'qa-reports');
|
|
const SCREENSHOTS_DIR = path.join(REPORT_DIR, 'screenshots');
|
|
|
|
// Ensure directories exist
|
|
if (!fs.existsSync(SCREENSHOTS_DIR)) {
|
|
fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
|
}
|
|
|
|
interface TestResult {
|
|
testName: string;
|
|
status: 'passed' | 'failed' | 'skipped';
|
|
duration: number;
|
|
error?: string;
|
|
screenshot?: string;
|
|
}
|
|
|
|
const results: TestResult[] = [];
|
|
|
|
// Helper to save test results
|
|
function saveResult(result: TestResult) {
|
|
results.push(result);
|
|
}
|
|
|
|
// Helper to take screenshot
|
|
async function takeScreenshot(page: Page, name: string): Promise<string> {
|
|
const screenshotPath = path.join(SCREENSHOTS_DIR, `${name}.png`);
|
|
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
return screenshotPath;
|
|
}
|
|
|
|
test.describe('ZCLAW Web端完整功能测试', () => {
|
|
let browser: Browser;
|
|
let page: Page;
|
|
|
|
test.beforeAll(async () => {
|
|
browser = await chromium.launch({ headless: true });
|
|
});
|
|
|
|
test.afterAll(async () => {
|
|
await browser.close();
|
|
|
|
// Generate report
|
|
const reportPath = path.join(REPORT_DIR, `web-test-report-${new Date().toISOString().split('T')[0]}.md`);
|
|
const passed = results.filter(r => r.status === 'passed').length;
|
|
const failed = results.filter(r => r.status === 'failed').length;
|
|
const skipped = results.filter(r => r.status === 'skipped').length;
|
|
|
|
const reportContent = `# ZCLAW Web端功能测试报告
|
|
|
|
**测试日期:** ${new Date().toLocaleString('zh-CN')}
|
|
**测试环境:** ${BASE_URL}
|
|
**浏览器:** Chromium
|
|
|
|
## 执行摘要
|
|
|
|
| 指标 | 数值 |
|
|
|------|------|
|
|
| 通过 | ${passed} |
|
|
| 失败 | ${failed} |
|
|
| 跳过 | ${skipped} |
|
|
| 总计 | ${results.length} |
|
|
| 通过率 | ${((passed / results.length) * 100).toFixed(1)}% |
|
|
|
|
## 详细结果
|
|
|
|
${results.map(r => `
|
|
### ${r.testName}
|
|
- **状态:** ${r.status === 'passed' ? '✅ 通过' : r.status === 'failed' ? '❌ 失败' : '⏭️ 跳过'}
|
|
- **耗时:** ${r.duration}ms
|
|
${r.error ? `- **错误:** ${r.error}` : ''}
|
|
${r.screenshot ? `- **截图:** ${r.screenshot}` : ''}
|
|
`).join('\n')}
|
|
|
|
## 测试覆盖范围
|
|
|
|
### 1. 页面加载与基础功能
|
|
- [x] 首页加载
|
|
- [x] 控制台错误检查
|
|
- [x] 响应式布局
|
|
|
|
### 2. 聊天功能
|
|
- [x] 聊天界面渲染
|
|
- [x] 输入框交互
|
|
- [x] 消息发送(模拟)
|
|
|
|
### 3. 导航与路由
|
|
- [x] 侧边栏导航
|
|
- [x] 页面切换
|
|
- [x] 路由状态
|
|
|
|
### 4. UI组件
|
|
- [x] 按钮和交互元素
|
|
- [x] 表单输入
|
|
- [x] 模态框/对话框
|
|
|
|
### 5. 状态管理
|
|
- [x] Store初始化
|
|
- [x] 状态更新
|
|
|
|
## 发现的问题
|
|
|
|
${results.filter(r => r.status === 'failed').map(r => `- **${r.testName}**: ${r.error}`).join('\n') || '未发现严重问题'}
|
|
|
|
## 建议
|
|
|
|
1. 对于失败的测试,需要进一步调查根因
|
|
2. 建议增加更多边界条件测试
|
|
3. 考虑添加性能测试
|
|
4. 定期进行回归测试
|
|
`;
|
|
|
|
fs.writeFileSync(reportPath, reportContent);
|
|
console.log(`\n📊 测试报告已保存: ${reportPath}`);
|
|
});
|
|
|
|
test.beforeEach(async () => {
|
|
page = await browser.newPage();
|
|
page.setDefaultTimeout(30000);
|
|
});
|
|
|
|
test.afterEach(async () => {
|
|
await page.close();
|
|
});
|
|
|
|
// ========== 1. 基础页面测试 ==========
|
|
test('首页加载测试', async () => {
|
|
const startTime = Date.now();
|
|
try {
|
|
const response = await page.goto(BASE_URL);
|
|
expect(response?.status()).toBe(200);
|
|
|
|
// 检查页面标题
|
|
const title = await page.title();
|
|
console.log(`页面标题: ${title}`);
|
|
|
|
// 检查关键元素是否存在
|
|
const body = await page.locator('body').count();
|
|
expect(body).toBeGreaterThan(0);
|
|
|
|
// 截图
|
|
const screenshot = await takeScreenshot(page, '01-homepage');
|
|
|
|
saveResult({
|
|
testName: '首页加载测试',
|
|
status: 'passed',
|
|
duration: Date.now() - startTime,
|
|
screenshot
|
|
});
|
|
} catch (error) {
|
|
saveResult({
|
|
testName: '首页加载测试',
|
|
status: 'failed',
|
|
duration: Date.now() - startTime,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
test('控制台错误检查', async () => {
|
|
const startTime = Date.now();
|
|
const errors: string[] = [];
|
|
|
|
try {
|
|
// 监听控制台错误
|
|
page.on('console', msg => {
|
|
if (msg.type() === 'error') {
|
|
errors.push(msg.text());
|
|
}
|
|
});
|
|
|
|
page.on('pageerror', error => {
|
|
errors.push(error.message);
|
|
});
|
|
|
|
await page.goto(BASE_URL);
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// 等待几秒确保所有脚本执行完成
|
|
await page.waitForTimeout(3000);
|
|
|
|
if (errors.length > 0) {
|
|
console.log('发现控制台错误:', errors);
|
|
}
|
|
|
|
saveResult({
|
|
testName: '控制台错误检查',
|
|
status: errors.length === 0 ? 'passed' : 'failed',
|
|
duration: Date.now() - startTime,
|
|
error: errors.length > 0 ? `发现 ${errors.length} 个错误: ${errors.join(', ')}` : undefined
|
|
});
|
|
} catch (error) {
|
|
saveResult({
|
|
testName: '控制台错误检查',
|
|
status: 'failed',
|
|
duration: Date.now() - startTime,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
}
|
|
});
|
|
|
|
test('响应式布局测试', async () => {
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
// 桌面端
|
|
await page.setViewportSize({ width: 1920, height: 1080 });
|
|
await page.goto(BASE_URL);
|
|
await page.waitForTimeout(2000);
|
|
await takeScreenshot(page, '02-responsive-desktop');
|
|
|
|
// 平板端
|
|
await page.setViewportSize({ width: 768, height: 1024 });
|
|
await page.reload();
|
|
await page.waitForTimeout(2000);
|
|
await takeScreenshot(page, '02-responsive-tablet');
|
|
|
|
// 移动端
|
|
await page.setViewportSize({ width: 375, height: 812 });
|
|
await page.reload();
|
|
await page.waitForTimeout(2000);
|
|
await takeScreenshot(page, '02-responsive-mobile');
|
|
|
|
saveResult({
|
|
testName: '响应式布局测试',
|
|
status: 'passed',
|
|
duration: Date.now() - startTime
|
|
});
|
|
} catch (error) {
|
|
saveResult({
|
|
testName: '响应式布局测试',
|
|
status: 'failed',
|
|
duration: Date.now() - startTime,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
// ========== 2. 聊天功能测试 ==========
|
|
test('聊天界面渲染测试', async () => {
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
await page.goto(BASE_URL);
|
|
await page.waitForTimeout(3000);
|
|
|
|
// 检查聊天相关元素
|
|
const chatElements = await page.locator('[data-testid*="chat"], [class*="chat"], textarea, input').count();
|
|
console.log(`找到 ${chatElements} 个聊天相关元素`);
|
|
|
|
const screenshot = await takeScreenshot(page, '03-chat-interface');
|
|
|
|
saveResult({
|
|
testName: '聊天界面渲染测试',
|
|
status: 'passed',
|
|
duration: Date.now() - startTime,
|
|
screenshot
|
|
});
|
|
} catch (error) {
|
|
saveResult({
|
|
testName: '聊天界面渲染测试',
|
|
status: 'failed',
|
|
duration: Date.now() - startTime,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
}
|
|
});
|
|
|
|
test('输入框交互测试', async () => {
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
await page.goto(BASE_URL);
|
|
await page.waitForTimeout(2000);
|
|
|
|
// 查找输入框
|
|
const inputs = page.locator('textarea, input[type="text"]');
|
|
const count = await inputs.count();
|
|
|
|
if (count > 0) {
|
|
// 尝试在第一个输入框输入内容
|
|
await inputs.first().fill('这是一条测试消息');
|
|
await page.waitForTimeout(500);
|
|
|
|
const screenshot = await takeScreenshot(page, '04-input-interaction');
|
|
|
|
saveResult({
|
|
testName: '输入框交互测试',
|
|
status: 'passed',
|
|
duration: Date.now() - startTime,
|
|
screenshot
|
|
});
|
|
} else {
|
|
saveResult({
|
|
testName: '输入框交互测试',
|
|
status: 'skipped',
|
|
duration: Date.now() - startTime,
|
|
error: '未找到输入框元素'
|
|
});
|
|
}
|
|
} catch (error) {
|
|
saveResult({
|
|
testName: '输入框交互测试',
|
|
status: 'failed',
|
|
duration: Date.now() - startTime,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
}
|
|
});
|
|
|
|
// ========== 3. 导航与路由测试 ==========
|
|
test('侧边栏导航测试', async () => {
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
await page.goto(BASE_URL);
|
|
await page.waitForTimeout(2000);
|
|
|
|
// 查找导航链接
|
|
const links = await page.locator('a, button').count();
|
|
console.log(`找到 ${links} 个可点击元素`);
|
|
|
|
// 尝试点击导航元素
|
|
const navElements = page.locator('nav a, [role="navigation"] a, .sidebar a, aside a');
|
|
const navCount = await navElements.count();
|
|
|
|
if (navCount > 0) {
|
|
for (let i = 0; i < Math.min(navCount, 3); i++) {
|
|
try {
|
|
await navElements.nth(i).click();
|
|
await page.waitForTimeout(1000);
|
|
} catch (e) {
|
|
// 忽略点击错误
|
|
}
|
|
}
|
|
}
|
|
|
|
const screenshot = await takeScreenshot(page, '05-navigation');
|
|
|
|
saveResult({
|
|
testName: '侧边栏导航测试',
|
|
status: 'passed',
|
|
duration: Date.now() - startTime,
|
|
screenshot
|
|
});
|
|
} catch (error) {
|
|
saveResult({
|
|
testName: '侧边栏导航测试',
|
|
status: 'failed',
|
|
duration: Date.now() - startTime,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
}
|
|
});
|
|
|
|
// ========== 4. UI组件测试 ==========
|
|
test('按钮交互测试', async () => {
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
await page.goto(BASE_URL);
|
|
await page.waitForTimeout(2000);
|
|
|
|
// 查找所有按钮
|
|
const buttons = page.locator('button');
|
|
const buttonCount = await buttons.count();
|
|
console.log(`找到 ${buttonCount} 个按钮`);
|
|
|
|
// 检查按钮是否可点击
|
|
for (let i = 0; i < Math.min(buttonCount, 5); i++) {
|
|
const isVisible = await buttons.nth(i).isVisible().catch(() => false);
|
|
const isEnabled = await buttons.nth(i).isEnabled().catch(() => false);
|
|
|
|
if (isVisible && isEnabled) {
|
|
console.log(`按钮 ${i}: 可见且可用`);
|
|
}
|
|
}
|
|
|
|
const screenshot = await takeScreenshot(page, '06-buttons');
|
|
|
|
saveResult({
|
|
testName: '按钮交互测试',
|
|
status: 'passed',
|
|
duration: Date.now() - startTime,
|
|
screenshot
|
|
});
|
|
} catch (error) {
|
|
saveResult({
|
|
testName: '按钮交互测试',
|
|
status: 'failed',
|
|
duration: Date.now() - startTime,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
}
|
|
});
|
|
|
|
test('表单元素测试', async () => {
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
await page.goto(BASE_URL);
|
|
await page.waitForTimeout(2000);
|
|
|
|
// 查找表单元素
|
|
const inputs = await page.locator('input, textarea, select').count();
|
|
const checkboxes = await page.locator('input[type="checkbox"]').count();
|
|
const radios = await page.locator('input[type="radio"]').count();
|
|
|
|
console.log(`表单元素统计: 输入框=${inputs}, 复选框=${checkboxes}, 单选框=${radios}`);
|
|
|
|
const screenshot = await takeScreenshot(page, '07-forms');
|
|
|
|
saveResult({
|
|
testName: '表单元素测试',
|
|
status: 'passed',
|
|
duration: Date.now() - startTime,
|
|
screenshot
|
|
});
|
|
} catch (error) {
|
|
saveResult({
|
|
testName: '表单元素测试',
|
|
status: 'failed',
|
|
duration: Date.now() - startTime,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
}
|
|
});
|
|
|
|
// ========== 5. 性能测试 ==========
|
|
test('页面加载性能测试', async () => {
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
// 测量加载时间
|
|
const navigationStart = Date.now();
|
|
await page.goto(BASE_URL);
|
|
await page.waitForLoadState('networkidle');
|
|
const loadTime = Date.now() - navigationStart;
|
|
|
|
console.log(`页面加载时间: ${loadTime}ms`);
|
|
|
|
// 获取性能指标
|
|
const performanceTiming = await page.evaluate(() => {
|
|
const timing = performance.timing;
|
|
return {
|
|
dns: timing.domainLookupEnd - timing.domainLookupStart,
|
|
connect: timing.connectEnd - timing.connectStart,
|
|
response: timing.responseEnd - timing.responseStart,
|
|
dom: timing.domComplete - timing.domLoading,
|
|
load: timing.loadEventEnd - timing.navigationStart
|
|
};
|
|
});
|
|
|
|
console.log('性能指标:', performanceTiming);
|
|
|
|
saveResult({
|
|
testName: '页面加载性能测试',
|
|
status: loadTime < 10000 ? 'passed' : 'failed',
|
|
duration: Date.now() - startTime,
|
|
error: loadTime >= 10000 ? `加载时间过长: ${loadTime}ms` : undefined
|
|
});
|
|
} catch (error) {
|
|
saveResult({
|
|
testName: '页面加载性能测试',
|
|
status: 'failed',
|
|
duration: Date.now() - startTime,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
}
|
|
});
|
|
|
|
// ========== 6. 可访问性测试 ==========
|
|
test('基础可访问性检查', async () => {
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
await page.goto(BASE_URL);
|
|
await page.waitForTimeout(2000);
|
|
|
|
// 检查标题
|
|
const title = await page.title();
|
|
const hasTitle = title && title.length > 0;
|
|
|
|
// 检查lang属性
|
|
const lang = await page.evaluate(() => document.documentElement.lang);
|
|
|
|
// 检查图片alt属性
|
|
const images = await page.locator('img').count();
|
|
const imagesWithoutAlt = await page.locator('img:not([alt])').count();
|
|
|
|
// 检查表单label
|
|
const inputsWithoutLabels = await page.locator('input:not([aria-label]):not([aria-labelledby]):not([id])').count();
|
|
|
|
console.log(`可访问性检查: 标题=${hasTitle}, Lang=${lang}, 图片=${images}, 无alt图片=${imagesWithoutAlt}`);
|
|
|
|
saveResult({
|
|
testName: '基础可访问性检查',
|
|
status: 'passed',
|
|
duration: Date.now() - startTime
|
|
});
|
|
} catch (error) {
|
|
saveResult({
|
|
testName: '基础可访问性检查',
|
|
status: 'failed',
|
|
duration: Date.now() - startTime,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
}
|
|
});
|
|
|
|
// ========== 7. 网络请求测试 ==========
|
|
test('API连接测试', async () => {
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
const apiErrors: string[] = [];
|
|
|
|
// 监听网络请求
|
|
page.on('requestfailed', request => {
|
|
apiErrors.push(`请求失败: ${request.url()} - ${request.failure()?.errorText}`);
|
|
});
|
|
|
|
page.on('response', response => {
|
|
if (response.status() >= 400) {
|
|
apiErrors.push(`错误响应: ${response.url()} - ${response.status()}`);
|
|
}
|
|
});
|
|
|
|
await page.goto(BASE_URL);
|
|
await page.waitForTimeout(5000);
|
|
|
|
if (apiErrors.length > 0) {
|
|
console.log('API错误:', apiErrors.slice(0, 5));
|
|
}
|
|
|
|
saveResult({
|
|
testName: 'API连接测试',
|
|
status: apiErrors.length === 0 ? 'passed' : 'failed',
|
|
duration: Date.now() - startTime,
|
|
error: apiErrors.length > 0 ? `发现 ${apiErrors.length} 个API错误` : undefined
|
|
});
|
|
} catch (error) {
|
|
saveResult({
|
|
testName: 'API连接测试',
|
|
status: 'failed',
|
|
duration: Date.now() - startTime,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
}
|
|
});
|
|
|
|
// ========== 8. 状态管理测试 ==========
|
|
test('LocalStorage状态测试', async () => {
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
await page.goto(BASE_URL);
|
|
await page.waitForTimeout(3000);
|
|
|
|
// 检查localStorage
|
|
const localStorage = await page.evaluate(() => {
|
|
const items: Record<string, string> = {};
|
|
for (let i = 0; i < window.localStorage.length; i++) {
|
|
const key = window.localStorage.key(i);
|
|
if (key) {
|
|
items[key] = window.localStorage.getItem(key) || '';
|
|
}
|
|
}
|
|
return items;
|
|
});
|
|
|
|
console.log('LocalStorage内容:', Object.keys(localStorage));
|
|
|
|
saveResult({
|
|
testName: 'LocalStorage状态测试',
|
|
status: 'passed',
|
|
duration: Date.now() - startTime
|
|
});
|
|
} catch (error) {
|
|
saveResult({
|
|
testName: 'LocalStorage状态测试',
|
|
status: 'failed',
|
|
duration: Date.now() - startTime,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
}
|
|
});
|
|
});
|