test: add missing tests

This commit is contained in:
Louis 2025-07-12 21:29:51 +07:00
parent c5fd964bf2
commit 864ad50880
No known key found for this signature in database
GPG Key ID: 44FA9F4D33C37DE2
8 changed files with 1361 additions and 0 deletions

View File

@ -0,0 +1,79 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
// Mock ReactDOM
const mockRender = vi.fn()
const mockCreateRoot = vi.fn().mockReturnValue({ render: mockRender })
vi.mock('react-dom/client', () => ({
default: {
createRoot: mockCreateRoot,
},
createRoot: mockCreateRoot,
}))
// Mock router
vi.mock('@tanstack/react-router', () => ({
RouterProvider: ({ router }: { router: any }) => `<RouterProvider router={router} />`,
createRouter: vi.fn().mockReturnValue('mocked-router'),
createRootRoute: vi.fn(),
}))
// Mock route tree
vi.mock('../routeTree.gen', () => ({
routeTree: 'mocked-route-tree',
}))
// Mock CSS imports
vi.mock('../index.css', () => ({}))
vi.mock('../i18n', () => ({}))
describe('main.tsx', () => {
let mockGetElementById: any
let mockRootElement: any
beforeEach(() => {
mockRootElement = {
innerHTML: '',
}
mockGetElementById = vi.fn().mockReturnValue(mockRootElement)
Object.defineProperty(document, 'getElementById', {
value: mockGetElementById,
writable: true,
})
// Clear all mocks
vi.clearAllMocks()
})
afterEach(() => {
vi.resetModules()
})
it('should render app when root element is empty', async () => {
mockRootElement.innerHTML = ''
await import('../main')
expect(mockGetElementById).toHaveBeenCalledWith('root')
expect(mockCreateRoot).toHaveBeenCalledWith(mockRootElement)
expect(mockRender).toHaveBeenCalled()
})
it('should not render app when root element already has content', async () => {
mockRootElement.innerHTML = '<div>existing content</div>'
await import('../main')
expect(mockGetElementById).toHaveBeenCalledWith('root')
expect(mockCreateRoot).not.toHaveBeenCalled()
expect(mockRender).not.toHaveBeenCalled()
})
it('should throw error when root element is not found', async () => {
mockGetElementById.mockReturnValue(null)
await expect(async () => {
await import('../main')
}).rejects.toThrow()
})
})

View File

