Major changes: - Shift from "OpenFang desktop client" to "independent AI Agent desktop app" - Add decision principle: "Is this useful for ZCLAW? Does it affect ZCLAW?" - Simplify project structure and tech stack sections - Replace OpenClaw vs OpenFang comparison with unified backend approach - Consolidate troubleshooting from scattered sections into organized FAQ - Update Hands system documentation with 8 capabilities and status - Stream
660 lines
21 KiB
TypeScript
660 lines
21 KiB
TypeScript
/**
|
||
* 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 = '<script>alert("XSS")</script><img src=x onerror=alert("XSS")>';
|
||
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 = '<div>测试</div><b>粗体</b><a href="#">链接</a>';
|
||
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');
|
||
});
|