test: add missing tests
This commit is contained in:
parent
c5fd964bf2
commit
864ad50880
79
web-app/src/__tests__/main.test.tsx
Normal file
79
web-app/src/__tests__/main.test.tsx
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
231
web-app/src/containers/__tests__/LeftPanel.test.tsx
Normal file
231
web-app/src/containers/__tests__/LeftPanel.test.tsx
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
172
web-app/src/hooks/__tests__/useAppearance.test.ts
Normal file
172
web-app/src/hooks/__tests__/useAppearance.test.ts
Normal 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')
|
||||||
|
})
|
||||||
|
})
|
||||||
264
web-app/src/hooks/__tests__/useHardware.test.ts
Normal file
264
web-app/src/hooks/__tests__/useHardware.test.ts
Normal 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')
|
||||||
|
})
|
||||||
|
})
|
||||||
190
web-app/src/lib/__tests__/completion.test.ts
Normal file
190
web-app/src/lib/__tests__/completion.test.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
141
web-app/src/lib/__tests__/extension.test.ts
Normal file
141
web-app/src/lib/__tests__/extension.test.ts
Normal 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' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
126
web-app/src/routes/__tests__/__root.test.tsx
Normal file
126
web-app/src/routes/__tests__/__root.test.tsx
Normal 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
|
||||||
|
})
|
||||||
158
web-app/src/routes/__tests__/index.test.tsx
Normal file
158
web-app/src/routes/__tests__/index.test.tsx
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user