test: add unit tests for getPotentialIndex and input utils (#7947)

* test: add unit tests for getPotentialIndex and input utils

- Add tests for getPotentialIndex function covering streaming text tag detection scenarios
- Add tests for input utils including file drop and keyboard shortcut detection

* test: refactor test structure to comply with TEST_UTILS.md guidelines

- Add file-level describe blocks for both test files
- Fix mock cleanup in input.test.ts:
  - Add vi.clearAllMocks() in beforeEach
  - Replace vi.clearAllMocks() with vi.restoreAllMocks() in afterEach
- Maintain two-layer describe structure as per project standards
This commit is contained in:
Jason Young 2025-07-14 21:52:52 +08:00 committed by GitHub
parent e0eac6ab7e
commit bf6ccea1e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 235 additions and 0 deletions

View File

@ -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 <thinking>', '<thinking>')).toBe(14)
// 部分标签(流式传输中断)
expect(getPotentialStartIndex('Response with <thin', '<thinking>')).toBe(14)
expect(getPotentialStartIndex('Response with <', '<thinking>')).toBe(14)
// 多个标签场景
const text = 'Start <tag1>content</tag1> middle <tag'
expect(getPotentialStartIndex(text, '<tag2>')).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)
})
})
})

View File

@ -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<HTMLTextAreaElement>
// 多个修饰键时,任何快捷键都不应触发
expect(isSendMessageKeyPressed(multiModifierEvent, 'Enter')).toBe(false)
expect(isSendMessageKeyPressed(multiModifierEvent, 'Ctrl+Enter')).toBe(false)
expect(isSendMessageKeyPressed(multiModifierEvent, 'Shift+Enter')).toBe(false)
})
})
})