diff --git a/src/renderer/src/utils/__tests__/getPotentialIndex.test.ts b/src/renderer/src/utils/__tests__/getPotentialIndex.test.ts new file mode 100644 index 0000000000..58437488a3 --- /dev/null +++ b/src/renderer/src/utils/__tests__/getPotentialIndex.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest' + +import { getPotentialStartIndex } from '../getPotentialIndex' + +describe('getPotentialIndex', () => { + describe('getPotentialStartIndex', () => { + // 核心功能:直接匹配 + it('should return the index of exact match', () => { + expect(getPotentialStartIndex('Hello world', 'world')).toBe(6) + expect(getPotentialStartIndex('Hello world world', 'world')).toBe(6) // 返回第一个匹配 + }) + + // 核心功能:后缀-前缀匹配(流式文本的关键场景) + it('should return index when text suffix matches search prefix', () => { + expect(getPotentialStartIndex('Hello wo', 'world')).toBe(6) + expect(getPotentialStartIndex('Hello w', 'world')).toBe(6) + expect(getPotentialStartIndex('I am thinking', 'thinking about')).toBe(5) + }) + + // 边界情况:空字符串 + it('should return null when searchedText is empty', () => { + expect(getPotentialStartIndex('Hello', '')).toBe(null) + expect(getPotentialStartIndex('', '')).toBe(null) + }) + + // 边界情况:无匹配 + it('should return null when no match is found', () => { + expect(getPotentialStartIndex('Hello', 'world')).toBe(null) + expect(getPotentialStartIndex('', 'world')).toBe(null) + }) + + // 流式文本实际场景:标签检测 + it('should handle tag detection in streaming response', () => { + // 完整标签 + expect(getPotentialStartIndex('Response with ', '')).toBe(14) + + // 部分标签(流式传输中断) + expect(getPotentialStartIndex('Response with ')).toBe(14) + expect(getPotentialStartIndex('Response with <', '')).toBe(14) + + // 多个标签场景 + const text = 'Start content middle ')).toBe(34) + }) + + // 特殊字符处理 + it('should handle special characters correctly', () => { + expect(getPotentialStartIndex('Hello\nworld', 'world')).toBe(6) + expect(getPotentialStartIndex('Hello\n', '\nworld')).toBe(5) + expect(getPotentialStartIndex('Test 中文', '中文测试')).toBe(5) + }) + }) +}) diff --git a/src/renderer/src/utils/__tests__/input.test.ts b/src/renderer/src/utils/__tests__/input.test.ts new file mode 100644 index 0000000000..0eb697d36b --- /dev/null +++ b/src/renderer/src/utils/__tests__/input.test.ts @@ -0,0 +1,182 @@ +import type { SendMessageShortcut } from '@renderer/store/settings' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { getFilesFromDropEvent, getSendMessageShortcutLabel, isSendMessageKeyPressed } from '../input' + +// Mock 外部依赖 +vi.mock('@renderer/config/logger', () => ({ + default: { error: vi.fn() } +})) + +vi.mock('@renderer/config/constant', () => ({ + isMac: false, + isWin: true +})) + +// Mock window.api +const mockGetPathForFile = vi.fn() +const mockFileGet = vi.fn() + +describe('input', () => { + beforeEach(() => { + vi.clearAllMocks() + // 设置 window.api mock + global.window = { + api: { + file: { + getPathForFile: mockGetPathForFile, + get: mockFileGet + } + } + } as any + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe('getFilesFromDropEvent', () => { + // 核心功能:处理文件拖放 + it('should handle file drop with File objects', async () => { + const mockFile1 = new File(['content1'], 'file1.txt') + const mockFile2 = new File(['content2'], 'file2.txt') + const mockMetadata1 = { id: '1', name: 'file1.txt', path: '/path/file1.txt' } + const mockMetadata2 = { id: '2', name: 'file2.txt', path: '/path/file2.txt' } + + mockGetPathForFile.mockImplementation((file) => { + if (file === mockFile1) return '/path/file1.txt' + if (file === mockFile2) return '/path/file2.txt' + return null + }) + + mockFileGet.mockImplementation((path) => { + if (path === '/path/file1.txt') return mockMetadata1 + if (path === '/path/file2.txt') return mockMetadata2 + return null + }) + + const event = { + dataTransfer: { + files: [mockFile1, mockFile2], + items: [] + } + } as any + + const result = await getFilesFromDropEvent(event) + expect(result).toEqual([mockMetadata1, mockMetadata2]) + expect(mockGetPathForFile).toHaveBeenCalledTimes(2) + expect(mockFileGet).toHaveBeenCalledTimes(2) + }) + + // 处理 codefiles 格式 + it('should handle codefiles format from drag event', async () => { + const mockMetadata = { id: '1', name: 'file.txt', path: '/path/file.txt' } + mockFileGet.mockResolvedValue(mockMetadata) + + const mockGetAsString = vi.fn((callback) => { + callback(JSON.stringify(['/path/file.txt'])) + }) + + const event = { + dataTransfer: { + files: [], + items: [ + { + type: 'codefiles', + getAsString: mockGetAsString + } + ] + } + } as any + + const result = await getFilesFromDropEvent(event) + expect(result).toEqual([mockMetadata]) + expect(mockGetAsString).toHaveBeenCalled() + }) + + // 边界情况:空文件列表 + it('should return empty array when no files are dropped', async () => { + const event = { + dataTransfer: { + files: [], + items: [] + } + } as any + + const result = await getFilesFromDropEvent(event) + expect(result).toEqual([]) + }) + + // 错误处理 + it('should handle errors gracefully when file path cannot be obtained', async () => { + const mockFile = new File(['content'], 'file.txt') + mockGetPathForFile.mockImplementation(() => { + throw new Error('Path error') + }) + + const event = { + dataTransfer: { + files: [mockFile], + items: [] + } + } as any + + const result = await getFilesFromDropEvent(event) + expect(result).toEqual([]) + }) + }) + + describe('getSendMessageShortcutLabel', () => { + // 核心功能:快捷键标签转换 + it('should return correct labels for shortcuts in Windows environment', () => { + expect(getSendMessageShortcutLabel('Enter')).toBe('Enter') + expect(getSendMessageShortcutLabel('Ctrl+Enter')).toBe('Ctrl + Enter') + expect(getSendMessageShortcutLabel('Command+Enter')).toBe('Win + Enter') // Windows 环境特殊处理 + expect(getSendMessageShortcutLabel('Custom+Enter' as SendMessageShortcut)).toBe('Custom+Enter') // 未知快捷键保持原样 + }) + }) + + describe('isSendMessageKeyPressed', () => { + // 核心功能:检测正确的快捷键组合 + it('should correctly detect each shortcut combination', () => { + // 单独 Enter 键 + expect( + isSendMessageKeyPressed( + { key: 'Enter', shiftKey: false, ctrlKey: false, metaKey: false, altKey: false } as any, + 'Enter' + ) + ).toBe(true) + + // 组合键 - 每个快捷键只需一个有效案例 + expect( + isSendMessageKeyPressed( + { key: 'Enter', shiftKey: false, ctrlKey: true, metaKey: false, altKey: false } as any, + 'Ctrl+Enter' + ) + ).toBe(true) + + expect( + isSendMessageKeyPressed( + { key: 'Enter', shiftKey: false, ctrlKey: false, metaKey: true, altKey: false } as any, + 'Command+Enter' + ) + ).toBe(true) + }) + + // 边界情况:确保快捷键互斥 + it('should require exact modifier key combination', () => { + const multiModifierEvent = { + key: 'Enter', + shiftKey: true, + ctrlKey: true, + metaKey: false, + altKey: false + } as React.KeyboardEvent + + // 多个修饰键时,任何快捷键都不应触发 + expect(isSendMessageKeyPressed(multiModifierEvent, 'Enter')).toBe(false) + expect(isSendMessageKeyPressed(multiModifierEvent, 'Ctrl+Enter')).toBe(false) + expect(isSendMessageKeyPressed(multiModifierEvent, 'Shift+Enter')).toBe(false) + }) + }) +})