/** * Browser Hand Store Tests */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { useBrowserHandStore } from '../../desktop/src/store/browserHandStore'; import { validateTemplateParams, mergeParamsWithDefaults, } from '../../desktop/src/components/BrowserHand/templates'; import type { TaskTemplateParam } from '../../desktop/src/components/BrowserHand/templates'; // Mock the browser-client module vi.mock('../../desktop/src/lib/browser-client', () => ({ default: vi.fn().mockImplementation(() => ({ start: vi.fn().mockResolvedValue('test-session-id'), close: vi.fn().mockResolvedValue(undefined), goto: vi.fn().mockResolvedValue({ url: 'https://example.com', title: 'Test Page' }), screenshot: vi.fn().mockResolvedValue({ base64: 'test-base64', format: 'png' }), url: vi.fn().mockResolvedValue('https://example.com'), title: vi.fn().mockResolvedValue('Test Page'), click: vi.fn().mockResolvedValue(undefined), type: vi.fn().mockResolvedValue(undefined), wait: vi.fn().mockResolvedValue({}), eval: vi.fn().mockResolvedValue(null), })), createSession: vi.fn().mockResolvedValue({ session_id: 'test-session-id' }), closeSession: vi.fn().mockResolvedValue(undefined), listSessions: vi.fn().mockResolvedValue([]), })); // Mock uuid vi.mock('uuid', () => ({ v4: vi.fn().mockReturnValue('test-uuid'), })); describe('browserHandStore', () => { beforeEach(() => { // Reset store state before each test useBrowserHandStore.setState({ sessions: [], activeSessionId: null, execution: { isRunning: false, currentAction: null, currentUrl: null, lastScreenshot: null, progress: 0, startTime: null, status: 'idle', error: null, }, logs: [], templates: [], recentTasks: [], isTemplateModalOpen: false, isLoading: false, error: null, }); }); describe('initial state', () => { it('should have correct initial state', () => { const state = useBrowserHandStore.getState(); expect(state.sessions).toEqual([]); expect(state.activeSessionId).toBeNull(); expect(state.execution.isRunning).toBe(false); expect(state.logs).toEqual([]); expect(state.isTemplateModalOpen).toBe(false); expect(state.isLoading).toBe(false); expect(state.error).toBeNull(); }); }); describe('UI actions', () => { it('should open template modal', () => { const { openTemplateModal } = useBrowserHandStore.getState(); openTemplateModal(); const state = useBrowserHandStore.getState(); expect(state.isTemplateModalOpen).toBe(true); }); it('should close template modal', () => { const { openTemplateModal, closeTemplateModal } = useBrowserHandStore.getState(); openTemplateModal(); closeTemplateModal(); const state = useBrowserHandStore.getState(); expect(state.isTemplateModalOpen).toBe(false); }); it('should set loading state', () => { const { setLoading } = useBrowserHandStore.getState(); setLoading(true); const state = useBrowserHandStore.getState(); expect(state.isLoading).toBe(true); }); it('should set and clear error', () => { const { setError, clearError } = useBrowserHandStore.getState(); setError('Test error'); let state = useBrowserHandStore.getState(); expect(state.error).toBe('Test error'); clearError(); state = useBrowserHandStore.getState(); expect(state.error).toBeNull(); }); }); describe('execution state', () => { it('should update execution state', () => { const { updateExecutionState } = useBrowserHandStore.getState(); updateExecutionState({ currentAction: 'Navigating...', progress: 50, }); const state = useBrowserHandStore.getState(); expect(state.execution.currentAction).toBe('Navigating...'); expect(state.execution.progress).toBe(50); }); }); describe('logs', () => { it('should add log entries', () => { const { addLog } = useBrowserHandStore.getState(); addLog({ level: 'info', message: 'Test log' }); const state = useBrowserHandStore.getState(); expect(state.logs).toHaveLength(1); expect(state.logs[0].level).toBe('info'); expect(state.logs[0].message).toBe('Test log'); }); it('should clear logs', () => { const { addLog, clearLogs } = useBrowserHandStore.getState(); addLog({ level: 'info', message: 'Test log' }); clearLogs(); const state = useBrowserHandStore.getState(); expect(state.logs).toHaveLength(0); }); it('should limit log entries', () => { const store = useBrowserHandStore.getState(); // Add more than max logs for (let i = 0; i < 150; i++) { store.addLog({ level: 'info', message: `Log ${i}` }); } const state = useBrowserHandStore.getState(); expect(state.logs.length).toBeLessThanOrEqual(state.maxLogs); }); }); describe('recent tasks', () => { it('should add recent task', () => { const { addRecentTask } = useBrowserHandStore.getState(); addRecentTask({ templateId: 'basic_navigate_screenshot', templateName: '打开网页并截图', params: { url: 'https://example.com' }, status: 'success', duration: 5000, }); const state = useBrowserHandStore.getState(); expect(state.recentTasks).toHaveLength(1); expect(state.recentTasks[0].templateId).toBe('basic_navigate_screenshot'); }); it('should clear recent tasks', () => { const { addRecentTask, clearRecentTasks } = useBrowserHandStore.getState(); addRecentTask({ templateId: 'test', templateName: 'Test', params: {}, status: 'success', duration: 100, }); clearRecentTasks(); const state = useBrowserHandStore.getState(); expect(state.recentTasks).toHaveLength(0); }); }); }); describe('Template validation utilities', () => { describe('validateTemplateParams', () => { it('should validate required params', () => { const params: TaskTemplateParam[] = [ { key: 'url', label: 'URL', type: 'url', required: true }, { key: 'name', label: 'Name', type: 'text', required: false }, ]; // Missing required param let result = validateTemplateParams(params, {}); expect(result.valid).toBe(false); expect(result.errors).toHaveLength(1); // All params provided result = validateTemplateParams(params, { url: 'https://example.com' }); expect(result.valid).toBe(true); }); it('should validate URL type', () => { const params: TaskTemplateParam[] = [ { key: 'url', label: 'URL', type: 'url', required: true }, ]; // Invalid URL let result = validateTemplateParams(params, { url: 'not-a-url' }); expect(result.valid).toBe(false); // Valid URL result = validateTemplateParams(params, { url: 'https://example.com' }); expect(result.valid).toBe(true); }); it('should validate number type with min/max', () => { const params: TaskTemplateParam[] = [ { key: 'count', label: 'Count', type: 'number', required: true, min: 1, max: 10 }, ]; // Below min let result = validateTemplateParams(params, { count: 0 }); expect(result.valid).toBe(false); // Above max result = validateTemplateParams(params, { count: 20 }); expect(result.valid).toBe(false); // Valid result = validateTemplateParams(params, { count: 5 }); expect(result.valid).toBe(true); }); }); describe('mergeParamsWithDefaults', () => { it('should merge with default values', () => { const params: TaskTemplateParam[] = [ { key: 'url', label: 'URL', type: 'url', required: true }, { key: 'timeout', label: 'Timeout', type: 'number', required: false, default: 5000 }, { key: 'headless', label: 'Headless', type: 'boolean', required: false, default: true }, ]; const merged = mergeParamsWithDefaults(params, { url: 'https://example.com' }); expect(merged.url).toBe('https://example.com'); expect(merged.timeout).toBe(5000); expect(merged.headless).toBe(true); }); it('should override defaults with provided values', () => { const params: TaskTemplateParam[] = [ { key: 'url', label: 'URL', type: 'url', required: true }, { key: 'timeout', label: 'Timeout', type: 'number', required: false, default: 5000 }, ]; const merged = mergeParamsWithDefaults(params, { url: 'https://example.com', timeout: 10000 }); expect(merged.url).toBe('https://example.com'); expect(merged.timeout).toBe(10000); }); }); });