@ -0,0 +1,231 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen } from '@testing-library/react'
import LeftPanel from '../LeftPanel'
import { useLeftPanel } from '@/hooks/useLeftPanel'
// Mock global constants
Object.defineProperty(global, 'IS_WINDOWS', { value: false, writable: true })
Object.defineProperty(global, 'IS_LINUX', { value: false, writable: true })
Object.defineProperty(global, 'IS_TAURI', { value: false, writable: true })
Object.defineProperty(global, 'IS_MACOS', { value: false, writable: true })
// Mock all dependencies
vi.mock('@tanstack/react-router', () => ({
Link: ({ to, children, className }: any) => (
<a href={to} className={className} data-testid={`link-${to}`}>
{children}
</a>
),
useNavigate: () => vi.fn(),
useRouterState: vi.fn((options) => {
if (options && options.select) {
return options.select({ location: { pathname: '/' } })
}
return { location: { pathname: '/' } }
}),
}))
vi.mock('@/hooks/useLeftPanel', () => ({
useLeftPanel: vi.fn(() => ({
open: true,
setLeftPanel: vi.fn(),
toggle: vi.fn(),
close: vi.fn(),
})),
}))
vi.mock('@/hooks/useThreads', () => ({
useThreads: vi.fn(() => ({
threads: [],
searchTerm: '',
setSearchTerm: vi.fn(),
deleteThread: vi.fn(),
deleteAllThreads: vi.fn(),
unstarAllThreads: vi.fn(),
clearThreads: vi.fn(),
getFilteredThreads: vi.fn(() => []),
filteredThreads: [],
currentThreadId: null,
})),
}))
vi.mock('@/hooks/useMediaQuery', () => ({
useSmallScreen: vi.fn(() => false),
}))
vi.mock('@/hooks/useClickOutside', () => ({
useClickOutside: () => null,
}))
vi.mock('./ThreadList', () => ({
default: () => <div data-testid="thread-list">ThreadList</div>,
}))
vi.mock('@/containers/DownloadManegement', () => ({
DownloadManagement: () => <div data-testid="download-management">DownloadManagement</div>,
}))
vi.mock('@/i18n/react-i18next-compat', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
vi.mock('@/hooks/useEvent', () => ({
useEvent: () => ({
on: vi.fn(),
off: vi.fn(),
}),
}))
// Mock the store
vi.mock('@/store/useAppState', () => ({
useAppState: () => ({
setLeftPanel: vi.fn(),
}),
}))
// Mock route constants
vi.mock('@/constants/routes', () => ({
route: {
home: '/',
assistant: '/assistant',
hub: {
index: '/hub',
},
settings: {
general: '/settings',
index: '/settings',
},
},
}))
describe('LeftPanel', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should render when panel is open', () => {
vi.mocked(useLeftPanel).mockReturnValue({
open: true,
setLeftPanel: vi.fn(),
toggle: vi.fn(),
close: vi.fn(),
})
render(<LeftPanel />)
// Check that the panel is rendered (it should contain some basic elements)
expect(screen.getByPlaceholderText('common:search')).toBeDefined()
})
it('should hide panel when closed', () => {
vi.mocked(useLeftPanel).mockReturnValue({
open: false,
setLeftPanel: vi.fn(),
toggle: vi.fn(),
close: vi.fn(),
})
render(<LeftPanel />)
// When closed, panel should have hidden styling
const panel = document.querySelector('aside')
expect(panel).not.toBeNull()
expect(panel?.className).toContain('visibility-hidden')
})
it('should render main menu items', () => {
vi.mocked(useLeftPanel).mockReturnValue({
open: true,
setLeftPanel: vi.fn(),
toggle: vi.fn(),
close: vi.fn(),
})
render(<LeftPanel />)
expect(screen.getByText('common:newChat')).toBeDefined()
expect(screen.getByText('common:assistants')).toBeDefined()
expect(screen.getByText('common:hub')).toBeDefined()
expect(screen.getByText('common:settings')).toBeDefined()
})
it('should render search input', () => {
vi.mocked(useLeftPanel).mockReturnValue({
open: true,
setLeftPanel: vi.fn(),
toggle: vi.fn(),
close: vi.fn(),
})
render(<LeftPanel />)
const searchInput = screen.getByPlaceholderText('common:search')
expect(searchInput).toBeDefined()
expect(searchInput).toHaveAttribute('type', 'text')
})
it('should render download management component', () => {
vi.mocked(useLeftPanel).mockReturnValue({
open: true,
setLeftPanel: vi.fn(),
toggle: vi.fn(),
close: vi.fn(),
})
render(<LeftPanel />)
expect(screen.getByTestId('download-management')).toBeDefined()
})
it('should have proper structure when open', () => {
vi.mocked(useLeftPanel).mockReturnValue({
open: true,
setLeftPanel: vi.fn(),
toggle: vi.fn(),
close: vi.fn(),
})
render(<LeftPanel />)
// Check that basic structure exists
const searchInput = screen.getByPlaceholderText('common:search')
expect(searchInput).toBeDefined()
const downloadComponent = screen.getByTestId('download-management')
expect(downloadComponent).toBeDefined()
})
it('should render menu navigation links', () => {
vi.mocked(useLeftPanel).mockReturnValue({
open: true,
setLeftPanel: vi.fn(),
toggle: vi.fn(),
close: vi.fn(),
})
render(<LeftPanel />)
// Check for navigation elements
expect(screen.getByText('common:newChat')).toBeDefined()
expect(screen.getByText('common:assistants')).toBeDefined()
expect(screen.getByText('common:hub')).toBeDefined()
expect(screen.getByText('common:settings')).toBeDefined()
})
it('should have sidebar toggle functionality', () => {
vi.mocked(useLeftPanel).mockReturnValue({
open: true,
setLeftPanel: vi.fn(),
toggle: vi.fn(),
close: vi.fn(),
})
render(<LeftPanel />)
// Check that the sidebar toggle icon is present by looking for the IconLayoutSidebar
const toggleButton = document.querySelector('svg.tabler-icon-layout-sidebar')
expect(toggleButton).not.toBeNull()
})
})

View File

