Files
zclaw_openfang/desktop/tests/e2e/specs/tauri-core.spec.ts
iven 3ff08faa56 release(v0.2.0): streaming, MCP protocol, Browser Hand, security enhancements
## Major Features

### Streaming Response System
- Implement LlmDriver trait with `stream()` method returning async Stream
- Add SSE parsing for Anthropic and OpenAI API streaming
- Integrate Tauri event system for frontend streaming (`stream:chunk` events)
- Add StreamChunk types: Delta, ToolStart, ToolEnd, Complete, Error

### MCP Protocol Implementation
- Add MCP JSON-RPC 2.0 types (mcp_types.rs)
- Implement stdio-based MCP transport (mcp_transport.rs)
- Support tool discovery, execution, and resource operations

### Browser Hand Implementation
- Complete browser automation with Playwright-style actions
- Support Navigate, Click, Type, Scrape, Screenshot, Wait actions
- Add educational Hands: Whiteboard, Slideshow, Speech, Quiz

### Security Enhancements
- Implement command whitelist/blacklist for shell_exec tool
- Add SSRF protection with private IP blocking
- Create security.toml configuration file

## Test Improvements
- Fix test import paths (security-utils, setup)
- Fix vi.mock hoisting issues with vi.hoisted()
- Update test expectations for validateUrl and sanitizeFilename
- Add getUnsupportedLocalGatewayStatus mock

## Documentation Updates
- Update architecture documentation
- Improve configuration reference
- Add quick-start guide updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 03:24:24 +08:00

