Files
zclaw_openfang/desktop/tests/e2e/quick-test.mjs
iven 74dbf42644 refactor(startup): simplify stack to Tauri-managed OpenFang + optional ChromeDriver
- Remove OpenFang CLI dependency from startup scripts
- OpenFang now bundled with Tauri and managed via gateway_start/gateway_status commands
- Add bootstrap screen in App.tsx to auto-start local gateway before UI loads
- Update Makefile: replace start-no-gateway with start-desktop-only
- Fix gateway config endpoints: use /api/config instead of /api/config/quick
- Add Playwright dependencies for future E2E testing
2026-03-17 14:08:03 +08:00

212 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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);