- 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
488 lines
15 KiB
TypeScript
488 lines
15 KiB
TypeScript
/**
|
|
* ZCLAW 前端功能验证测试
|
|
*
|
|
* 验证所有核心功能的完整性和可用性
|
|
*/
|
|
|
|
import { test, expect, Page } from '@playwright/test';
|
|
|
|
// 测试超时配置
|
|
test.setTimeout(60000);
|
|
|
|
// 辅助函数:等待组件加载
|
|
async function waitForAppReady(page: Page) {
|
|
await page.waitForLoadState('networkidle');
|
|
// 等待主应用容器出现
|
|
await page.waitForSelector('.h-screen', { timeout: 10000 });
|
|
}
|
|
|
|
// 辅助函数:截图并保存
|
|
async function takeScreenshot(page: Page, name: string) {
|
|
await page.screenshot({
|
|
path: `test-results/screenshots/${name}.png`,
|
|
fullPage: true
|
|
});
|
|
}
|
|
|
|
test.describe('ZCLAW 前端功能验证', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await waitForAppReady(page);
|
|
});
|
|
|
|
test.describe('1. 应用基础渲染', () => {
|
|
test('应用容器正确渲染', async ({ page }) => {
|
|
// 检查主容器存在
|
|
const appContainer = page.locator('.h-screen');
|
|
await expect(appContainer).toBeVisible();
|
|
|
|
// 检查三栏布局 - sidebar 和 main 都应该存在
|
|
const sidebar = page.locator('aside');
|
|
const mainContent = page.locator('main');
|
|
|
|
// 验证 sidebar 和 main 都存在
|
|
await expect(sidebar.first()).toBeVisible();
|
|
await expect(mainContent).toBeVisible();
|
|
|
|
await takeScreenshot(page, '01-app-layout');
|
|
});
|
|
|
|
test('页面标题正确', async ({ page }) => {
|
|
await expect(page).toHaveTitle(/ZCLAW/);
|
|
});
|
|
});
|
|
|
|
test.describe('2. Sidebar 侧边栏导航', () => {
|
|
test('侧边栏可见并包含导航项', async ({ page }) => {
|
|
// 验证侧边栏存在
|
|
const sidebar = page.locator('aside').first();
|
|
await expect(sidebar).toBeVisible();
|
|
|
|
// 检查导航按钮存在 - 使用 role="tab" 匹配
|
|
const cloneBtn = page.getByRole('tab', { name: '分身' });
|
|
const handsBtn = page.getByRole('tab', { name: 'Hands' });
|
|
const workflowBtn = page.getByRole('tab', { name: '工作流' });
|
|
const teamBtn = page.getByRole('tab', { name: '团队' });
|
|
const swarmBtn = page.getByRole('tab', { name: '协作' });
|
|
|
|
// 验证所有导航标签都存在
|
|
await expect(cloneBtn).toBeVisible();
|
|
await expect(handsBtn).toBeVisible();
|
|
await expect(workflowBtn).toBeVisible();
|
|
await expect(teamBtn).toBeVisible();
|
|
await expect(swarmBtn).toBeVisible();
|
|
|
|
await takeScreenshot(page, '02-sidebar-navigation');
|
|
});
|
|
|
|
test('导航切换功能', async ({ page }) => {
|
|
// 尝试点击不同的导航项
|
|
const navButtons = page.locator('button').filter({
|
|
has: page.locator('svg')
|
|
});
|
|
|
|
const count = await navButtons.count();
|
|
if (count > 1) {
|
|
await navButtons.nth(1).click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// 验证视图切换
|
|
await takeScreenshot(page, '03-navigation-switch');
|
|
}
|
|
});
|
|
|
|
test('设置按钮可用', async ({ page }) => {
|
|
const settingsBtn = page.getByRole('button', { name: /settings|设置|⚙/i }).or(
|
|
page.locator('button').filter({ hasText: /设置|Settings/ })
|
|
);
|
|
|
|
if (await settingsBtn.isVisible()) {
|
|
await settingsBtn.click();
|
|
await page.waitForTimeout(300);
|
|
await takeScreenshot(page, '04-settings-access');
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('3. ChatArea 聊天功能', () => {
|
|
test('聊天区域渲染', async ({ page }) => {
|
|
// 查找聊天输入框
|
|
const chatInput = page.locator('textarea').or(
|
|
page.locator('input[type="text"]')
|
|
).or(
|
|
page.locator('[contenteditable="true"]')
|
|
);
|
|
|
|
// 检查消息区域
|
|
const messageArea = page.locator('[class*="flex-1"]').filter({
|
|
has: page.locator('[class*="message"], [class*="chat"]')
|
|
});
|
|
|
|
await takeScreenshot(page, '05-chat-area');
|
|
|
|
// 记录聊天组件状态
|
|
const inputExists = await chatInput.count() > 0;
|
|
console.log(`Chat input found: ${inputExists}`);
|
|
});
|
|
|
|
test('消息发送功能', async ({ page }) => {
|
|
const chatInput = page.locator('textarea').first();
|
|
|
|
if (await chatInput.isVisible()) {
|
|
await chatInput.fill('测试消息');
|
|
|
|
const sendBtn = page.getByRole('button', { name: '发送消息' });
|
|
|
|
if (await sendBtn.isVisible()) {
|
|
await sendBtn.click();
|
|
await page.waitForTimeout(500);
|
|
} else {
|
|
// 可能支持回车发送
|
|
await chatInput.press('Enter');
|
|
}
|
|
|
|
await takeScreenshot(page, '06-message-send');
|
|
}
|
|
});
|
|
|
|
test('会话列表渲染', async ({ page }) => {
|
|
const conversationList = page.locator('[class*="conversation"]').or(
|
|
page.locator('[class*="session"]')
|
|
).or(
|
|
page.locator('ul, ol').filter({ has: page.locator('li') })
|
|
);
|
|
|
|
await takeScreenshot(page, '07-conversation-list');
|
|
});
|
|
});
|
|
|
|
test.describe('4. Hands 系统UI', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
// 导航到 Hands 视图
|
|
const handsBtn = page.getByRole('button', { name: 'Hands' });
|
|
|
|
if (await handsBtn.isVisible()) {
|
|
await handsBtn.click();
|
|
await page.waitForTimeout(1000); // 等待数据加载
|
|
}
|
|
});
|
|
|
|
test('Hands 列表渲染', async ({ page }) => {
|
|
const handsList = page.locator('[class*="hand"]').or(
|
|
page.locator('[class*="capability"]')
|
|
);
|
|
|
|
await takeScreenshot(page, '08-hands-list');
|
|
|
|
// 检查是否有 Hand 卡片
|
|
const handCards = page.locator('[class*="card"]').filter({
|
|
hasText: /Clip|Lead|Collector|Predictor|Researcher|Twitter|Browser/i
|
|
});
|
|
|
|
const cardCount = await handCards.count();
|
|
console.log(`Found ${cardCount} hand cards`);
|
|
});
|
|
|
|
test('Hand 触发按钮', async ({ page }) => {
|
|
const triggerBtn = page.getByRole('button', { name: /trigger|触发|执行|run/i });
|
|
|
|
if (await triggerBtn.first().isVisible()) {
|
|
await takeScreenshot(page, '09-hand-trigger');
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('5. Workflow/Scheduler 面板', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
const workflowBtn = page.getByRole('button', { name: '工作流' });
|
|
|
|
if (await workflowBtn.isVisible()) {
|
|
await workflowBtn.click();
|
|
await page.waitForTimeout(500);
|
|
}
|
|
});
|
|
|
|
test('Scheduler 面板渲染', async ({ page }) => {
|
|
const schedulerPanel = page.locator('[class*="scheduler"]').or(
|
|
page.locator('[class*="workflow"]')
|
|
);
|
|
|
|
await takeScreenshot(page, '10-scheduler-panel');
|
|
|
|
// 检查定时任务列表
|
|
const taskList = page.locator('table, ul, [class*="list"]');
|
|
const hasTaskList = await taskList.count() > 0;
|
|
console.log(`Task list found: ${hasTaskList}`);
|
|
});
|
|
|
|
test('工作流编辑器', async ({ page }) => {
|
|
const workflowEditor = page.locator('[class*="editor"]').or(
|
|
page.locator('[class*="workflow-editor"]')
|
|
);
|
|
|
|
if (await workflowEditor.isVisible()) {
|
|
await takeScreenshot(page, '11-workflow-editor');
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('6. Team 协作视图', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
const teamBtn = page.getByRole('button', { name: '团队' });
|
|
|
|
if (await teamBtn.isVisible()) {
|
|
await teamBtn.click();
|
|
await page.waitForTimeout(500);
|
|
}
|
|
});
|
|
|
|
test('Team 列表和创建', async ({ page }) => {
|
|
const teamList = page.locator('[class*="team"]').or(
|
|
page.locator('[class*="group"]')
|
|
);
|
|
|
|
const createBtn = page.getByRole('button', { name: /create|创建|new|新建|\+/i });
|
|
|
|
await takeScreenshot(page, '12-team-view');
|
|
|
|
if (await createBtn.first().isVisible()) {
|
|
console.log('Team create button available');
|
|
}
|
|
});
|
|
|
|
test('团队成员显示', async ({ page }) => {
|
|
const members = page.locator('[class*="member"]').or(
|
|
page.locator('[class*="agent"]')
|
|
);
|
|
|
|
const memberCount = await members.count();
|
|
console.log(`Found ${memberCount} team members`);
|
|
});
|
|
});
|
|
|
|
test.describe('7. Swarm Dashboard', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
const swarmBtn = page.getByRole('button', { name: '协作' });
|
|
|
|
if (await swarmBtn.isVisible()) {
|
|
await swarmBtn.click();
|
|
await page.waitForTimeout(500);
|
|
}
|
|
});
|
|
|
|
test('Swarm 仪表板渲染', async ({ page }) => {
|
|
const dashboard = page.locator('[class*="swarm"]').or(
|
|
page.locator('[class*="dashboard"]')
|
|
);
|
|
|
|
await takeScreenshot(page, '13-swarm-dashboard');
|
|
|
|
// 检查状态指示器
|
|
const statusIndicators = page.locator('[class*="status"]');
|
|
const statusCount = await statusIndicators.count();
|
|
console.log(`Found ${statusCount} status indicators`);
|
|
});
|
|
});
|
|
|
|
test.describe('8. Settings 设置页面', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
const settingsBtn = page.getByRole('button', { name: /settings|设置|⚙/i });
|
|
|
|
if (await settingsBtn.isVisible()) {
|
|
await settingsBtn.click();
|
|
await page.waitForTimeout(500);
|
|
}
|
|
});
|
|
|
|
test('设置页面渲染', async ({ page }) => {
|
|
const settingsLayout = page.locator('[class*="settings"]').or(
|
|
page.locator('form')
|
|
);
|
|
|
|
await takeScreenshot(page, '14-settings-page');
|
|
|
|
// 检查设置分类
|
|
const settingsTabs = page.locator('[role="tab"]').or(
|
|
page.locator('button').filter({ hasText: /General|通用|Security|安全|Model|模型/i })
|
|
);
|
|
|
|
const tabCount = await settingsTabs.count();
|
|
console.log(`Found ${tabCount} settings tabs`);
|
|
});
|
|
|
|
test('通用设置', async ({ page }) => {
|
|
const generalSettings = page.locator('[class*="general"]').or(
|
|
page.getByText(/general|通用设置/i)
|
|
);
|
|
|
|
if (await generalSettings.isVisible()) {
|
|
await takeScreenshot(page, '15-general-settings');
|
|
}
|
|
});
|
|
|
|
test('模型配置', async ({ page }) => {
|
|
// 检查设置页面是否有模型相关内容
|
|
const modelSection = page.getByRole('button', { name: /模型|Model/i }).or(
|
|
page.locator('text=/模型|Model/i')
|
|
);
|
|
|
|
// 这个测试是可选的,因为模型配置可能在不同的标签页
|
|
const isVisible = await modelSection.first().isVisible().catch(() => false);
|
|
if (isVisible) {
|
|
await takeScreenshot(page, '16-model-settings');
|
|
} else {
|
|
// 如果没有找到模型配置,跳过测试
|
|
console.log('Model settings section not found - may be in a different tab');
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('9. RightPanel 右侧面板', () => {
|
|
test('右侧面板渲染', async ({ page }) => {
|
|
// 查找右侧面板
|
|
const rightPanel = page.locator('[class*="w-"][class*="border-l"]').or(
|
|
page.locator('aside').last()
|
|
);
|
|
|
|
if (await rightPanel.isVisible()) {
|
|
await takeScreenshot(page, '17-right-panel');
|
|
|
|
// 检查面板内容
|
|
const panelContent = rightPanel.locator('[class*="info"], [class*="detail"], [class*="context"]');
|
|
console.log(`Right panel content found: ${await panelContent.count() > 0}`);
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('10. 错误处理和边界情况', () => {
|
|
test('网络错误处理', async ({ page }) => {
|
|
// 模拟离线
|
|
await page.context().setOffline(true);
|
|
await page.waitForTimeout(1000);
|
|
|
|
// 检查错误提示
|
|
const errorMessage = page.locator('[class*="error"]').or(
|
|
page.locator('[role="alert"]')
|
|
);
|
|
|
|
await takeScreenshot(page, '18-offline-error');
|
|
|
|
// 恢复网络
|
|
await page.context().setOffline(false);
|
|
});
|
|
|
|
test('空状态显示', async ({ page }) => {
|
|
// 检查空状态组件
|
|
const emptyState = page.locator('[class*="empty"]').or(
|
|
page.locator('[class*="no-data"]')
|
|
);
|
|
|
|
if (await emptyState.isVisible()) {
|
|
await takeScreenshot(page, '19-empty-state');
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('11. 响应式布局', () => {
|
|
test('移动端布局', async ({ page }) => {
|
|
await page.setViewportSize({ width: 375, height: 667 });
|
|
await page.waitForTimeout(500);
|
|
|
|
await takeScreenshot(page, '20-mobile-layout');
|
|
|
|
// 检查移动端导航
|
|
const mobileMenu = page.locator('[class*="mobile"]').or(
|
|
page.locator('button[aria-label*="menu"]')
|
|
);
|
|
|
|
console.log(`Mobile menu found: ${await mobileMenu.count() > 0}`);
|
|
});
|
|
|
|
test('平板布局', async ({ page }) => {
|
|
await page.setViewportSize({ width: 768, height: 1024 });
|
|
await page.waitForTimeout(500);
|
|
|
|
await takeScreenshot(page, '21-tablet-layout');
|
|
});
|
|
|
|
test('桌面布局', async ({ page }) => {
|
|
await page.setViewportSize({ width: 1920, height: 1080 });
|
|
await page.waitForTimeout(500);
|
|
|
|
await takeScreenshot(page, '22-desktop-layout');
|
|
});
|
|
});
|
|
|
|
test.describe('12. 性能检查', () => {
|
|
test('页面加载性能', async ({ page }) => {
|
|
const startTime = Date.now();
|
|
await page.goto('/');
|
|
await waitForAppReady(page);
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
console.log(`Page load time: ${loadTime}ms`);
|
|
|
|
// 页面加载时间应该小于 5 秒
|
|
expect(loadTime).toBeLessThan(5000);
|
|
});
|
|
|
|
test('内存使用检查', async ({ page }) => {
|
|
// 获取页面指标
|
|
const metrics = await page.evaluate(() => {
|
|
return {
|
|
memory: (performance as any).memory?.usedJSHeapSize || 0,
|
|
domNodes: document.querySelectorAll('*').length,
|
|
};
|
|
});
|
|
|
|
console.log(`DOM nodes: ${metrics.domNodes}`);
|
|
console.log(`Memory used: ${Math.round(metrics.memory / 1024 / 1024)}MB`);
|
|
});
|
|
});
|
|
});
|
|
|
|
test.describe('13. 控制台错误检查', () => {
|
|
test('无 JavaScript 错误', async ({ page }) => {
|
|
const errors: string[] = [];
|
|
|
|
page.on('pageerror', error => {
|
|
errors.push(error.message);
|
|
});
|
|
|
|
await page.goto('/');
|
|
await waitForAppReady(page);
|
|
|
|
// 执行一些交互
|
|
await page.click('body');
|
|
await page.waitForTimeout(1000);
|
|
|
|
// 检查是否有严重错误
|
|
const criticalErrors = errors.filter(e =>
|
|
!e.includes('Warning:') &&
|
|
!e.includes('DevTools') &&
|
|
!e.includes('extension')
|
|
);
|
|
|
|
console.log(`Console errors: ${criticalErrors.length}`);
|
|
criticalErrors.forEach(e => console.log(` - ${e}`));
|
|
|
|
// 允许少量非严重错误
|
|
expect(criticalErrors.length).toBeLessThan(5);
|
|
});
|
|
|
|
test('无网络请求失败', async ({ page }) => {
|
|
const failedRequests: string[] = [];
|
|
|
|
page.on('requestfailed', request => {
|
|
failedRequests.push(request.url());
|
|
});
|
|
|
|
await page.goto('/');
|
|
await waitForAppReady(page);
|
|
await page.waitForTimeout(2000);
|
|
|
|
console.log(`Failed requests: ${failedRequests.length}`);
|
|
failedRequests.forEach(r => console.log(` - ${r}`));
|
|
});
|
|
});
|