348 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* ZCLAW Tauri 模式 E2E 测试
*
* 测试 Tauri 桌面应用特有的功能和集成
* 验证 Kernel Client、Rust 后端和 Native 功能的完整性
*/
import { test, expect, Page } from '@playwright/test';
test.setTimeout(120000);
async function waitForAppReady(page: Page) {
await page.waitForLoadState('domcontentloaded');
await page.waitForTimeout(2000);
}
async function takeScreenshot(page: Page, name: string) {
await page.screenshot({
path: `test-results/tauri-artifacts/${name}.png`,
fullPage: true,
});
}
test.describe('ZCLAW Tauri 模式核心功能', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await waitForAppReady(page);
});
test.describe('1. Tauri 运行时检测', () => {
test('应该检测到 Tauri 运行时环境', async ({ page }) => {
const isTauri = await page.evaluate(() => {
return '__TAURI_INTERNALS__' in window;
});
console.log('[Tauri Check] isTauriRuntime:', isTauri);
if (!isTauri) {
console.warn('[Tauri Check] Warning: Not running in Tauri environment');
console.warn('[Tauri Check] Some tests may not work correctly');
}
await takeScreenshot(page, '01-tauri-runtime-check');
});
test('Tauri API 应该可用', async ({ page }) => {
const tauriAvailable = await page.evaluate(async () => {
try {
const { invoke } = await import('@tauri-apps/api/core');
const result = await invoke('kernel_status');
return { available: true, result };
} catch (error) {
return { available: false, error: String(error) };
}
});
console.log('[Tauri API] Available:', tauriAvailable);
if (tauriAvailable.available) {
console.log('[Tauri API] Kernel status:', tauriAvailable.result);
} else {
console.warn('[Tauri API] Not available:', tauriAvailable.error);
}
await takeScreenshot(page, '02-tauri-api-check');
});
});
test.describe('2. 内核状态验证', () => {
test('内核初始化状态', async ({ page }) => {
const kernelStatus = await page.evaluate(async () => {
try {
const { invoke } = await import('@tauri-apps/api/core');
const status = await invoke<{
initialized: boolean;
agentCount: number;
databaseUrl: string | null;
defaultProvider: string | null;
defaultModel: string | null;
}>('kernel_status');
return {
success: true,
initialized: status.initialized,
agentCount: status.agentCount,
provider: status.defaultProvider,
model: status.defaultModel,
};
} catch (error) {
return {
success: false,
error: String(error),
};
}
});
console.log('[Kernel Status]', kernelStatus);
if (kernelStatus.success) {
console.log('[Kernel] Initialized:', kernelStatus.initialized);
console.log('[Kernel] Agents:', kernelStatus.agentCount);
console.log('[Kernel] Provider:', kernelStatus.provider);
console.log('[Kernel] Model:', kernelStatus.model);
}
await takeScreenshot(page, '03-kernel-status');
});
test('Agent 列表获取', async ({ page }) => {
const agents = await page.evaluate(async () => {
try {
const { invoke } = await import('@tauri-apps/api/core');
const agentList = await invoke<Array<{
id: string;
name: string;
state: string;
model?: string;
provider?: string;
}>>('agent_list');
return { success: true, agents: agentList };
} catch (error) {
return { success: false, error: String(error) };
}
});
console.log('[Agent List]', agents);
if (agents.success) {
console.log('[Agents] Count:', agents.agents?.length);
agents.agents?.forEach((agent, i) => {
console.log(`[Agent ${i + 1}]`, agent);
});
}
await takeScreenshot(page, '04-agent-list');
});
});
test.describe('3. 连接状态', () => {
test('应用应该正确显示连接状态', async ({ page }) => {
await page.waitForTimeout(3000);
const connectionState = await page.evaluate(() => {
const statusElements = document.querySelectorAll('[class*="status"], [class*="connection"]');
return {
foundElements: statusElements.length,
texts: Array.from(statusElements).map((el) => el.textContent?.trim()).filter(Boolean),
};
});
console.log('[Connection State]', connectionState);
const pageText = await page.textContent('body');
console.log('[Page Text]', pageText?.substring(0, 500));
await takeScreenshot(page, '05-connection-state');
});
test('设置按钮应该可用', async ({ page }) => {
const settingsBtn = page.locator('button').filter({ hasText: /设置|Settings|⚙/i });
if (await settingsBtn.isVisible()) {
await settingsBtn.click();
await page.waitForTimeout(1000);
await takeScreenshot(page, '06-settings-access');
} else {
console.log('[Settings] Button not visible');
}
});
});
test.describe('4. UI 布局验证', () => {
test('主布局应该正确渲染', async ({ page }) => {
const layout = await page.evaluate(() => {
const app = document.querySelector('.h-screen');
const sidebar = document.querySelector('aside');
const main = document.querySelector('main');
return {
hasApp: !!app,
hasSidebar: !!sidebar,
hasMain: !!main,
appClasses: app?.className,
};
});
console.log('[Layout]', layout);
expect(layout.hasApp).toBe(true);
expect(layout.hasSidebar).toBe(true);
expect(layout.hasMain).toBe(true);
await takeScreenshot(page, '07-layout');
});
test('侧边栏导航应该存在', async ({ page }) => {
const navButtons = await page.locator('aside button').count();
console.log('[Navigation] Button count:', navButtons);
expect(navButtons).toBeGreaterThan(0);
await takeScreenshot(page, '08-navigation');
});
});
test.describe('5. 聊天功能 (Tauri 模式)', () => {
test('聊天输入框应该可用', async ({ page }) => {
const chatInput = page.locator('textarea').first();
if (await chatInput.isVisible()) {
await chatInput.fill('你好ZCLAW');
const value = await chatInput.inputValue();
console.log('[Chat Input] Value:', value);
expect(value).toBe('你好ZCLAW');
} else {
console.log('[Chat Input] Not visible - may need connection');
}
await takeScreenshot(page, '09-chat-input');
});
test('模型选择器应该可用', async ({ page }) => {
const modelSelector = page.locator('button').filter({ hasText: /模型|Model|选择/i });
if (await modelSelector.isVisible()) {
await modelSelector.click();
await page.waitForTimeout(500);
console.log('[Model Selector] Clicked');
} else {
console.log('[Model Selector] Not visible');
}
await takeScreenshot(page, '10-model-selector');
});
});
test.describe('6. 设置页面 (Tauri 模式)', () => {
test('设置页面应该能打开', async ({ page }) => {
const settingsBtn = page.getByRole('button', { name: /设置|Settings/i }).first();
if (await settingsBtn.isVisible()) {
await settingsBtn.click();
await page.waitForTimeout(1000);
const settingsContent = await page.locator('[class*="settings"]').count();
console.log('[Settings] Content elements:', settingsContent);
expect(settingsContent).toBeGreaterThan(0);
} else {
console.log('[Settings] Button not found');
}
await takeScreenshot(page, '11-settings-page');
});
test('通用设置标签应该可见', async ({ page }) => {
await page.getByRole('button', { name: /设置|Settings/i }).first().click();
await page.waitForTimeout(500);
const tabs = await page.getByRole('tab').count();
console.log('[Settings Tabs] Count:', tabs);
await takeScreenshot(page, '12-settings-tabs');
});
});
test.describe('7. 控制台日志检查', () => {
test('应该没有严重 JavaScript 错误', async ({ page }) => {
const errors: string[] = [];
page.on('pageerror', (error) => {
errors.push(error.message);
});
page.on('console', (msg) => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});
await page.waitForTimeout(3000);
const criticalErrors = errors.filter(
(e) =>
!e.includes('Warning') &&
!e.includes('DevTools') &&
!e.includes('extension') &&
!e.includes('favicon')
);
console.log('[Console Errors]', criticalErrors.length);
criticalErrors.forEach((e) => console.log(' -', e.substring(0, 200)));
await takeScreenshot(page, '13-console-errors');
});
test('Tauri 特定日志应该存在', async ({ page }) => {
const logs: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'log' || msg.type() === 'info') {
const text = msg.text();
if (text.includes('Tauri') || text.includes('Kernel') || text.includes('tauri')) {
logs.push(text);
}
}
});
await page.waitForTimeout(2000);
console.log('[Tauri Logs]', logs.length);
logs.forEach((log) => console.log(' -', log.substring(0, 200)));
await takeScreenshot(page, '14-tauri-logs');
});
});
});
test.describe('ZCLAW Tauri 设置页面测试', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
});
test('模型与 API 设置', async ({ page }) => {
await page.getByRole('button', { name: /设置|Settings/i }).first().click();
await page.waitForTimeout(1000);
const modelSettings = await page.getByText(/模型|Model|API/i).count();
console.log('[Model Settings] Found:', modelSettings);
await takeScreenshot(page, '15-model-settings');
});
test('安全设置', async ({ page }) => {
await page.getByRole('button', { name: /设置|Settings/i }).first().click();
await page.waitForTimeout(500);
const securityTab = page.getByRole('tab', { name: /安全|Security|Privacy/i });
if (await securityTab.isVisible()) {
await securityTab.click();
await page.waitForTimeout(500);
}
await takeScreenshot(page, '16-security-settings');
});
});