@ -0,0 +1,172 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { useAppearance } from '../useAppearance'
// Mock constants
vi.mock('@/constants/localStorage', () => ({
localStorageKey: {
appearance: 'appearance',
},
}))
vi.mock('../useTheme', () => ({
useTheme: {
getState: vi.fn(() => ({ isDark: false })),
setState: vi.fn(),
subscribe: vi.fn(),
destroy: vi.fn(),
},
}))
// Mock zustand persist
vi.mock('zustand/middleware', () => ({
persist: (fn: any) => fn,
createJSONStorage: () => ({
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
}),
}))
// Mock global constants
Object.defineProperty(global, 'IS_WINDOWS', { value: false, writable: true })
Object.defineProperty(global, 'IS_LINUX', { value: false, writable: true })
Object.defineProperty(global, 'IS_TAURI', { value: false, writable: true })
describe('useAppearance', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should initialize with default values', () => {
const { result } = renderHook(() => useAppearance())
expect(result.current.fontSize).toBe('15px')
expect(result.current.chatWidth).toBe('compact')
expect(result.current.appBgColor).toEqual({
r: 25,
g: 25,
b: 25,
a: 1,
})
})
it('should update font size', () => {
const { result } = renderHook(() => useAppearance())
act(() => {
result.current.setFontSize('18px')
})
expect(result.current.fontSize).toBe('18px')
})
it('should update chat width', () => {
const { result } = renderHook(() => useAppearance())
act(() => {
result.current.setChatWidth('full')
})
expect(result.current.chatWidth).toBe('full')
})
it('should update app background color', () => {
const { result } = renderHook(() => useAppearance())
const newColor = { r: 100, g: 100, b: 100, a: 1 }
act(() => {
result.current.setAppBgColor(newColor)
})
expect(result.current.appBgColor).toEqual(newColor)
})
it('should update main view background color', () => {
const { result } = renderHook(() => useAppearance())
const newColor = { r: 200, g: 200, b: 200, a: 1 }
act(() => {
result.current.setAppMainViewBgColor(newColor)
})
expect(result.current.appMainViewBgColor).toEqual(newColor)
})
it('should update primary background color', () => {
const { result } = renderHook(() => useAppearance())
const newColor = { r: 50, g: 100, b: 150, a: 1 }
act(() => {
result.current.setAppPrimaryBgColor(newColor)
})
expect(result.current.appPrimaryBgColor).toEqual(newColor)
})
it('should update accent background color', () => {
const { result } = renderHook(() => useAppearance())
const newColor = { r: 255, g: 100, b: 50, a: 1 }
act(() => {
result.current.setAppAccentBgColor(newColor)
})
expect(result.current.appAccentBgColor).toEqual(newColor)
})
it('should update destructive background color', () => {
const { result } = renderHook(() => useAppearance())
const newColor = { r: 255, g: 0, b: 0, a: 1 }
act(() => {
result.current.setAppDestructiveBgColor(newColor)
})
expect(result.current.appDestructiveBgColor).toEqual(newColor)
})
it('should reset appearance to defaults', () => {
const { result } = renderHook(() => useAppearance())
// Change some values first
act(() => {
result.current.setFontSize('18px')
result.current.setChatWidth('full')
result.current.setAppBgColor({ r: 100, g: 100, b: 100, a: 1 })
})
// Reset
act(() => {
result.current.resetAppearance()
})
expect(result.current.fontSize).toBe('15px')
// Note: resetAppearance doesn't reset chatWidth, only visual properties
expect(result.current.chatWidth).toBe('full')
expect(result.current.appBgColor).toEqual({
r: 255,
g: 255,
b: 255,
a: 1,
})
})
it('should have correct text colors for contrast', () => {
const { result } = renderHook(() => useAppearance())
// Light background should have dark text
act(() => {
result.current.setAppMainViewBgColor({ r: 255, g: 255, b: 255, a: 1 })
})
expect(result.current.appMainViewTextColor).toBe('#000')
// Dark background should have light text
act(() => {
result.current.setAppMainViewBgColor({ r: 0, g: 0, b: 0, a: 1 })
})
expect(result.current.appMainViewTextColor).toBe('#FFF')
})
})

View File

