/** * ZCLAW 边界情况验证测试 * * 测试各种边界条件、错误处理和异常场景 * 确保系统在极端情况下仍能稳定运行 */ import { test, expect, Page, BrowserContext } from '@playwright/test'; import { networkHelpers } from '../utils/network-helpers'; import { storeInspectors, STORE_NAMES } from '../fixtures/store-inspectors'; import { userActions, waitForAppReady, navigateToTab } from '../utils/user-actions'; import { mockErrorResponse, mockTimeout, setupMockGateway } from '../fixtures/mock-gateway'; // 测试超时配置 test.setTimeout(180000); const BASE_URL = 'http://localhost:1420'; // ============================================ // 测试套件 1: 网络边界情况 // ============================================ test.describe('网络边界情况', () => { test('NET-EDGE-01: 完全离线状态', async ({ page, context }) => { // 1. 设置离线 await context.setOffline(true); // 2. 尝试加载页面 await page.goto(BASE_URL).catch(() => { // 预期可能失败 }); // 3. 验证页面处理 // 页面应该显示某种错误状态或重试机制 const bodyText = await page.locator('body').textContent(); console.log('Offline state page content:', bodyText?.substring(0, 200)); // 4. 恢复网络 await context.setOffline(false); }); test('NET-EDGE-02: 网络中断恢复', async ({ page, context }) => { // 1. 正常加载页面 await page.goto(BASE_URL); await waitForAppReady(page); // 2. 获取初始状态 const stateBefore = await storeInspectors.getPersistedState<{ connectionState: string; }>(page, STORE_NAMES.CONNECTION); // 3. 断开网络 await context.setOffline(true); await page.waitForTimeout(2000); // 4. 恢复网络 await context.setOffline(false); await page.waitForTimeout(3000); // 5. 验证连接恢复 const stateAfter = await storeInspectors.getPersistedState<{ connectionState: string; }>(page, STORE_NAMES.CONNECTION); console.log(`Connection: ${stateBefore?.connectionState} -> ${stateAfter?.connectionState}`); }); test('NET-EDGE-03: 请求超时处理', async ({ page }) => { await page.goto(BASE_URL); await waitForAppReady(page); // 1. 模拟超时 await mockTimeout(page, 'chat'); // 2. 尝试发送消息 const chatInput = page.locator('textarea').first(); if (await chatInput.isVisible()) { await chatInput.fill('超时测试消息'); // 点击发送(不等待响应) await page.getByRole('button', { name: '发送消息' }).click(); // 3. 等待并验证错误处理 await page.waitForTimeout(5000); // 验证流式状态已重置 const state = await storeInspectors.getPersistedState<{ isStreaming: boolean; }>(page, STORE_NAMES.CHAT); expect(state?.isStreaming).toBe(false); } }); test('NET-EDGE-04: 服务器错误 (500)', async ({ page }) => { await page.goto(BASE_URL); await waitForAppReady(page); // 1. Mock 500 错误 await mockErrorResponse(page, 'chat', 500, 'Internal Server Error'); // 2. 尝试发送消息 const chatInput = page.locator('textarea').first(); if (await chatInput.isVisible()) { await chatInput.fill('错误测试消息'); await page.getByRole('button', { name: '发送消息' }).click(); // 3. 验证错误处理 await page.waitForTimeout(3000); // 检查是否有错误提示 const errorElement = page.locator('[class*="error"]').or( page.locator('[role="alert"]'), ); console.log(`Error shown: ${await errorElement.count() > 0}`); } }); test('NET-EDGE-05: 限流处理 (429)', async ({ page }) => { await page.goto(BASE_URL); await waitForAppReady(page); // 1. Mock 429 限流 await networkHelpers.simulateRateLimit(page, 'chat', 60); // 2. 尝试发送消息 const chatInput = page.locator('textarea').first(); if (await chatInput.isVisible()) { await chatInput.fill('限流测试消息'); await page.getByRole('button', { name: '发送消息' }).click(); // 3. 验证限流处理 await page.waitForTimeout(3000); console.log('Rate limit handling verified'); } }); test('NET-EDGE-06: 慢速网络', async ({ page }) => { // 1. 模拟慢速网络 await page.route('**/api/**', async (route) => { await new Promise((r) => setTimeout(r, 2000)); // 2秒延迟 await route.continue(); }); // 2. 加载页面 const startTime = Date.now(); await page.goto(BASE_URL); await waitForAppReady(page); const loadTime = Date.now() - startTime; // 3. 验证加载时间 console.log(`Page load time with slow network: ${loadTime}ms`); // 4. 验证页面仍然可用 const sidebar = page.locator('aside').first(); await expect(sidebar).toBeVisible(); }); }); // ============================================ // 测试套件 2: 数据边界情况 // ============================================ test.describe('数据边界情况', () => { test.beforeEach(async ({ page }) => { await page.goto(BASE_URL); await waitForAppReady(page); }); test('DATA-EDGE-01: 超长消息', async ({ page }) => { const chatInput = page.locator('textarea').first(); if (await chatInput.isVisible()) { // 1. 生成超长消息 const longMessage = '这是一条很长的测试消息。'.repeat(500); // ~15000 字符 // 2. 输入消息 await chatInput.fill(longMessage); // 3. 验证输入被接受 const value = await chatInput.inputValue(); expect(value.length).toBeGreaterThan(10000); // 4. 发送消息 await page.getByRole('button', { name: '发送消息' }).click(); await page.waitForTimeout(3000); // 5. 验证消息显示(可能被截断) const messageElement = page.locator('[class*="message"]').filter({ hasText: '这是一条很长的测试消息', }); console.log(`Long message visible: ${await messageElement.count() > 0}`); } }); test('DATA-EDGE-02: 空消息', async ({ page }) => { const chatInput = page.locator('textarea').first(); if (await chatInput.isVisible()) { // 1. 获取初始消息数量 const stateBefore = await storeInspectors.getPersistedState<{ messages: unknown[]; }>(page, STORE_NAMES.CHAT); const countBefore = stateBefore?.messages?.length ?? 0; // 2. 尝试发送空消息 await chatInput.fill(''); await page.getByRole('button', { name: '发送消息' }).click(); // 3. 验证空消息不应被发送 await page.waitForTimeout(1000); const stateAfter = await storeInspectors.getPersistedState<{ messages: unknown[]; }>(page, STORE_NAMES.CHAT); const countAfter = stateAfter?.messages?.length ?? 0; // 消息数量不应增加 expect(countAfter).toBe(countBefore); } }); test('DATA-EDGE-03: 特殊字符消息', async ({ page }) => { const chatInput = page.locator('textarea').first(); if (await chatInput.isVisible()) { // 1. 发送包含特殊字符的消息 const specialChars = '!@#$%^&*(){}[]|\\:";\'<>?,./~`\n\t测试'; await chatInput.fill(specialChars); await page.getByRole('button', { name: '发送消息' }).click(); // 2. 验证消息显示 await page.waitForTimeout(2000); console.log('Special characters message sent'); } }); test('DATA-EDGE-04: Unicode 和 Emoji', async ({ page }) => { const chatInput = page.locator('textarea').first(); if (await chatInput.isVisible()) { // 1. 发送包含 Unicode 和 Emoji 的消息 const unicodeMessage = '你好世界 🌍 مرحبا Привет 🎉 こんにちは'; await chatInput.fill(unicodeMessage); await page.getByRole('button', { name: '发送消息' }).click(); // 2. 验证消息显示 await page.waitForTimeout(2000); const messageElement = page.locator('[class*="message"]').filter({ hasText: '你好世界', }); console.log(`Unicode message visible: ${await messageElement.count() > 0}`); } }); test('DATA-EDGE-05: 代码块内容', async ({ page }) => { const chatInput = page.locator('textarea').first(); if (await chatInput.isVisible()) { // 1. 发送包含代码块的消息 const codeMessage = ` 请帮我检查这段代码: \`\`\`javascript function hello() { console.log("Hello, World!"); } \`\`\` `; await chatInput.fill(codeMessage); await page.getByRole('button', { name: '发送消息' }).click(); // 2. 验证代码块渲染 await page.waitForTimeout(2000); const codeBlock = page.locator('pre').or(page.locator('code')); console.log(`Code block visible: ${await codeBlock.count() > 0}`); } }); test('DATA-EDGE-06: 空 Hands 列表', async ({ page }) => { // 1. Mock 空 Hands 响应 await page.route('**/api/hands', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([]), }); }); // 2. 导航到 Hands await navigateToTab(page, 'Hands'); await page.waitForTimeout(2000); // 3. 验证空状态显示 const emptyState = page.locator('text=暂无').or( page.locator('text=无可用').or( page.locator('text=empty', { exact: false }), ), ); console.log(`Empty state shown: ${await emptyState.count() > 0}`); }); }); // ============================================ // 测试套件 3: 状态边界情况 // ============================================ test.describe('状态边界情况', () => { test.beforeEach(async ({ page }) => { await page.goto(BASE_URL); await waitForAppReady(page); }); test('STATE-EDGE-01: 快速连续点击', async ({ page }) => { const chatInput = page.locator('textarea').first(); if (await chatInput.isVisible()) { // 1. 获取初始消息数量 const stateBefore = await storeInspectors.getPersistedState<{ messages: unknown[]; }>(page, STORE_NAMES.CHAT); const countBefore = stateBefore?.messages?.length ?? 0; // 2. 快速点击发送按钮多次 await chatInput.fill('快速点击测试'); const sendBtn = page.getByRole('button', { name: '发送消息' }); // 连续点击 5 次 for (let i = 0; i < 5; i++) { await sendBtn.click({ delay: 50 }); } // 3. 等待处理完成 await page.waitForTimeout(5000); // 4. 验证只发送了一条消息(防抖生效) const stateAfter = await storeInspectors.getPersistedState<{ messages: unknown[]; }>(page, STORE_NAMES.CHAT); const countAfter = stateAfter?.messages?.length ?? 0; // 消息数量应该只增加有限数量(理想情况是 1) console.log(`Messages: ${countBefore} -> ${countAfter}`); expect(countAfter - countBefore).toBeLessThan(5); } }); test('STATE-EDGE-02: 流式中刷新页面', async ({ page }) => { const chatInput = page.locator('textarea').first(); if (await chatInput.isVisible()) { // 1. 发送消息 await chatInput.fill('流式测试消息'); await page.getByRole('button', { name: '发送消息' }).click(); // 2. 立即刷新页面(在流式响应中) await page.waitForTimeout(500); await page.reload(); await waitForAppReady(page); // 3. 验证状态恢复 const state = await storeInspectors.getPersistedState<{ isStreaming: boolean; messages: unknown[]; }>(page, STORE_NAMES.CHAT); // 流式状态应该是 false expect(state?.isStreaming).toBe(false); } }); test('STATE-EDGE-03: 多次切换标签', async ({ page }) => { const tabs = ['分身', 'Hands', '工作流', '团队', '协作']; // 1. 快速切换标签 20 次 for (let i = 0; i < 20; i++) { const tab = tabs[i % tabs.length]; await navigateToTab(page, tab); await page.waitForTimeout(100); } // 2. 验证无错误 const errorElements = page.locator('[class*="error"]'); const errorCount = await errorElements.count(); console.log(`Errors after rapid switching: ${errorCount}`); // 3. 验证最终标签正确显示 const sidebar = page.locator('aside').first(); await expect(sidebar).toBeVisible(); }); test('STATE-EDGE-04: 清除 localStorage 后恢复', async ({ page }) => { // 1. 加载页面 await page.goto(BASE_URL); await waitForAppReady(page); // 2. 清除 localStorage await page.evaluate(() => { localStorage.clear(); sessionStorage.clear(); }); // 3. 刷新页面 await page.reload(); await waitForAppReady(page); // 4. 验证应用正常初始化 const sidebar = page.locator('aside').first(); await expect(sidebar).toBeVisible(); // 5. 验证 Store 重新初始化 const chatState = await storeInspectors.getPersistedState<{ messages: unknown[]; }>(page, STORE_NAMES.CHAT); expect(Array.isArray(chatState?.messages)).toBe(true); }); test('STATE-EDGE-05: 长时间运行稳定性', async ({ page }) => { // 1. 加载页面 await page.goto(BASE_URL); await waitForAppReady(page); // 2. 记录初始内存 const initialMetrics = await page.evaluate(() => ({ domNodes: document.querySelectorAll('*').length, jsHeapSize: (performance as any).memory?.usedJSHeapSize || 0, })); // 3. 执行多次操作 for (let i = 0; i < 5; i++) { await navigateToTab(page, ['分身', 'Hands', '工作流'][i % 3]); await page.waitForTimeout(500); } // 4. 记录最终内存 const finalMetrics = await page.evaluate(() => ({ domNodes: document.querySelectorAll('*').length, jsHeapSize: (performance as any).memory?.usedJSHeapSize || 0, })); // 5. 验证内存没有显著增长 console.log(`DOM nodes: ${initialMetrics.domNodes} -> ${finalMetrics.domNodes}`); console.log(`JS heap: ${initialMetrics.jsHeapSize} -> ${finalMetrics.jsHeapSize}`); // DOM 节点不应显著增加 expect(finalMetrics.domNodes).toBeLessThan(initialMetrics.domNodes * 2); }); }); // ============================================ // 测试套件 4: UI 边界情况 // ============================================ test.describe('UI 边界情况', () => { test('UI-EDGE-01: 最小窗口尺寸', async ({ page }) => { // 1. 设置最小窗口尺寸 await page.setViewportSize({ width: 375, height: 667 }); await page.goto(BASE_URL); await waitForAppReady(page); // 2. 验证核心功能可用 const sidebar = page.locator('aside').first(); const main = page.locator('main'); // 至少应该有一个可见 const sidebarVisible = await sidebar.isVisible(); const mainVisible = await main.isVisible(); expect(sidebarVisible || mainVisible).toBe(true); }); test('UI-EDGE-02: 大窗口尺寸', async ({ page }) => { // 1. 设置大窗口尺寸 await page.setViewportSize({ width: 2560, height: 1440 }); await page.goto(BASE_URL); await waitForAppReady(page); // 2. 验证布局正确 const sidebar = page.locator('aside').first(); const main = page.locator('main'); await expect(sidebar).toBeVisible(); await expect(main).toBeVisible(); }); test('UI-EDGE-03: 窗口尺寸变化', async ({ page }) => { // 1. 从大窗口开始 await page.setViewportSize({ width: 1920, height: 1080 }); await page.goto(BASE_URL); await waitForAppReady(page); // 2. 逐步缩小窗口 const sizes = [ { width: 1200, height: 800 }, { width: 768, height: 1024 }, { width: 375, height: 667 }, ]; for (const size of sizes) { await page.setViewportSize(size); await page.waitForTimeout(300); // 验证无布局错误 const body = page.locator('body'); await expect(body).toBeVisible(); } }); test('UI-EDGE-04: 深色模式(如果支持)', async ({ page }) => { // 1. 模拟深色模式偏好 await page.emulateMedia({ colorScheme: 'dark' }); await page.goto(BASE_URL); await waitForAppReady(page); // 2. 验证页面加载 const body = page.locator('body'); await expect(body).toBeVisible(); }); test('UI-EDGE-05: 减少动画(如果支持)', async ({ page }) => { // 1. 模拟减少动画偏好 await page.emulateMedia({ reducedMotion: 'reduce' }); await page.goto(BASE_URL); await waitForAppReady(page); // 2. 验证页面加载 const body = page.locator('body'); await expect(body).toBeVisible(); }); }); // ============================================ // 测试套件 5: 输入验证边界情况 // ============================================ test.describe('输入验证边界情况', () => { test.beforeEach(async ({ page }) => { await page.goto(BASE_URL); await waitForAppReady(page); }); test('INPUT-EDGE-01: XSS 注入尝试', async ({ page }) => { const chatInput = page.locator('textarea').first(); if (await chatInput.isVisible()) { // 1. 发送包含潜在 XSS 的消息 const xssPayload = ''; await chatInput.fill(xssPayload); await page.getByRole('button', { name: '发送消息' }).click(); // 2. 验证消息显示(应该被转义) await page.waitForTimeout(2000); // 3. 检查没有 alert 弹出 // (Playwright 不会执行 alert,所以只需要验证没有错误) console.log('XSS test passed - no alert shown'); } }); test('INPUT-EDGE-02: HTML 标签输入', async ({ page }) => { const chatInput = page.locator('textarea').first(); if (await chatInput.isVisible()) { // 1. 发送包含 HTML 的消息 const htmlContent = '
测试
粗体链接'; await chatInput.fill(htmlContent); await page.getByRole('button', { name: '发送消息' }).click(); // 2. 验证消息显示 await page.waitForTimeout(2000); console.log('HTML input test completed'); } }); test('INPUT-EDGE-03: JSON 格式参数', async ({ page }) => { await navigateToTab(page, 'Hands'); await page.waitForTimeout(1000); // 1. 查找 JSON 输入框(如果有) const jsonInput = page.locator('textarea').filter({ hasText: /{/, }).or( page.locator('input[placeholder*="JSON"]'), ); if (await jsonInput.isVisible()) { // 2. 输入无效 JSON await jsonInput.fill('{ invalid json }'); await page.waitForTimeout(300); // 3. 验证错误提示 const errorElement = page.locator('[class*="error"]').filter({ hasText: /JSON|格式|解析/, }); console.log(`JSON error shown: ${await errorElement.count() > 0}`); // 4. 输入有效 JSON await jsonInput.fill('{ "valid": "json" }'); await page.waitForTimeout(300); } }); }); // ============================================ // 测试套件 6: 并发操作边界情况 // ============================================ test.describe('并发操作边界情况', () => { test.beforeEach(async ({ page }) => { await page.goto(BASE_URL); await waitForAppReady(page); }); test('CONCURRENT-EDGE-01: 同时发送多条消息', async ({ page }) => { const chatInput = page.locator('textarea').first(); if (await chatInput.isVisible()) { // 1. 获取初始状态 const stateBefore = await storeInspectors.getPersistedState<{ messages: unknown[]; isStreaming: boolean; }>(page, STORE_NAMES.CHAT); // 2. 快速发送多条消息 for (let i = 0; i < 3; i++) { await chatInput.fill(`并发消息 ${i + 1}`); await page.getByRole('button', { name: '发送消息' }).click(); await page.waitForTimeout(100); } // 3. 等待所有处理完成 await page.waitForTimeout(10000); // 4. 验证最终状态 const stateAfter = await storeInspectors.getPersistedState<{ isStreaming: boolean; }>(page, STORE_NAMES.CHAT); expect(stateAfter?.isStreaming).toBe(false); } }); test('CONCURRENT-EDGE-02: 操作中切换视图', async ({ page }) => { const chatInput = page.locator('textarea').first(); if (await chatInput.isVisible()) { // 1. 发送消息 await chatInput.fill('测试消息'); await page.getByRole('button', { name: '发送消息' }).click(); // 2. 立即切换视图 await navigateToTab(page, 'Hands'); await page.waitForTimeout(500); // 3. 切回聊天 await navigateToTab(page, '分身'); // 4. 验证无错误 const sidebar = page.locator('aside').first(); await expect(sidebar).toBeVisible(); } }); }); // 测试报告 test.afterAll(async ({}, testInfo) => { console.log('\n========================================'); console.log('ZCLAW 边界情况验证测试完成'); console.log('========================================'); console.log(`测试时间: ${new Date().toISOString()}`); console.log('========================================\n'); });