- 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
1115 lines
33 KiB
TypeScript
1115 lines
33 KiB
TypeScript
/**
|
|
* ZCLAW 前端功能深度验证测试
|
|
*
|
|
* 模拟真实用户使用场景,测试功能完整性和可用性
|
|
*/
|
|
|
|
import { test, expect, Page, BrowserContext } from '@playwright/test';
|
|
|
|
// 测试超时配置
|
|
test.setTimeout(120000);
|
|
|
|
// 测试配置
|
|
const BASE_URL = 'http://localhost:1420';
|
|
const SCREENSHOT_DIR = 'test-results/screenshots';
|
|
|
|
// 辅助函数
|
|
async function waitForAppReady(page: Page) {
|
|
await page.waitForLoadState('networkidle');
|
|
await page.waitForSelector('aside', { timeout: 15000 });
|
|
await page.waitForTimeout(1000); // 等待状态初始化
|
|
}
|
|
|
|
async function takeScreenshot(page: Page, name: string) {
|
|
try {
|
|
await page.screenshot({
|
|
path: `${SCREENSHOT_DIR}/${name}.png`,
|
|
fullPage: true
|
|
});
|
|
} catch (e) {
|
|
console.log(`Screenshot failed: ${name}`);
|
|
}
|
|
}
|
|
|
|
async function navigateToTab(page: Page, tabName: string) {
|
|
// Map Chinese names to aria-labels
|
|
const tabLabels: Record<string, string> = {
|
|
'分身': '分身',
|
|
'Hands': 'Hands',
|
|
'工作流': '工作流',
|
|
'团队': '团队',
|
|
'协作': '协作',
|
|
};
|
|
const label = tabLabels[tabName] || tabName;
|
|
const tabButton = page.getByRole('button', { name: label });
|
|
if (await tabButton.isVisible()) {
|
|
await tabButton.click();
|
|
await page.waitForTimeout(500);
|
|
}
|
|
}
|
|
|
|
// 获取控制台日志
|
|
function captureConsoleLogs(page: Page) {
|
|
const logs: { type: string; message: string }[] = [];
|
|
page.on('console', msg => {
|
|
logs.push({ type: msg.type(), message: msg.text() });
|
|
});
|
|
page.on('pageerror', error => {
|
|
logs.push({ type: 'error', message: error.message });
|
|
});
|
|
return logs;
|
|
}
|
|
|
|
// ============================================
|
|
// 测试套件 1: 应用启动与初始化
|
|
// ============================================
|
|
test.describe('1. 应用启动与初始化', () => {
|
|
|
|
test('1.1 应用正常启动并渲染所有核心组件', async ({ page }) => {
|
|
const logs = captureConsoleLogs(page);
|
|
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
|
|
// 验证核心布局组件
|
|
const sidebar = page.locator('aside');
|
|
const main = page.locator('main');
|
|
|
|
await expect(sidebar).toBeVisible();
|
|
await expect(main).toBeVisible();
|
|
|
|
// 验证侧边栏标签
|
|
const tabs = ['分身', 'Hands', '工作流', '团队', '协作'];
|
|
for (const tab of tabs) {
|
|
const tabBtn = page.getByRole('button', { name: new RegExp(tab, 'i') });
|
|
await expect(tabBtn).toBeVisible();
|
|
}
|
|
|
|
await takeScreenshot(page, '01-app-initialized');
|
|
|
|
// 检查关键错误
|
|
const criticalErrors = logs.filter(l =>
|
|
l.type === 'error' &&
|
|
!l.message.includes('DevTools') &&
|
|
!l.message.includes('extension') &&
|
|
!l.message.includes('Warning:')
|
|
);
|
|
console.log(`Critical errors during startup: ${criticalErrors.length}`);
|
|
expect(criticalErrors.length).toBeLessThan(3);
|
|
});
|
|
|
|
test('1.2 Zustand 状态持久化正常加载', async ({ page }) => {
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
|
|
// 检查 localStorage 中的持久化状态
|
|
const chatStorage = await page.evaluate(() => {
|
|
return localStorage.getItem('zclaw-chat-storage');
|
|
});
|
|
|
|
const gatewayStorage = await page.evaluate(() => {
|
|
return localStorage.getItem('zclaw-gateway-storage');
|
|
});
|
|
|
|
console.log('Chat storage exists:', !!chatStorage);
|
|
console.log('Gateway storage exists:', !!gatewayStorage);
|
|
|
|
// 验证状态可以被解析
|
|
if (chatStorage) {
|
|
const parsed = JSON.parse(chatStorage);
|
|
expect(parsed).toHaveProperty('state');
|
|
console.log('Chat state keys:', Object.keys(parsed.state || {}));
|
|
}
|
|
});
|
|
|
|
test('1.3 Gateway 连接状态检查', async ({ page }) => {
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
|
|
// 等待连接尝试
|
|
await page.waitForTimeout(3000);
|
|
|
|
// 检查连接状态指示器(如果有)
|
|
const connectionStatus = page.locator('[class*="connection"]').or(
|
|
page.locator('[class*="status"]').filter({ hasText: /connected|disconnected|connecting/i })
|
|
);
|
|
|
|
// 获取控制台日志检查连接状态
|
|
const connectionLogs: string[] = [];
|
|
page.on('console', msg => {
|
|
if (msg.text().toLowerCase().includes('connect') ||
|
|
msg.text().toLowerCase().includes('gateway')) {
|
|
connectionLogs.push(msg.text());
|
|
}
|
|
});
|
|
|
|
await page.waitForTimeout(2000);
|
|
console.log('Connection logs:', connectionLogs.slice(-5));
|
|
|
|
await takeScreenshot(page, '02-connection-state');
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// 测试套件 2: 聊天功能 (核心场景)
|
|
// ============================================
|
|
test.describe('2. 聊天功能', () => {
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
});
|
|
|
|
test('2.1 聊天输入框功能', async ({ page }) => {
|
|
// 查找聊天输入框
|
|
const chatInput = page.locator('textarea').or(
|
|
page.locator('[contenteditable="true"]')
|
|
).first();
|
|
|
|
if (await chatInput.isVisible()) {
|
|
// 测试输入
|
|
await chatInput.click();
|
|
await chatInput.fill('这是一条测试消息');
|
|
|
|
// 验证输入内容
|
|
const value = await chatInput.inputValue();
|
|
expect(value).toContain('测试消息');
|
|
|
|
await takeScreenshot(page, '03-chat-input');
|
|
} else {
|
|
console.log('Chat input not found - may need to navigate to chat view');
|
|
}
|
|
});
|
|
|
|
test('2.2 发送消息并检查响应', async ({ page }) => {
|
|
const chatInput = page.locator('textarea').first();
|
|
|
|
if (await chatInput.isVisible()) {
|
|
await chatInput.fill('你好,请介绍一下你自己');
|
|
|
|
// 发送消息 - 使用更精确的选择器
|
|
const sendBtn = page.getByRole('button', { name: '发送消息' });
|
|
|
|
await sendBtn.click();
|
|
|
|
// 等待响应
|
|
await page.waitForTimeout(5000);
|
|
|
|
// 检查用户消息是否显示
|
|
const userMessage = page.locator('[class*="message"]').filter({ hasText: '你好' });
|
|
const hasUserMessage = await userMessage.count() > 0;
|
|
|
|
// 检查是否有响应(可能是错误或实际响应)
|
|
const messages = page.locator('[class*="message"], [class*="assistant"]');
|
|
const messageCount = await messages.count();
|
|
|
|
console.log(`Messages found: ${messageCount}`);
|
|
console.log(`User message visible: ${hasUserMessage}`);
|
|
|
|
await takeScreenshot(page, '04-chat-response');
|
|
}
|
|
});
|
|
|
|
test('2.3 会话切换功能', async ({ page }) => {
|
|
// 导航到分身标签查看会话列表
|
|
await navigateToTab(page, '分身');
|
|
|
|
const conversationItems = page.locator('[class*="conversation"]').or(
|
|
page.locator('li').filter({ has: page.locator('[class*="message"]') })
|
|
);
|
|
|
|
const count = await conversationItems.count();
|
|
console.log(`Conversation items found: ${count}`);
|
|
|
|
if (count > 1) {
|
|
// 点击第二个会话
|
|
await conversationItems.nth(1).click();
|
|
await page.waitForTimeout(500);
|
|
|
|
await takeScreenshot(page, '05-conversation-switch');
|
|
}
|
|
});
|
|
|
|
test('2.4 新建会话功能', async ({ page }) => {
|
|
// 查找新建会话按钮
|
|
const newChatBtn = page.getByRole('button', { name: /新|new|create|\+/i }).first();
|
|
|
|
if (await newChatBtn.isVisible()) {
|
|
await newChatBtn.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// 验证消息列表已清空
|
|
const messages = page.locator('[class*="message"]');
|
|
const count = await messages.count();
|
|
|
|
console.log(`Messages after new chat: ${count}`);
|
|
await takeScreenshot(page, '06-new-conversation');
|
|
}
|
|
});
|
|
|
|
test('2.5 消息流式显示', async ({ page }) => {
|
|
const chatInput = page.locator('textarea').first();
|
|
|
|
if (await chatInput.isVisible()) {
|
|
await chatInput.fill('请写一首短诗');
|
|
await page.getByRole('button', { name: '发送消息' }).click();
|
|
|
|
// 检查是否有 streaming 状态
|
|
await page.waitForTimeout(1000);
|
|
|
|
const streamingIndicator = page.locator('[class*="streaming"]').or(
|
|
page.locator('[class*="loading"]')
|
|
);
|
|
|
|
const isStreaming = await streamingIndicator.count() > 0;
|
|
console.log(`Streaming indicator visible: ${isStreaming}`);
|
|
|
|
// 等待完成
|
|
await page.waitForTimeout(5000);
|
|
|
|
await takeScreenshot(page, '07-streaming-response');
|
|
}
|
|
});
|
|
|
|
test('2.6 错误处理 - 网络断开', async ({ page, context }) => {
|
|
const chatInput = page.locator('textarea').first();
|
|
|
|
if (await chatInput.isVisible()) {
|
|
// 模拟离线
|
|
await context.setOffline(true);
|
|
|
|
await chatInput.fill('离线测试消息');
|
|
await page.getByRole('button', { name: '发送消息' }).click();
|
|
|
|
await page.waitForTimeout(3000);
|
|
|
|
// 检查错误提示
|
|
const errorMsg = page.locator('[class*="error"]').or(
|
|
page.locator('[role="alert"]')
|
|
).or(
|
|
page.locator('text=/无法连接|网络|错误|failed|error/i')
|
|
);
|
|
|
|
const hasError = await errorMsg.count() > 0;
|
|
console.log(`Error message shown: ${hasError}`);
|
|
|
|
await takeScreenshot(page, '08-offline-error');
|
|
|
|
// 恢复网络
|
|
await context.setOffline(false);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// 测试套件 3: Agent/分身管理
|
|
// ============================================
|
|
test.describe('3. Agent/分身管理', () => {
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
await navigateToTab(page, '分身');
|
|
});
|
|
|
|
test('3.1 分身列表显示', async ({ page }) => {
|
|
await page.waitForTimeout(1000);
|
|
|
|
// 检查分身列表
|
|
const cloneItems = page.locator('[class*="clone"]').or(
|
|
page.locator('[class*="agent"]')
|
|
).or(
|
|
page.locator('li').filter({ hasText: /分身|agent|ZCLAW/i })
|
|
);
|
|
|
|
const count = await cloneItems.count();
|
|
console.log(`Clone/Agent items found: ${count}`);
|
|
|
|
// 至少应该有默认 Agent
|
|
expect(count).toBeGreaterThanOrEqual(1);
|
|
|
|
await takeScreenshot(page, '09-clone-list');
|
|
});
|
|
|
|
test('3.2 创建新分身', async ({ page }) => {
|
|
// 查找创建按钮
|
|
const createBtn = page.getByRole('button', { name: /创建|new|添加|\+/i }).first();
|
|
|
|
if (await createBtn.isVisible()) {
|
|
await createBtn.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// 检查创建表单/对话框
|
|
const createForm = page.locator('[role="dialog"]').or(
|
|
page.locator('[class*="modal"]')
|
|
).or(
|
|
page.locator('form')
|
|
);
|
|
|
|
if (await createForm.isVisible()) {
|
|
// 填写分身信息
|
|
const nameInput = createForm.locator('input').first();
|
|
if (await nameInput.isVisible()) {
|
|
await nameInput.fill('测试分身');
|
|
}
|
|
|
|
await takeScreenshot(page, '10-create-clone-form');
|
|
}
|
|
}
|
|
});
|
|
|
|
test('3.3 切换分身', async ({ page }) => {
|
|
const cloneItems = page.locator('[class*="clone"]').or(
|
|
page.locator('li').filter({ hasText: /分身|agent/i })
|
|
);
|
|
|
|
const count = await cloneItems.count();
|
|
if (count > 1) {
|
|
await cloneItems.nth(1).click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// 验证切换后的状态
|
|
const activeIndicator = page.locator('[class*="active"]').or(
|
|
page.locator('[class*="selected"]')
|
|
);
|
|
|
|
console.log(`Active indicator visible: ${await activeIndicator.count() > 0}`);
|
|
|
|
await takeScreenshot(page, '11-clone-switch');
|
|
}
|
|
});
|
|
|
|
test('3.4 分身设置修改', async ({ page }) => {
|
|
const cloneItems = page.locator('[class*="clone"]').or(
|
|
page.locator('li').filter({ hasText: /分身|agent/i })
|
|
);
|
|
|
|
if (await cloneItems.first().isVisible()) {
|
|
// 查找设置/编辑按钮
|
|
const settingsBtn = cloneItems.first().locator('button').filter({
|
|
has: page.locator('svg')
|
|
});
|
|
|
|
if (await settingsBtn.isVisible()) {
|
|
await settingsBtn.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
await takeScreenshot(page, '12-clone-settings');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// 测试套件 4: Hands 系统
|
|
// ============================================
|
|
test.describe('4. Hands 系统', () => {
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
await navigateToTab(page, 'Hands');
|
|
await page.waitForTimeout(1000);
|
|
});
|
|
|
|
test('4.1 Hands 列表显示', async ({ page }) => {
|
|
// 检查 Hand 卡片
|
|
const handCards = page.locator('[class*="hand"]').or(
|
|
page.locator('[class*="card"]')
|
|
).filter({
|
|
hasText: /Clip|Lead|Collector|Predictor|Researcher|Twitter|Browser|能力|自主/i
|
|
});
|
|
|
|
const count = await handCards.count();
|
|
console.log(`Hand cards found: ${count}`);
|
|
|
|
// OpenFang 应该有 7 个 Hands
|
|
expect(count).toBeGreaterThanOrEqual(1);
|
|
|
|
await takeScreenshot(page, '13-hands-list');
|
|
});
|
|
|
|
test('4.2 Hand 触发功能', async ({ page }) => {
|
|
// 查找触发按钮
|
|
const triggerBtn = page.getByRole('button', { name: /触发|trigger|执行|run|start/i }).first();
|
|
|
|
if (await triggerBtn.isVisible()) {
|
|
await triggerBtn.click();
|
|
await page.waitForTimeout(1000);
|
|
|
|
// 检查触发后的状态变化
|
|
const statusIndicator = page.locator('[class*="status"]').or(
|
|
page.locator('[class*="running"]').or(
|
|
page.locator('[class*="progress"]')
|
|
)
|
|
);
|
|
|
|
console.log(`Status indicator after trigger: ${await statusIndicator.count() > 0}`);
|
|
|
|
await takeScreenshot(page, '14-hand-triggered');
|
|
}
|
|
});
|
|
|
|
test('4.3 Hand 审批流程', async ({ page }) => {
|
|
// 查找需要审批的 Hand
|
|
const approvalBtn = page.getByRole('button', { name: /审批|approve|确认/i });
|
|
|
|
if (await approvalBtn.isVisible()) {
|
|
await approvalBtn.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// 检查审批对话框
|
|
const approvalDialog = page.locator('[role="dialog"]').or(
|
|
page.locator('[class*="modal"]')
|
|
);
|
|
|
|
if (await approvalDialog.isVisible()) {
|
|
// 批准/拒绝按钮
|
|
const confirmBtn = page.getByRole('button', { name: /批准|confirm|yes/i });
|
|
const rejectBtn = page.getByRole('button', { name: /拒绝|reject|no/i });
|
|
|
|
console.log(`Approval dialog buttons visible: ${await confirmBtn.isVisible() && await rejectBtn.isVisible()}`);
|
|
|
|
await takeScreenshot(page, '15-hand-approval');
|
|
}
|
|
}
|
|
});
|
|
|
|
test('4.4 Hand 任务历史', async ({ page }) => {
|
|
// 查找历史/日志按钮
|
|
const historyBtn = page.getByRole('button', { name: /历史|history|日志|log/i });
|
|
|
|
if (await historyBtn.isVisible()) {
|
|
await historyBtn.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
await takeScreenshot(page, '16-hand-history');
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// 测试套件 5: 工作流管理
|
|
// ============================================
|
|
test.describe('5. 工作流管理', () => {
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
await navigateToTab(page, '工作流');
|
|
await page.waitForTimeout(1000);
|
|
});
|
|
|
|
test('5.1 工作流列表显示', async ({ page }) => {
|
|
const workflowItems = page.locator('[class*="workflow"]').or(
|
|
page.locator('[class*="scheduler"]')
|
|
);
|
|
|
|
console.log(`Workflow items found: ${await workflowItems.count()}`);
|
|
|
|
await takeScreenshot(page, '17-workflow-list');
|
|
});
|
|
|
|
test('5.2 创建工作流', async ({ page }) => {
|
|
const createBtn = page.getByRole('button', { name: /创建|new|添加|\+/i }).first();
|
|
|
|
if (await createBtn.isVisible()) {
|
|
await createBtn.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// 检查工作流编辑器
|
|
const editor = page.locator('[class*="editor"]').or(
|
|
page.locator('form')
|
|
);
|
|
|
|
console.log(`Workflow editor visible: ${await editor.isVisible()}`);
|
|
|
|
await takeScreenshot(page, '18-workflow-create');
|
|
}
|
|
});
|
|
|
|
test('5.3 工作流执行状态', async ({ page }) => {
|
|
// 查找运行中的工作流
|
|
const runningWorkflow = page.locator('[class*="running"]').or(
|
|
page.locator('[class*="active"]').or(
|
|
page.locator('[class*="executing"]')
|
|
)
|
|
);
|
|
|
|
if (await runningWorkflow.isVisible()) {
|
|
await takeScreenshot(page, '19-workflow-running');
|
|
}
|
|
});
|
|
|
|
test('5.4 定时任务配置', async ({ page }) => {
|
|
const scheduleBtn = page.getByRole('button', { name: /定时|schedule|cron/i });
|
|
|
|
if (await scheduleBtn.isVisible()) {
|
|
await scheduleBtn.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
await takeScreenshot(page, '20-scheduler-config');
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// 测试套件 6: 团队协作
|
|
// ============================================
|
|
test.describe('6. 团队协作', () => {
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
await navigateToTab(page, '团队');
|
|
await page.waitForTimeout(1000);
|
|
});
|
|
|
|
test('6.1 团队列表显示', async ({ page }) => {
|
|
const teamItems = page.locator('[class*="team"]').or(
|
|
page.locator('li').filter({ hasText: /团队|team/i })
|
|
);
|
|
|
|
const count = await teamItems.count();
|
|
console.log(`Team items found: ${count}`);
|
|
|
|
await takeScreenshot(page, '21-team-list');
|
|
});
|
|
|
|
test('6.2 创建团队', async ({ page }) => {
|
|
const createBtn = page.getByRole('button', { name: /创建|new|\+/i }).first();
|
|
|
|
if (await createBtn.isVisible()) {
|
|
await createBtn.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// 填写团队信息
|
|
const nameInput = page.locator('input').first();
|
|
if (await nameInput.isVisible()) {
|
|
await nameInput.fill('测试团队');
|
|
}
|
|
|
|
await takeScreenshot(page, '22-team-create');
|
|
}
|
|
});
|
|
|
|
test('6.3 团队成员管理', async ({ page }) => {
|
|
const teamItems = page.locator('[class*="team"]');
|
|
|
|
if (await teamItems.first().isVisible()) {
|
|
await teamItems.first().click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// 检查成员列表
|
|
const members = page.locator('[class*="member"]').or(
|
|
page.locator('[class*="agent"]')
|
|
);
|
|
|
|
console.log(`Team members found: ${await members.count()}`);
|
|
|
|
await takeScreenshot(page, '23-team-members');
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// 测试套件 7: Swarm 协作
|
|
// ============================================
|
|
test.describe('7. Swarm 协作', () => {
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
await navigateToTab(page, '协作');
|
|
await page.waitForTimeout(1000);
|
|
});
|
|
|
|
test('7.1 Swarm 仪表板显示', async ({ page }) => {
|
|
const dashboard = page.locator('[class*="swarm"]').or(
|
|
page.locator('[class*="dashboard"]')
|
|
);
|
|
|
|
console.log(`Swarm dashboard visible: ${await dashboard.isVisible()}`);
|
|
|
|
await takeScreenshot(page, '24-swarm-dashboard');
|
|
});
|
|
|
|
test('7.2 创建协作任务', async ({ page }) => {
|
|
const createBtn = page.getByRole('button', { name: /创建|new|任务|task|\+/i }).first();
|
|
|
|
if (await createBtn.isVisible()) {
|
|
await createBtn.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// 填写任务描述
|
|
const descInput = page.locator('textarea').or(
|
|
page.locator('input[type="text"]')
|
|
).first();
|
|
|
|
if (await descInput.isVisible()) {
|
|
await descInput.fill('这是一个测试协作任务');
|
|
}
|
|
|
|
await takeScreenshot(page, '25-swarm-task-create');
|
|
}
|
|
});
|
|
|
|
test('7.3 协作模式选择', async ({ page }) => {
|
|
const modeSelector = page.locator('[class*="mode"]').or(
|
|
page.locator('select').or(
|
|
page.locator('[role="listbox"]')
|
|
)
|
|
);
|
|
|
|
if (await modeSelector.isVisible()) {
|
|
// 检查是否有并行/串行/辩论模式
|
|
const parallelOption = page.getByText(/parallel|并行/i);
|
|
const sequentialOption = page.getByText(/sequential|串行/i);
|
|
const debateOption = page.getByText(/debate|辩论/i);
|
|
|
|
console.log(`Mode options available:
|
|
parallel: ${await parallelOption.isVisible()},
|
|
sequential: ${await sequentialOption.isVisible()},
|
|
debate: ${await debateOption.isVisible()}`);
|
|
|
|
await takeScreenshot(page, '26-swarm-modes');
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// 测试套件 8: 设置页面
|
|
// ============================================
|
|
test.describe('8. 设置页面', () => {
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
});
|
|
|
|
test('8.1 打开设置页面', async ({ page }) => {
|
|
const settingsBtn = page.getByRole('button', { name: /设置|settings|⚙/i });
|
|
|
|
if (await settingsBtn.isVisible()) {
|
|
await settingsBtn.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// 验证设置页面
|
|
const settingsLayout = page.locator('[class*="settings"]').or(
|
|
page.locator('form').or(
|
|
page.locator('[role="tabpanel"]')
|
|
)
|
|
);
|
|
|
|
console.log(`Settings layout visible: ${await settingsLayout.isVisible()}`);
|
|
|
|
await takeScreenshot(page, '27-settings-page');
|
|
}
|
|
});
|
|
|
|
test('8.2 通用设置', async ({ page }) => {
|
|
const settingsBtn = page.getByRole('button', { name: /设置|settings|⚙/i });
|
|
|
|
if (await settingsBtn.isVisible()) {
|
|
await settingsBtn.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// 检查用户名设置
|
|
const usernameInput = page.locator('input').filter({ hasText: '' }).first();
|
|
|
|
// 检查主题设置
|
|
const themeSelector = page.locator('[class*="theme"]').or(
|
|
page.locator('select').first()
|
|
);
|
|
|
|
console.log(`Username input visible: ${await usernameInput.isVisible()}`);
|
|
console.log(`Theme selector visible: ${await themeSelector.isVisible()}`);
|
|
|
|
await takeScreenshot(page, '28-general-settings');
|
|
}
|
|
});
|
|
|
|
test('8.3 模型配置', async ({ page }) => {
|
|
const settingsBtn = page.getByRole('button', { name: /设置|settings|⚙/i });
|
|
|
|
if (await settingsBtn.isVisible()) {
|
|
await settingsBtn.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// 查找模型选择器
|
|
const modelSelector = page.locator('[class*="model"]').or(
|
|
page.getByText(/模型|model/i)
|
|
);
|
|
|
|
if (await modelSelector.isVisible()) {
|
|
await modelSelector.click();
|
|
await page.waitForTimeout(300);
|
|
|
|
// 检查可用模型列表
|
|
const modelOptions = page.locator('[role="option"]').or(
|
|
page.locator('li')
|
|
);
|
|
|
|
console.log(`Model options found: ${await modelOptions.count()}`);
|
|
|
|
await takeScreenshot(page, '29-model-settings');
|
|
}
|
|
}
|
|
});
|
|
|
|
test('8.4 Gateway 配置', async ({ page }) => {
|
|
const settingsBtn = page.getByRole('button', { name: /设置|settings|⚙/i });
|
|
|
|
if (await settingsBtn.isVisible()) {
|
|
await settingsBtn.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// 查找 Gateway 配置
|
|
const gatewaySection = page.locator('[class*="gateway"]').or(
|
|
page.getByText(/gateway|服务器|server/i)
|
|
);
|
|
|
|
console.log(`Gateway section visible: ${await gatewaySection.isVisible()}`);
|
|
|
|
await takeScreenshot(page, '30-gateway-settings');
|
|
}
|
|
});
|
|
|
|
test('8.5 保存设置', async ({ page }) => {
|
|
const settingsBtn = page.getByRole('button', { name: /设置|settings|⚙/i });
|
|
|
|
if (await settingsBtn.isVisible()) {
|
|
await settingsBtn.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// 查找保存按钮
|
|
const saveBtn = page.getByRole('button', { name: /保存|save|apply/i });
|
|
|
|
if (await saveBtn.isVisible()) {
|
|
await saveBtn.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// 检查成功提示
|
|
const successMsg = page.locator('[class*="success"]').or(
|
|
page.locator('[class*="toast"]').or(
|
|
page.locator('[role="status"]')
|
|
)
|
|
);
|
|
|
|
console.log(`Success message shown: ${await successMsg.isVisible()}`);
|
|
|
|
await takeScreenshot(page, '31-settings-saved');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// 测试套件 9: 右侧面板
|
|
// ============================================
|
|
test.describe('9. 右侧面板', () => {
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
});
|
|
|
|
test('9.1 右侧面板显示', async ({ page }) => {
|
|
const rightPanel = page.locator('[class*="w-"][class*="border-l"]').or(
|
|
page.locator('aside').last()
|
|
);
|
|
|
|
if (await rightPanel.isVisible()) {
|
|
console.log(`Right panel visible`);
|
|
|
|
await takeScreenshot(page, '32-right-panel');
|
|
}
|
|
});
|
|
|
|
test('9.2 上下文信息显示', async ({ page }) => {
|
|
const contextInfo = page.locator('[class*="context"]').or(
|
|
page.locator('[class*="info"]')
|
|
);
|
|
|
|
if (await contextInfo.isVisible()) {
|
|
console.log(`Context info visible`);
|
|
|
|
await takeScreenshot(page, '33-context-info');
|
|
}
|
|
});
|
|
|
|
test('9.3 记忆面板', async ({ page }) => {
|
|
const memoryPanel = page.locator('[class*="memory"]').or(
|
|
page.getByText(/记忆|memory/i)
|
|
);
|
|
|
|
if (await memoryPanel.isVisible()) {
|
|
console.log(`Memory panel visible`);
|
|
|
|
await takeScreenshot(page, '34-memory-panel');
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// 测试套件 10: 完整用户流程
|
|
// ============================================
|
|
test.describe('10. 完整用户流程', () => {
|
|
|
|
test('10.1 新用户首次使用流程', async ({ page }) => {
|
|
// 清除所有存储
|
|
await page.goto(BASE_URL);
|
|
await page.evaluate(() => {
|
|
localStorage.clear();
|
|
sessionStorage.clear();
|
|
});
|
|
|
|
await page.reload();
|
|
await waitForAppReady(page);
|
|
|
|
// 检查是否有引导/欢迎界面
|
|
const onboarding = page.locator('[class*="onboarding"]').or(
|
|
page.locator('[class*="welcome"]').or(
|
|
page.locator('[role="dialog"]').filter({ hasText: /欢迎|welcome|开始/i })
|
|
)
|
|
);
|
|
|
|
console.log(`Onboarding visible: ${await onboarding.isVisible()}`);
|
|
|
|
await takeScreenshot(page, '35-first-time-user');
|
|
});
|
|
|
|
test('10.2 完整聊天流程', async ({ page }) => {
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
|
|
const chatInput = page.locator('textarea').first();
|
|
|
|
if (await chatInput.isVisible()) {
|
|
// 发送多条消息
|
|
const messages = [
|
|
'你好',
|
|
'请帮我写一个简单的函数',
|
|
'谢谢'
|
|
];
|
|
|
|
for (const msg of messages) {
|
|
await chatInput.fill(msg);
|
|
await page.getByRole('button', { name: '发送消息' }).click();
|
|
await page.waitForTimeout(2000);
|
|
}
|
|
|
|
// 检查消息数量
|
|
const messageElements = page.locator('[class*="message"]');
|
|
const count = await messageElements.count();
|
|
console.log(`Total messages: ${count}`);
|
|
|
|
await takeScreenshot(page, '36-full-chat-flow');
|
|
}
|
|
});
|
|
|
|
test('10.3 跨视图切换流程', async ({ page }) => {
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
|
|
const tabs = ['分身', 'Hands', '工作流', '团队', '协作'];
|
|
|
|
for (const tab of tabs) {
|
|
await navigateToTab(page, tab);
|
|
await page.waitForTimeout(1000);
|
|
|
|
// 验证视图已切换
|
|
console.log(`Switched to: ${tab}`);
|
|
}
|
|
|
|
await takeScreenshot(page, '37-view-switching');
|
|
});
|
|
|
|
test('10.4 会话持久化测试', async ({ page }) => {
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
|
|
// 发送一条消息
|
|
const chatInput = page.locator('textarea').first();
|
|
|
|
if (await chatInput.isVisible()) {
|
|
await chatInput.fill('持久化测试消息');
|
|
await page.getByRole('button', { name: '发送消息' }).click();
|
|
await page.waitForTimeout(2000);
|
|
|
|
// 刷新页面
|
|
await page.reload();
|
|
await waitForAppReady(page);
|
|
|
|
// 检查消息是否恢复
|
|
const messages = page.locator('[class*="message"]');
|
|
const count = await messages.count();
|
|
console.log(`Messages after reload: ${count}`);
|
|
|
|
await takeScreenshot(page, '38-session-persistence');
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// 测试套件 11: 性能与稳定性
|
|
// ============================================
|
|
test.describe('11. 性能与稳定性', () => {
|
|
|
|
test('11.1 页面加载性能', async ({ page }) => {
|
|
const startTime = Date.now();
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
console.log(`Page load time: ${loadTime}ms`);
|
|
expect(loadTime).toBeLessThan(10000); // 应该在 10 秒内加载完成
|
|
});
|
|
|
|
test('11.2 内存使用', async ({ page }) => {
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
|
|
const metrics = await page.evaluate(() => {
|
|
return {
|
|
domNodes: document.querySelectorAll('*').length,
|
|
jsHeapSize: (performance as any).memory?.usedJSHeapSize || 0,
|
|
};
|
|
});
|
|
|
|
console.log(`DOM nodes: ${metrics.domNodes}`);
|
|
console.log(`JS heap: ${Math.round(metrics.jsHeapSize / 1024 / 1024)}MB`);
|
|
|
|
// DOM 节点不应该过多
|
|
expect(metrics.domNodes).toBeLessThan(5000);
|
|
});
|
|
|
|
test('11.3 快速操作稳定性', async ({ page }) => {
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
|
|
// 快速切换标签
|
|
for (let i = 0; i < 10; i++) {
|
|
const tabs = ['分身', 'Hands', '工作流', '团队', '协作'];
|
|
const tab = tabs[i % tabs.length];
|
|
await navigateToTab(page, tab);
|
|
await page.waitForTimeout(100);
|
|
}
|
|
|
|
// 检查是否有错误
|
|
const errorElements = page.locator('[class*="error"]');
|
|
const errorCount = await errorElements.count();
|
|
console.log(`Errors after rapid switching: ${errorCount}`);
|
|
|
|
await takeScreenshot(page, '39-rapid-switching');
|
|
});
|
|
|
|
test('11.4 长时间运行稳定性', async ({ page }) => {
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
|
|
// 模拟 5 分钟的使用
|
|
const chatInput = page.locator('textarea').first();
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
if (await chatInput.isVisible()) {
|
|
await chatInput.fill(`测试消息 ${i + 1}`);
|
|
await page.getByRole('button', { name: '发送消息' }).click();
|
|
}
|
|
|
|
await navigateToTab(page, ['分身', 'Hands', '工作流', '团队', '协作'][i % 5]);
|
|
await page.waitForTimeout(1000);
|
|
}
|
|
|
|
// 检查内存和状态
|
|
const metrics = await page.evaluate(() => {
|
|
return {
|
|
domNodes: document.querySelectorAll('*').length,
|
|
localStorage: Object.keys(localStorage).length,
|
|
};
|
|
});
|
|
|
|
console.log(`After extended use - DOM: ${metrics.domNodes}, localStorage keys: ${metrics.localStorage}`);
|
|
|
|
await takeScreenshot(page, '40-extended-use');
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// 测试套件 12: 无障碍性
|
|
// ============================================
|
|
test.describe('12. 无障碍性', () => {
|
|
|
|
test('12.1 键盘导航', async ({ page }) => {
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
|
|
// 使用 Tab 键导航
|
|
for (let i = 0; i < 10; i++) {
|
|
await page.keyboard.press('Tab');
|
|
await page.waitForTimeout(100);
|
|
}
|
|
|
|
// 检查焦点元素
|
|
const focusedElement = page.locator(':focus');
|
|
console.log(`Focused element visible: ${await focusedElement.isVisible()}`);
|
|
|
|
await takeScreenshot(page, '41-keyboard-nav');
|
|
});
|
|
|
|
test('12.2 ARIA 属性', async ({ page }) => {
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
|
|
// 检查按钮有 accessible name
|
|
const buttons = page.locator('button');
|
|
const count = await buttons.count();
|
|
|
|
let buttonsWithoutLabel = 0;
|
|
for (let i = 0; i < Math.min(count, 20); i++) {
|
|
const btn = buttons.nth(i);
|
|
const label = await btn.getAttribute('aria-label');
|
|
const title = await btn.getAttribute('title');
|
|
const text = await btn.textContent();
|
|
|
|
if (!label && !title && !text?.trim()) {
|
|
buttonsWithoutLabel++;
|
|
}
|
|
}
|
|
|
|
console.log(`Buttons without accessible name: ${buttonsWithoutLabel} out of ${Math.min(count, 20)}`);
|
|
});
|
|
|
|
test('12.3 焦点管理', async ({ page }) => {
|
|
await page.goto(BASE_URL);
|
|
await waitForAppReady(page);
|
|
|
|
// 打开设置
|
|
const settingsBtn = page.getByRole('button', { name: /设置|settings|⚙/i });
|
|
if (await settingsBtn.isVisible()) {
|
|
await settingsBtn.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// 按 Escape 关闭
|
|
await page.keyboard.press('Escape');
|
|
await page.waitForTimeout(500);
|
|
|
|
// 检查焦点是否返回
|
|
const focusedElement = page.locator(':focus');
|
|
console.log(`Focus returned after Escape: ${await focusedElement.isVisible()}`);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// 测试报告生成
|
|
// ============================================
|
|
test.afterAll(async ({}, testInfo) => {
|
|
console.log('\n========================================');
|
|
console.log('ZCLAW 前端功能验证测试完成');
|
|
console.log('========================================');
|
|
console.log(`测试时间: ${new Date().toISOString()}`);
|
|
console.log(`截图目录: ${SCREENSHOT_DIR}`);
|
|
console.log('========================================\n');
|
|
});
|