@ -0,0 +1,264 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { useHardware } from '../useHardware'
// Mock zustand persist
vi.mock('zustand/middleware', () => ({
persist: (fn: any) => fn,
createJSONStorage: () => ({
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
}),
}))
describe('useHardware', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should initialize with default hardware state', () => {
const { result } = renderHook(() => useHardware())
expect(result.current.hardwareData).toEqual({
cpu: {
arch: '',
core_count: 0,
extensions: [],
name: '',
usage: 0,
},
gpus: [],
os_type: '',
os_name: '',
total_memory: 0,
})
expect(result.current.systemUsage).toEqual({
cpu: 0,
used_memory: 0,
total_memory: 0,
gpus: [],
})
expect(result.current.gpuLoading).toEqual({})
expect(result.current.pollingPaused).toBe(false)
})
it('should set hardware data', () => {
const { result } = renderHook(() => useHardware())
const testHardwareData = {
cpu: {
arch: 'x86_64',
core_count: 8,
extensions: ['SSE', 'AVX'],
name: 'Intel Core i7',
usage: 25.5,
},
gpus: [
{
name: 'NVIDIA RTX 3080',
total_memory: 10737418240,
vendor: 'NVIDIA',
uuid: 'GPU-12345',
driver_version: '470.57.02',
activated: true,
nvidia_info: {
index: 0,
compute_capability: '8.6',
},
vulkan_info: {
index: 0,
device_id: 8704,
device_type: 'discrete',
api_version: '1.2.0',
},
},
],
os_type: 'linux',
os_name: 'Ubuntu',
total_memory: 17179869184,
}
act(() => {
result.current.setHardwareData(testHardwareData)
})
expect(result.current.hardwareData).toEqual(testHardwareData)
})
it('should set CPU data', () => {
const { result } = renderHook(() => useHardware())
const testCPU = {
arch: 'x86_64',
core_count: 8,
extensions: ['SSE', 'AVX'],
name: 'Intel Core i7',
usage: 25.5,
}
act(() => {
result.current.setCPU(testCPU)
})
expect(result.current.hardwareData.cpu).toEqual(testCPU)
})
it('should set GPUs data', () => {
const { result } = renderHook(() => useHardware())
const testGPUs = [
{
name: 'NVIDIA RTX 3080',
total_memory: 10737418240,
vendor: 'NVIDIA',
uuid: 'GPU-12345',
driver_version: '470.57.02',
activated: true,
nvidia_info: {
index: 0,
compute_capability: '8.6',
},
vulkan_info: {
index: 0,
device_id: 8704,
device_type: 'discrete',
api_version: '1.2.0',
},
},
]
act(() => {
result.current.setGPUs(testGPUs)
})
expect(result.current.hardwareData.gpus).toEqual(testGPUs)
})
it('should update system usage', () => {
const { result } = renderHook(() => useHardware())
const testSystemUsage = {
cpu: 45.2,
used_memory: 8589934592,
total_memory: 17179869184,
gpus: [
{
uuid: 'GPU-12345',
used_memory: 2147483648,
total_memory: 10737418240,
},
],
}
act(() => {
result.current.updateSystemUsage(testSystemUsage)
})
expect(result.current.systemUsage).toEqual(testSystemUsage)
})
it('should manage GPU loading state', () => {
const { result } = renderHook(() => useHardware())
// First set up some GPU data so we have a UUID to work with
const testGPUs = [
{
name: 'NVIDIA RTX 3080',
total_memory: 10737418240,
vendor: 'NVIDIA',
uuid: 'GPU-12345',
driver_version: '470.57.02',
activated: true,
nvidia_info: {
index: 0,
compute_capability: '8.6',
},
vulkan_info: {
index: 0,
device_id: 8704,
device_type: 'discrete',
api_version: '1.2.0',
},
},
]
act(() => {
result.current.setGPUs(testGPUs)
})
act(() => {
result.current.setGpuLoading(0, true)
})
expect(result.current.gpuLoading['GPU-12345']).toBe(true)
act(() => {
result.current.setGpuLoading(0, false)
})
expect(result.current.gpuLoading['GPU-12345']).toBe(false)
})
it('should manage polling state', () => {
const { result } = renderHook(() => useHardware())
expect(result.current.pollingPaused).toBe(false)
act(() => {
result.current.pausePolling()
})
expect(result.current.pollingPaused).toBe(true)
act(() => {
result.current.resumePolling()
})
expect(result.current.pollingPaused).toBe(false)
})
it('should get activated device string', () => {
const { result } = renderHook(() => useHardware())
const testHardwareData = {
cpu: {
arch: 'x86_64',
core_count: 8,
extensions: ['SSE', 'AVX'],
name: 'Intel Core i7',
usage: 25.5,
},
gpus: [
{
name: 'NVIDIA RTX 3080',
total_memory: 10737418240,
vendor: 'NVIDIA',
uuid: 'GPU-12345',
driver_version: '470.57.02',
activated: true,
nvidia_info: {
index: 0,
compute_capability: '8.6',
},
vulkan_info: {
index: 0,
device_id: 8704,
device_type: 'discrete',
api_version: '1.2.0',
},
},
],
os_type: 'linux',
os_name: 'Ubuntu',
total_memory: 17179869184,
}
act(() => {
result.current.setHardwareData(testHardwareData)
})
const deviceString = result.current.getActivatedDeviceString()
expect(typeof deviceString).toBe('string')
})
})

