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
This commit is contained in:
211
desktop/tests/e2e/quick-test.mjs
Normal file
211
desktop/tests/e2e/quick-test.mjs
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* 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);
|
||||
Reference in New Issue
Block a user