/** * ZCLAW User Scenario E2E Tests — SaaS, Settings, Memory, Butler * * Scenario 5: 记忆系统 * Scenario 7: 设置配置 * Scenario 8: SaaS 集成 * Scenario 10: 管家面板 */ 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 5: 记忆系统 // ═══════════════════════════════════════════════════════════════════ test.describe('Scenario 5: 记忆系统', () => { test.describe.configure({ mode: 'serial' }); test('S5-01: 告知信息后应可查询记忆', async ({ page }) => { await setupMockGatewayWithWebSocket(page, { wsConfig: { responseContent: '好的,我记住了。您在澄海做塑料玩具。', streaming: true }, }); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); // 告知关键信息 const chatInput = page.locator('textarea').first(); await chatInput.fill('我的工厂在澄海,主要做塑料玩具出口'); await clickSend(page); await page.waitForTimeout(3000); // 查询记忆 API const memoryResponse = await page.evaluate(async () => { try { const response = await fetch('/api/memory/search?q=澄海'); return await response.json(); } catch { return null; } }); // 记忆 API 应返回结果(即使为空也应有响应) expect(memoryResponse).not.toBeNull(); }); test('S5-02: 记忆面板应可访问', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); // 查找记忆相关 UI 元素 const memoryElements = page.locator('[class*="memory"], [class*="记忆"], [data-testid*="memory"]').or( page.locator('text=记忆').or(page.locator('text=Memory')) ); // 无论是否可见,页面不应崩溃 await expect(page.locator('textarea').first()).toBeVisible({ timeout: 5000 }); }); test('S5-03: 多轮对话后搜索记忆', async ({ page }) => { await setupMockGatewayWithWebSocket(page, { wsConfig: { responseContent: '好的,我了解了。', streaming: true }, }); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); // 发送多轮包含关键信息的消息 const infoMessages = [ '我们工厂在澄海,做塑料玩具', '主要出口欧洲和北美市场', '年产量大约50万件', ]; for (const msg of infoMessages) { const chatInput = page.locator('textarea').first(); await chatInput.fill(msg); await clickSend(page); await page.waitForTimeout(2000); } // 搜索记忆 const searchResults = await page.evaluate(async () => { try { const response = await fetch('/api/memory/search?q=澄海'); return await response.json(); } catch { return null; } }); // 应有搜索结果结构 expect(searchResults).not.toBeNull(); }); }); // ═══════════════════════════════════════════════════════════════════ // Scenario 7: 设置配置 // ═══════════════════════════════════════════════════════════════════ test.describe('Scenario 7: 设置配置', () => { test.describe.configure({ mode: 'serial' }); test('S7-01: 设置页面应可访问', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); // 尝试打开设置 const settingsBtn = page.locator('aside button[aria-label="打开设置"]').or( page.locator('aside button[title="设置"]') ).or( page.locator('aside .p-3.border-t button') ).or( page.getByRole('button', { name: /打开设置|设置|settings/i }) ); if (await settingsBtn.first().isVisible({ timeout: 3000 }).catch(() => false)) { await settingsBtn.first().click(); await page.waitForTimeout(1000); // 设置面板应有内容 const settingsContent = page.locator('[role="dialog"]').or( page.locator('.fixed.inset-0').last() ).or( page.locator('[class*="settings"]') ); // 不应崩溃 await expect(page.locator('body')).toBeVisible(); } else { // 设置按钮可能不在侧边栏,跳过 test.skip(); } }); test('S7-02: 配置 API 读写应一致', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); // 读取配置 const configBefore = await page.evaluate(async () => { try { const response = await fetch('/api/config'); return await response.json(); } catch { return null; } }); expect(configBefore).not.toBeNull(); // 写入配置 const updateResponse = await page.evaluate(async () => { try { const response = await fetch('/api/config', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userName: 'E2E测试用户', userRole: '工厂老板' }), }); return await response.json(); } catch { return null; } }); expect(updateResponse).not.toBeNull(); // 再次读取,验证一致性 const configAfter = await page.evaluate(async () => { try { const response = await fetch('/api/config'); return await response.json(); } catch { return null; } }); expect(configAfter?.userName).toBe('E2E测试用户'); expect(configAfter?.userRole).toBe('工厂老板'); }); test('S7-03: 安全状态应可查询', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); const securityStatus = await page.evaluate(async () => { try { const response = await fetch('/api/security/status'); return await response.json(); } catch { return null; } }); expect(securityStatus).toHaveProperty('status'); expect(securityStatus.status).toBe('secure'); }); test('S7-04: 插件状态应可查询', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); const pluginStatus = await page.evaluate(async () => { try { const response = await fetch('/api/plugins/status'); return await response.json(); } catch { return null; } }); expect(Array.isArray(pluginStatus)).toBe(true); if (pluginStatus.length > 0) { expect(pluginStatus[0]).toHaveProperty('id'); expect(pluginStatus[0]).toHaveProperty('name'); expect(pluginStatus[0]).toHaveProperty('status'); } }); test('S7-05: 配置边界值处理', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); // 发送过长配置值 const longValue = 'A'.repeat(500); const response = await page.evaluate(async (value) => { try { const response = await fetch('/api/config', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userName: value }), }); return { status: response.status, ok: response.ok }; } catch { return { status: 0, ok: false }; } }, longValue); // 应返回响应,不崩溃 expect(response.status).toBeGreaterThan(0); }); }); // ═══════════════════════════════════════════════════════════════════ // Scenario 8: SaaS 集成 // ═══════════════════════════════════════════════════════════════════ test.describe('Scenario 8: SaaS 集成', () => { test.describe.configure({ mode: 'serial' }); test('S8-01: SaaS 连接模式应可切换', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); // 检查连接模式 store const connectionMode = await page.evaluate(() => { const stores = (window as any).__ZCLAW_STORES__; if (stores?.saas?.getState) { return stores.saas.getState().connectionMode; } return null; }); // 默认应为 tauri 模式 expect(connectionMode).toBeOneOf(['tauri', 'saas', 'gateway', null]); }); test('S8-02: SaaS 登录表单应可访问', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); // 查找 SaaS 登录相关元素 const loginElements = page.locator('text=登录').or( page.locator('text=SaaS').or( page.locator('[class*="login"]').or( page.locator('[class*="auth"]') ) ) ); // 页面不应崩溃 await expect(page.locator('body')).toBeVisible(); }); test('S8-03: 用量统计应可查询', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); const usageStats = await page.evaluate(async () => { try { const response = await fetch('/api/stats/usage'); return await response.json(); } catch { return null; } }); expect(usageStats).not.toBeNull(); expect(usageStats).toHaveProperty('totalSessions'); expect(usageStats).toHaveProperty('totalMessages'); expect(usageStats).toHaveProperty('totalTokens'); }); test('S8-04: 会话统计应可查询', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); const sessionStats = await page.evaluate(async () => { try { const response = await fetch('/api/stats/sessions'); return await response.json(); } catch { return null; } }); expect(sessionStats).not.toBeNull(); expect(sessionStats).toHaveProperty('total'); }); test('S8-05: 审计日志应可查询', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); const auditLogs = await page.evaluate(async () => { try { const response = await fetch('/api/audit/logs'); return await response.json(); } catch { return null; } }); expect(auditLogs).not.toBeNull(); expect(auditLogs).toHaveProperty('logs'); expect(auditLogs).toHaveProperty('total'); }); }); // ═══════════════════════════════════════════════════════════════════ // Scenario 10: 管家面板 // ═══════════════════════════════════════════════════════════════════ test.describe('Scenario 10: 管家面板', () => { test.describe.configure({ mode: 'serial' }); test('S10-01: 管家面板应可访问', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); // 查找管家面板入口 const butlerTab = page.locator('text=管家').or( page.locator('text=Butler').or( page.locator('[data-testid*="butler"]') ) ); if (await butlerTab.first().isVisible({ timeout: 3000 }).catch(() => false)) { await butlerTab.first().click(); await page.waitForTimeout(1000); } // UI 不崩溃 await expect(page.locator('body')).toBeVisible(); }); test('S10-02: 痛点区块应有结构', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); // 查找痛点相关元素 const painElements = page.locator('text=痛点').or( page.locator('text=关注').or( page.locator('text=Pain') ) ); // 页面不崩溃 await expect(page.locator('textarea').first()).toBeVisible({ timeout: 5000 }); }); test('S10-03: 推理链应有逻辑结构', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); // 查找推理链相关 UI const reasoningElements = page.locator('[class*="reasoning"]').or( page.locator('[class*="chain"]').or( page.locator('text=推理') ) ); // 页面不应崩溃 await expect(page.locator('body')).toBeVisible(); }); }); // ═══════════════════════════════════════════════════════════════════ // Test Report // ═══════════════════════════════════════════════════════════════════ test.afterAll(async ({}, testInfo) => { console.log('\n========================================'); console.log('ZCLAW SaaS/Memory/Butler Scenario Tests Complete'); console.log(`Test Time: ${new Date().toISOString()}`); console.log('========================================\n'); });