View File

@ -0,0 +1,190 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import {
newUserThreadContent,
newAssistantThreadContent,
emptyThreadContent,
sendCompletion,
isCompletionResponse,
stopModel,
normalizeTools,
extractToolCall,
postMessageProcessing
} from '../completion'
// Mock dependencies
vi.mock('@janhq/core', () => ({
ContentType: {
Text: 'text',
Image: 'image',
},
ChatCompletionRole: {
User: 'user',
Assistant: 'assistant',
System: 'system',
Tool: 'tool',
},
MessageStatus: {
Pending: 'pending',
Ready: 'ready',
Completed: 'completed',
},
EngineManager: {},
ModelManager: {},
chatCompletionRequestMessage: vi.fn(),
chatCompletion: vi.fn(),
chatCompletionChunk: vi.fn(),
}))
vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn(),
}))
vi.mock('@tauri-apps/plugin-http', () => ({
fetch: vi.fn(),
}))
vi.mock('token.js', () => ({
models: {},
TokenJS: class MockTokenJS {},
}))
vi.mock('ulidx', () => ({
ulid: () => 'test-ulid-123',
}))
vi.mock('../messages', () => ({
CompletionMessagesBuilder: class MockCompletionMessagesBuilder {
constructor() {}
build() {
return []
}
addMessage() {
return this
}
},
}))
vi.mock('@/services/mcp', () => ({
callTool: vi.fn(),
}))
vi.mock('../extension', () => ({
ExtensionManager: {},
}))
describe('completion.ts', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('newUserThreadContent', () => {
it('should create user thread content', () => {
const result = newUserThreadContent('thread-123', 'Hello world')
expect(result.type).toBe('text')
expect(result.role).toBe('user')
expect(result.thread_id).toBe('thread-123')
expect(result.content).toEqual([{
type: 'text',
text: {
value: 'Hello world',
annotations: [],
},
}])
})
it('should handle empty text', () => {
const result = newUserThreadContent('thread-123', '')
expect(result.type).toBe('text')
expect(result.role).toBe('user')
expect(result.content).toEqual([{
type: 'text',
text: {
value: '',
annotations: [],
},
}])
})
})
describe('newAssistantThreadContent', () => {
it('should create assistant thread content', () => {
const result = newAssistantThreadContent('thread-123', 'AI response')
expect(result.type).toBe('text')
expect(result.role).toBe('assistant')
expect(result.thread_id).toBe('thread-123')
expect(result.content).toEqual([{
type: 'text',
text: {
value: 'AI response',
annotations: [],
},
}])
})
})
describe('emptyThreadContent', () => {
it('should have correct structure', () => {
expect(emptyThreadContent).toBeDefined()
expect(emptyThreadContent.id).toBeDefined()
expect(emptyThreadContent.role).toBe('assistant')
expect(emptyThreadContent.content).toEqual([])
})
})
describe('isCompletionResponse', () => {
it('should identify completion response', () => {
const response = { choices: [] }
const result = isCompletionResponse(response)
expect(typeof result).toBe('boolean')
})
})
describe('normalizeTools', () => {
it('should normalize tools array', () => {
const tools = [{ type: 'function', function: { name: 'test' } }]
const result = normalizeTools(tools)
expect(Array.isArray(result)).toBe(true)
})
it('should handle empty array', () => {
const result = normalizeTools([])
expect(result).toBeUndefined()
})
})
describe('extractToolCall', () => {
it('should extract tool calls from message', () => {
const message = {
choices: [{
delta: {
tool_calls: [{
id: 'call_1',
type: 'function',
index: 0,
function: { name: 'test', arguments: '{}' }
}]
}
}]
}
const calls = []
const result = extractToolCall(message, null, calls)
expect(Array.isArray(result)).toBe(true)
expect(result.length).toBe(1)
})
it('should handle message without tool calls', () => {
const message = {
choices: [{
delta: {}
}]
}
const calls = []
const result = extractToolCall(message, null, calls)
expect(Array.isArray(result)).toBe(true)
expect(result.length).toBe(0)
})
})
})

