/** * ZCLAW User Scenario E2E Tests — Automation Scenarios * * Scenario 4: Hands 完整流程 * Scenario 6: Pipeline/Workflow 执行 * Scenario 9: 自动化触发器 */ import { test, expect } from '@playwright/test'; import { setupMockGateway, setupMockGatewayWithWebSocket, mockResponses, } from '../fixtures/mock-gateway'; import { storeInspectors, STORE_NAMES } from '../fixtures/store-inspectors'; import { skipOnboarding, waitForAppReady, navigateToTab } from '../utils/user-actions'; const BASE_URL = 'http://localhost:1420'; test.setTimeout(120000); /** Helper: click send button */ async function clickSend(page: import('@playwright/test').Page) { const sendButton = page.getByRole('button', { name: '发送消息' }).or( page.locator('button.bg-orange-500').first() ); await sendButton.first().click(); } // ═══════════════════════════════════════════════════════════════════ // Scenario 4: Hands 完整流程 // ═══════════════════════════════════════════════════════════════════ test.describe('Scenario 4: Hands 完整流程', () => { test.describe.configure({ mode: 'serial' }); test('S4-01: Hands 列表应显示可用 Hand', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); await navigateToTab(page, 'Hands'); await page.waitForTimeout(1000); // 获取 Hands 列表 const handsResponse = await page.evaluate(async () => { try { const response = await fetch('/api/hands'); return await response.json(); } catch { return null; } }); expect(handsResponse).toHaveProperty('hands'); expect(Array.isArray(handsResponse.hands)).toBe(true); expect(handsResponse.hands.length).toBeGreaterThan(0); // 验证每个 Hand 有必要字段 for (const hand of handsResponse.hands) { expect(hand).toHaveProperty('id'); expect(hand).toHaveProperty('name'); expect(hand).toHaveProperty('status'); expect(hand).toHaveProperty('requirements_met'); } }); test('S4-02: 触发 Researcher Hand 应返回 runId', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); // 触发 Researcher const activateResponse = await page.evaluate(async () => { try { const response = await fetch('/api/hands/researcher/activate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: '玩具出口欧盟标准' }), }); return await response.json(); } catch { return null; } }); expect(activateResponse).toHaveProperty('runId'); expect(activateResponse).toHaveProperty('status'); expect(activateResponse.status).toBe('running'); }); test('S4-03: Hand 审批流 — 批准执行', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); const approveResponse = await page.evaluate(async () => { try { const response = await fetch('/api/hands/browser/runs/test-run-id/approve', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ approved: true, comment: '看起来安全' }), }); return await response.json(); } catch { return null; } }); expect(approveResponse).toHaveProperty('status'); expect(approveResponse.status).toBe('approved'); }); test('S4-04: Hand 取消执行', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); const cancelResponse = await page.evaluate(async () => { try { const response = await fetch('/api/hands/browser/runs/test-run-id/cancel', { method: 'POST', }); return await response.json(); } catch { return null; } }); expect(cancelResponse).toHaveProperty('status'); expect(cancelResponse.status).toBe('cancelled'); }); test('S4-05: Hand 运行历史应可查询', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); const runsResponse = await page.evaluate(async () => { try { const response = await fetch('/api/hands/browser/runs'); return await response.json(); } catch { return null; } }); expect(runsResponse).toHaveProperty('runs'); expect(Array.isArray(runsResponse.runs)).toBe(true); }); test('S4-06: Hand 需求检查应正确', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); const handsResponse = await page.evaluate(async () => { try { const response = await fetch('/api/hands'); return await response.json(); } catch { return null; } }); for (const hand of handsResponse?.hands ?? []) { expect(hand).toHaveProperty('requirements_met'); expect(typeof hand.requirements_met).toBe('boolean'); } }); test('S4-07: 触发 Browser Hand 应返回 runId', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); const response = await page.evaluate(async () => { try { const res = await fetch('/api/hands/browser/activate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: 'https://example.com' }), }); return await res.json(); } catch { return null; } }); expect(response).toHaveProperty('runId'); expect(response).toHaveProperty('status'); }); }); // ═══════════════════════════════════════════════════════════════════ // Scenario 6: Pipeline/Workflow 执行 // ═══════════════════════════════════════════════════════════════════ test.describe('Scenario 6: Pipeline/Workflow', () => { test.describe.configure({ mode: 'serial' }); test('S6-01: 工作流列表应可加载', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); const workflowsResponse = await page.evaluate(async () => { try { const response = await fetch('/api/workflows'); return await response.json(); } catch { return null; } }); expect(workflowsResponse).toHaveProperty('workflows'); expect(Array.isArray(workflowsResponse.workflows)).toBe(true); }); test('S6-02: 创建新工作流', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); const createResponse = await page.evaluate(async () => { try { const response = await fetch('/api/workflows', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: '玩具市场调研 Pipeline', description: '自动收集玩具行业市场数据', steps: [ { handName: 'researcher', params: { topic: '2024年玩具市场趋势' } }, { handName: 'collector', params: { sources: ['行业报告', '海关数据'] } }, ], }), }); return await response.json(); } catch { return null; } }); expect(createResponse).toHaveProperty('id'); expect(createResponse.name).toBe('玩具市场调研 Pipeline'); }); test('S6-03: 执行工作流应返回 runId', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); const executeResponse = await page.evaluate(async () => { try { const response = await fetch('/api/workflows/wf-default/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ params: { topic: '测试执行' } }), }); return await response.json(); } catch { return null; } }); expect(executeResponse).toHaveProperty('runId'); expect(executeResponse).toHaveProperty('status'); expect(executeResponse.status).toBe('running'); }); test('S6-04: 工作流步骤应有合理结构', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); const workflowsResponse = await page.evaluate(async () => { try { const response = await fetch('/api/workflows'); return await response.json(); } catch { return null; } }); const workflows = workflowsResponse?.workflows ?? []; if (workflows.length > 0) { const wf = workflows[0]; expect(wf).toHaveProperty('id'); expect(wf).toHaveProperty('name'); } }); }); // ═══════════════════════════════════════════════════════════════════ // Scenario 9: 自动化触发器 // ═══════════════════════════════════════════════════════════════════ test.describe('Scenario 9: 自动化触发器', () => { test.describe.configure({ mode: 'serial' }); test('S9-01: 触发器列表应可加载', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); const triggersResponse = await page.evaluate(async () => { try { const response = await fetch('/api/triggers'); return await response.json(); } catch { return null; } }); expect(triggersResponse).toHaveProperty('triggers'); expect(Array.isArray(triggersResponse.triggers)).toBe(true); }); test('S9-02: 创建定时触发器', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); const createResponse = await page.evaluate(async () => { try { const response = await fetch('/api/triggers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'schedule', name: '每日行业动态推送', enabled: true, schedule: '0 9 * * *', handName: 'researcher', params: { topic: '玩具行业每日动态' }, }), }); return await response.json(); } catch { return null; } }); expect(createResponse).toHaveProperty('id'); }); test('S9-03: 创建条件触发器', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); const createResponse = await page.evaluate(async () => { try { const response = await fetch('/api/triggers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'event', name: '关键词触发', enabled: true, condition: { keywords: ['ABS', '原料', '涨价'] }, handName: 'collector', }), }); return await response.json(); } catch { return null; } }); expect(createResponse).toHaveProperty('id'); }); test('S9-04: 触发器创建后列表应增加', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); // 获取初始列表 const listBefore = await page.evaluate(async () => { try { const response = await fetch('/api/triggers'); return await response.json(); } catch { return { triggers: [] }; } }); const countBefore = listBefore.triggers.length; // 创建新触发器 await page.evaluate(async () => { await fetch('/api/triggers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'webhook', name: '新触发器', enabled: true, }), }); }); // 列表应增加 const listAfter = await page.evaluate(async () => { try { const response = await fetch('/api/triggers'); return await response.json(); } catch { return { triggers: [] }; } }); expect(listAfter.triggers.length).toBeGreaterThan(countBefore); }); test('S9-05: 删除触发器应成功', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); // 先创建一个 const createRes = await page.evaluate(async () => { try { const response = await fetch('/api/triggers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'webhook', name: '待删除触发器', enabled: true }), }); return await response.json(); } catch { return null; } }); const triggerId = createRes?.id; if (!triggerId) { test.skip(); return; } // 删除 const deleteResponse = await page.evaluate(async (id) => { try { const response = await fetch(`/api/triggers/${id}`, { method: 'DELETE' }); return await response.json(); } catch { return null; } }, triggerId); // 应成功 expect(deleteResponse).not.toBeNull(); }); }); // ═══════════════════════════════════════════════════════════════════ // Test Report // ═══════════════════════════════════════════════════════════════════ test.afterAll(async ({}, testInfo) => { console.log('\n========================================'); console.log('ZCLAW User Scenario Automation Tests Complete'); console.log(`Test Time: ${new Date().toISOString()}`); console.log('========================================\n'); });