mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 03:31:24 +08:00
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:
parent
e0eac6ab7e
commit
bf6ccea1e2
53
src/renderer/src/utils/__tests__/getPotentialIndex.test.ts
Normal file
53
src/renderer/src/utils/__tests__/getPotentialIndex.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
182
src/renderer/src/utils/__tests__/input.test.ts
Normal file
182
src/renderer/src/utils/__tests__/input.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user