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
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
481 lines
15 KiB
TypeScript
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');
|
|
});
|