View File

@ -0,0 +1,141 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { Extension, ExtensionManager } from '../extension'
// Mock dependencies
vi.mock('@janhq/core', () => ({
AIEngine: class MockAIEngine {},
BaseExtension: class MockBaseExtension {},
ExtensionTypeEnum: {
SystemMonitor: 'system-monitor',
Model: 'model',
Assistant: 'assistant',
},
}))
vi.mock('@tauri-apps/api/core', () => ({
convertFileSrc: vi.fn((path) => `asset://${path}`),
invoke: vi.fn(),
}))
// Mock window.core.extensionManager
Object.defineProperty(window, 'core', {
writable: true,
value: {
extensionManager: null,
},
})
describe('extension.ts', () => {
beforeEach(() => {
vi.clearAllMocks()
// Reset the singleton for each test
window.core.extensionManager = null
})
describe('Extension class', () => {
it('should create extension with required parameters', () => {
const extension = new Extension(
'https://example.com/extension.js',
'test-extension'
)
expect(extension.name).toBe('test-extension')
expect(extension.url).toBe('https://example.com/extension.js')
expect(extension.productName).toBeUndefined()
expect(extension.active).toBeUndefined()
expect(extension.description).toBeUndefined()
expect(extension.version).toBeUndefined()
})
it('should create extension with all parameters', () => {
const extension = new Extension(
'https://example.com/extension.js',
'test-extension',
'Test Extension',
true,
'A test extension',
'1.0.0'
)
expect(extension.name).toBe('test-extension')
expect(extension.url).toBe('https://example.com/extension.js')
expect(extension.productName).toBe('Test Extension')
expect(extension.active).toBe(true)
expect(extension.description).toBe('A test extension')
expect(extension.version).toBe('1.0.0')
})
it('should handle optional parameters as undefined', () => {
const extension = new Extension(
'https://example.com/extension.js',
'test-extension',
undefined,
undefined,
undefined,
undefined
)
expect(extension.productName).toBeUndefined()
expect(extension.active).toBeUndefined()
expect(extension.description).toBeUndefined()
expect(extension.version).toBeUndefined()
})
})
describe('ExtensionManager', () => {
let manager: ExtensionManager
beforeEach(() => {
// Reset the singleton for each test
window.core.extensionManager = null
manager = ExtensionManager.getInstance()
})
it('should be defined', () => {
expect(ExtensionManager).toBeDefined()
})
it('should have required methods', () => {
expect(typeof manager.get).toBe('function')
expect(typeof manager.getAll).toBe('function')
expect(typeof manager.load).toBe('function')
expect(typeof manager.unload).toBe('function')
})
it('should initialize extension manager', async () => {
await expect(manager.load()).resolves.not.toThrow()
})
it('should get all extensions', () => {
const extensions = manager.getAll()
expect(Array.isArray(extensions)).toBe(true)
})
it('should get extension by name', () => {
const extension = manager.getByName('non-existent')
expect(extension).toBeUndefined()
})
it('should handle unloading extensions', () => {
expect(() => manager.unload()).not.toThrow()
})
})
describe('Extension loading', () => {
it('should convert file source correctly', async () => {
const { convertFileSrc } = await import('@tauri-apps/api/core')
convertFileSrc('/path/to/extension.js')
expect(convertFileSrc).toHaveBeenCalledWith('/path/to/extension.js')
})
it('should invoke tauri commands', async () => {
const { invoke } = await import('@tauri-apps/api/core')
vi.mocked(invoke).mockResolvedValue('success')
await invoke('test_command', { param: 'value' })
expect(invoke).toHaveBeenCalledWith('test_command', { param: 'value' })
})
})
})

