/** * ZCLAW 快速功能验证 - Playwright CLI */ import { chromium } from 'playwright'; const BASE_URL = 'http://localhost:1420'; const SCREENSHOT_DIR = 'test-results/screenshots'; const results = { passed: [], failed: [], warnings: [], info: [] }; function log(msg) { console.log(`[${new Date().toISOString().slice(11, 19)}] ${msg}`); } async function screenshot(page, name) { try { await page.screenshot({ path: `${SCREENSHOT_DIR}/${name}.png`, fullPage: true }); log(`📸 ${name}.png`); } catch (e) {} } async function test(name, fn) { try { await fn(); results.passed.push(name); log(`✅ ${name}`); } catch (e) { results.failed.push({ name, error: e.message.slice(0, 100) }); log(`❌ ${name}`); } } async function main() { log('🚀 ZCLAW 快速功能验证'); log(`📍 ${BASE_URL}`); const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } }); const logs = []; page.on('console', msg => logs.push(`[${msg.type()}] ${msg.text()}`)); page.on('pageerror', e => logs.push(`[error] ${e.message}`)); try { // === 应用加载 === log('\n📋 应用加载'); await page.goto(BASE_URL, { timeout: 10000 }); await page.waitForTimeout(2000); await screenshot(page, '01-app'); // === 页面结构检查 === log('\n📋 页面结构'); await test('主区域', async () => { const main = page.locator('main'); if (!await main.isVisible()) throw new Error('主区域不可见'); }); await test('左侧边栏', async () => { const sidebar = page.locator('aside').first(); if (!await sidebar.isVisible()) throw new Error('左侧边栏不可见'); }); await test('右侧边栏', async () => { const rightPanel = page.locator('aside').last(); if (!await rightPanel.isVisible()) throw new Error('右侧边栏不可见'); }); // === 标签检查 === log('\n📋 标签导航'); const tabNames = ['分身', 'Hands', '工作流', '团队', '协作']; for (const name of tabNames) { await test(`标签: ${name}`, async () => { const tab = page.getByRole('button', { name: new RegExp(name, 'i') }); if (await tab.count() === 0) throw new Error('标签不存在'); }); } await screenshot(page, '02-tabs'); // === 标签切换测试 (只测前3个) === log('\n📋 标签切换'); for (const name of ['分身', 'Hands', '工作流']) { await test(`切换: ${name}`, async () => { const tab = page.getByRole('button', { name: new RegExp(name, 'i') }).first(); await tab.click({ timeout: 3000 }); await page.waitForTimeout(300); }); } await screenshot(page, '03-tab-switch'); // === 设置页面 === log('\n📋 设置'); await test('设置按钮', async () => { const btn = page.getByRole('button', { name: /设置|Settings/i }); if (await btn.count() === 0) throw new Error('设置按钮不存在'); }); await test('打开设置', async () => { const btn = page.getByRole('button', { name: /设置|Settings/i }).first(); await btn.click({ timeout: 3000 }); await page.waitForTimeout(500); await screenshot(page, '04-settings'); }); // === 聊天功能 === log('\n📋 聊天'); await page.goto(BASE_URL, { timeout: 5000 }); await page.waitForTimeout(1000); await test('聊天输入框', async () => { const input = page.locator('textarea'); if (await input.count() === 0) throw new Error('输入框不存在'); if (await input.first().isDisabled()) { results.warnings.push('输入框已禁用 - 需 Gateway 连接'); } }); await test('发送按钮', async () => { const btn = page.getByRole('button', { name: /发送|Send/i }); if (await btn.count() === 0) throw new Error('发送按钮不存在'); }); await test('模型选择器', async () => { const selector = page.getByRole('button', { name: /模型|Model/i }); if (await selector.count() === 0) throw new Error('模型选择器不存在'); }); await screenshot(page, '05-chat'); // === Gateway 状态 === log('\n📋 Gateway 状态'); await test('连接状态显示', async () => { const content = await page.content(); if (content.includes('Connecting') || content.includes('未连接')) { results.info.push('Gateway 状态: 未连接/连接中'); } else if (content.includes('已连接')) { results.info.push('Gateway 状态: 已连接'); } }); // === 控制台错误 === log('\n📋 控制台检查'); const jsErrors = logs.filter(l => l.includes('[error]') && !l.includes('DevTools')); if (jsErrors.length > 0) { results.warnings.push(`JS 错误: ${jsErrors.length} 个`); } // === 响应式 === log('\n📋 响应式'); await test('移动端', async () => { await page.setViewportSize({ width: 375, height: 667 }); await page.waitForTimeout(300); }); await test('桌面', async () => { await page.setViewportSize({ width: 1920, height: 1080 }); await page.waitForTimeout(300); }); await screenshot(page, '06-responsive'); // === 性能 === log('\n📋 性能'); await test('DOM 数量', async () => { const count = await page.evaluate(() => document.querySelectorAll('*').length); results.info.push(`DOM 节点: ${count}`); if (count > 3000) results.warnings.push(`DOM 过多: ${count}`); }); } catch (e) { log(`❌ 执行出错: ${e.message}`); } finally { await browser.close(); } // 报告 console.log('\n' + '='.repeat(60)); console.log('📊 ZCLAW 功能验证报告'); console.log('='.repeat(60)); console.log(`\n✅ 通过: ${results.passed.length}`); results.passed.forEach(n => console.log(` - ${n}`)); console.log(`\n❌ 失败: ${results.failed.length}`); results.failed.forEach(f => console.log(` - ${f.name}: ${f.error}`)); console.log(`\n⚠️ 警告: ${results.warnings.length}`); results.warnings.forEach(w => console.log(` - ${w}`)); console.log(`\nℹ️ 信息: ${results.info.length}`); results.info.forEach(i => console.log(` - ${i}`)); const total = results.passed.length + results.failed.length; const rate = total > 0 ? ((results.passed.length / total) * 100).toFixed(1) : 0; console.log(`\n📈 通过率: ${rate}% (${results.passed.length}/${total})`); process.exit(results.failed.length > 0 ? 1 : 0); } main().catch(console.error);