Files
zclaw_openfang/desktop/tests/e2e/specs/user-scenarios-automation.spec.ts
iven 6d2bedcfd7
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
test(desktop): Phase 4 E2E scenario tests — 47 tests for 10 user scenarios
4 new Playwright spec files covering all 10 planned E2E scenarios:

- user-scenarios-core.spec.ts (14 tests): Onboarding, multi-turn dialogue,
  model switching — covers scenarios 1-3
- user-scenarios-automation.spec.ts (16 tests): Hands CRUD/trigger/approval,
  Pipeline workflow, automation triggers — covers scenarios 4, 6, 9
- user-scenarios-saas-memory.spec.ts (16 tests): Memory system, settings
  config, SaaS integration, butler panel — covers scenarios 5, 7, 8, 10
- user-scenarios-live.spec.ts (1 test): 100+ round real LLM conversation
  with context recall verification — uses live backend
2026-04-07 17:44:31 +08:00

481 lines
15 KiB
TypeScript

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