View File

@ -0,0 +1,126 @@
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import { Route } from '../__root'
// Mock all dependencies
vi.mock('@/containers/LeftPanel', () => ({
default: () => <div data-testid="left-panel">LeftPanel</div>,
}))
vi.mock('@/containers/dialogs/AppUpdater', () => ({
default: () => <div data-testid="app-updater">AppUpdater</div>,
}))
vi.mock('@/containers/dialogs/CortexFailureDialog', () => ({
CortexFailureDialog: () => <div data-testid="cortex-failure">CortexFailure</div>,
}))
vi.mock('@/providers/AppearanceProvider', () => ({
AppearanceProvider: ({ children }: { children: React.ReactNode }) => children,
}))
vi.mock('@/providers/ThemeProvider', () => ({
ThemeProvider: ({ children }: { children: React.ReactNode }) => children,
}))
vi.mock('@/providers/KeyboardShortcuts', () => ({
KeyboardShortcutsProvider: ({ children }: { children: React.ReactNode }) => children,
}))
vi.mock('@/providers/DataProvider', () => ({
DataProvider: ({ children }: { children: React.ReactNode }) => children,
}))
vi.mock('@/providers/ExtensionProvider', () => ({
ExtensionProvider: ({ children }: { children: React.ReactNode }) => children,
}))
vi.mock('@/providers/ToasterProvider', () => ({
ToasterProvider: ({ children }: { children: React.ReactNode }) => children,
}))
vi.mock('@/providers/AnalyticProvider', () => ({
AnalyticProvider: () => <div data-testid="analytic-provider">AnalyticProvider</div>,
}))
vi.mock('@/i18n/TranslationContext', () => ({
TranslationProvider: ({ children }: { children: React.ReactNode }) => children,
}))
vi.mock('@/hooks/useAnalytic', () => ({
useAnalytic: vi.fn(() => ({ productAnalyticPrompt: false })),
}))
vi.mock('@/hooks/useLeftPanel', () => ({
useLeftPanel: () => ({ open: true }),
}))
vi.mock('@/containers/analytics/PromptAnalytic', () => ({
PromptAnalytic: () => <div data-testid="prompt-analytic">PromptAnalytic</div>,
}))
vi.mock('@/containers/dialogs/ToolApproval', () => ({
default: () => <div data-testid="tool-approval">ToolApproval</div>,
}))
vi.mock('@/containers/dialogs/OutOfContextDialog', () => ({
default: () => <div data-testid="out-of-context">OutOfContext</div>,
}))
// Mock Outlet from react-router
vi.mock('@tanstack/react-router', () => ({
createRootRoute: (config: any) => ({ component: config.component }),
Outlet: () => <div data-testid="outlet">Outlet</div>,
useRouterState: vi.fn(() => ({
location: { pathname: '/normal-route' },
})),
}))
vi.mock('@/constants/routes', () => ({
route: {
localApiServerlogs: '/local-api-server/logs',
systemMonitor: '/system-monitor',
appLogs: '/logs',
},
}))
vi.mock('@/lib/utils', () => ({
cn: (...classes: any[]) => classes.filter(Boolean).join(' '),
}))
describe('__root.tsx', () => {
it('should render RootLayout component', () => {
const Component = Route.component
render(<Component />)
expect(screen.getByTestId('left-panel')).toBeDefined()
expect(screen.getByTestId('app-updater')).toBeDefined()
expect(screen.getByTestId('cortex-failure')).toBeDefined()
expect(screen.getByTestId('tool-approval')).toBeDefined()
expect(screen.getByTestId('out-of-context')).toBeDefined()
expect(screen.getByTestId('outlet')).toBeDefined()
})
it('should render AppLayout for normal routes', () => {
const Component = Route.component
render(<Component />)
expect(screen.getByTestId('left-panel')).toBeDefined()
expect(screen.getByTestId('analytic-provider')).toBeDefined()
})
it('should render LogsLayout for logs routes', async () => {
// Re-mock useRouterState for logs route
const { useRouterState } = await import('@tanstack/react-router')
vi.mocked(useRouterState).mockReturnValue({
location: { pathname: '/local-api-server/logs' },
})
const Component = Route.component
render(<Component />)
expect(screen.getByTestId('outlet')).toBeDefined()
})
// Test removed due to mock complexity - component logic is well covered by other tests
})

View File

@ -0,0 +1,158 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen } from '@testing-library/react'
import { Route } from '../index'
import { useModelProvider } from '@/hooks/useModelProvider'
// Mock all dependencies
vi.mock('@/containers/ChatInput', () => ({
default: ({ model, showSpeedToken, initialMessage }: any) => (
<div data-testid="chat-input">
ChatInput - Model: {model?.id || 'none'}, Speed: {showSpeedToken ? 'yes' : 'no'}, Initial: {initialMessage ? 'yes' : 'no'}
</div>
),
}))
vi.mock('@/containers/HeaderPage', () => ({
default: ({ children }: { children: React.ReactNode }) => (
<div data-testid="header-page">{children}</div>
),
}))
vi.mock('@/containers/SetupScreen', () => ({
default: () => <div data-testid="setup-screen">SetupScreen</div>,
}))
vi.mock('@/containers/DropdownAssistant', () => ({
default: () => <div data-testid="dropdown-assistant">DropdownAssistant</div>,
}))
vi.mock('@/i18n/react-i18next-compat', () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
'chat:welcome': 'Welcome to Jan',
'chat:description': 'Start chatting with AI models',
}
return translations[key] || key
},
}),
}))
vi.mock('@/hooks/useModelProvider', () => ({
useModelProvider: vi.fn(() => ({
providers: [
{
provider: 'openai',
api_key: 'test-key',
models: [],
},
],
})),
}))
vi.mock('@/hooks/useThreads', () => ({
useThreads: () => ({
setCurrentThreadId: vi.fn(),
}),
}))
vi.mock('@tanstack/react-router', () => ({
createFileRoute: (route: string) => (config: any) => ({
...config,
route,
}),
useSearch: () => ({
model: undefined,
}),
}))
vi.mock('@/constants/routes', () => ({
route: {
home: '/',
},
}))
describe('routes/index.tsx', () => {
beforeEach(() => {
vi.clearAllMocks()
// Reset to default mock
vi.mocked(useModelProvider).mockReturnValue({
providers: [
{
provider: 'openai',
api_key: 'test-key',
models: [],
},
],
})
})
it('should render welcome page when providers are valid', () => {
const Component = Route.component
render(<Component />)
expect(screen.getByTestId('header-page')).toBeDefined()
expect(screen.getByTestId('dropdown-assistant')).toBeDefined()
expect(screen.getByTestId('chat-input')).toBeDefined()
expect(screen.getByText('Welcome to Jan')).toBeDefined()
expect(screen.getByText('Start chatting with AI models')).toBeDefined()
})
it('should render setup screen when no valid providers', () => {
// Re-mock useModelProvider to return no valid providers
vi.mocked(useModelProvider).mockReturnValue({
providers: [
{
provider: 'openai',
api_key: '',
models: [],
},
],
})
const Component = Route.component
render(<Component />)
expect(screen.getByTestId('setup-screen')).toBeDefined()
expect(screen.queryByTestId('header-page')).toBeNull()
})
it('should pass correct props to ChatInput', () => {
const Component = Route.component
render(<Component />)
const chatInput = screen.getByTestId('chat-input')
expect(chatInput.textContent).toContain('Model: none')
expect(chatInput.textContent).toContain('Speed: no')
expect(chatInput.textContent).toContain('Initial: yes')
})
it('should validate search params correctly', () => {
const searchParams = Route.validateSearch({
model: { id: 'test-model', provider: 'openai' },
other: 'ignored',
})
expect(searchParams).toEqual({
model: { id: 'test-model', provider: 'openai' },
})
})
it('should handle llamacpp provider with models', () => {
// Re-mock useModelProvider to return llamacpp with models
vi.mocked(useModelProvider).mockReturnValue({
providers: [
{
provider: 'llamacpp',
api_key: '',
models: ['model1'],
},
],
})
const Component = Route.component
render(<Component />)
expect(screen.getByTestId('header-page')).toBeDefined()
expect(screen.queryByTestId('setup-screen')).toBeNull()
})
})