test: add missing unit tests
This commit is contained in:
parent
c2790d9181
commit
03bcd02002
52
web-app/src/__tests__/i18n.test.ts
Normal file
52
web-app/src/__tests__/i18n.test.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
|
||||
// Mock the dependencies
|
||||
vi.mock('@/i18n/setup', () => ({
|
||||
default: { t: vi.fn(), init: vi.fn() },
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n/react-i18next-compat', () => ({
|
||||
useTranslation: vi.fn(() => ({ t: vi.fn() })),
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n/hooks', () => ({
|
||||
useAppTranslation: vi.fn(() => ({ t: vi.fn() })),
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n/TranslationContext', () => ({
|
||||
TranslationProvider: vi.fn(({ children }) => children),
|
||||
}))
|
||||
|
||||
describe('i18n module', () => {
|
||||
it('should re-export default from i18n/setup', async () => {
|
||||
const i18nModule = await import('../i18n')
|
||||
expect(i18nModule.default).toBeDefined()
|
||||
})
|
||||
|
||||
it('should re-export useTranslation', async () => {
|
||||
const i18nModule = await import('../i18n')
|
||||
expect(i18nModule.useTranslation).toBeDefined()
|
||||
expect(typeof i18nModule.useTranslation).toBe('function')
|
||||
})
|
||||
|
||||
it('should re-export useAppTranslation', async () => {
|
||||
const i18nModule = await import('../i18n')
|
||||
expect(i18nModule.useAppTranslation).toBeDefined()
|
||||
expect(typeof i18nModule.useAppTranslation).toBe('function')
|
||||
})
|
||||
|
||||
it('should re-export TranslationProvider', async () => {
|
||||
const i18nModule = await import('../i18n')
|
||||
expect(i18nModule.TranslationProvider).toBeDefined()
|
||||
expect(typeof i18nModule.TranslationProvider).toBe('function')
|
||||
})
|
||||
|
||||
it('should export all expected functions', async () => {
|
||||
const i18nModule = await import('../i18n')
|
||||
const expectedExports = ['default', 'useTranslation', 'useAppTranslation', 'TranslationProvider']
|
||||
|
||||
expectedExports.forEach(exportName => {
|
||||
expect(i18nModule[exportName]).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
168
web-app/src/components/ui/__tests__/hover-card.test.tsx
Normal file
168
web-app/src/components/ui/__tests__/hover-card.test.tsx
Normal file
@ -0,0 +1,168 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { HoverCard, HoverCardTrigger, HoverCardContent } from '../hover-card'
|
||||
|
||||
// Mock Radix UI
|
||||
vi.mock('@radix-ui/react-hover-card', () => ({
|
||||
Root: ({ children, ...props }: any) => <div data-testid="hover-card-root" {...props}>{children}</div>,
|
||||
Trigger: ({ children, ...props }: any) => <button data-testid="hover-card-trigger" {...props}>{children}</button>,
|
||||
Portal: ({ children, ...props }: any) => <div data-testid="hover-card-portal" {...props}>{children}</div>,
|
||||
Content: ({ children, className, align, sideOffset, ...props }: any) => (
|
||||
<div
|
||||
data-testid="hover-card-content"
|
||||
className={className}
|
||||
data-align={align}
|
||||
data-side-offset={sideOffset}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('HoverCard Components', () => {
|
||||
describe('HoverCard', () => {
|
||||
it('should render HoverCard root component', () => {
|
||||
render(
|
||||
<HoverCard>
|
||||
<div>Test content</div>
|
||||
</HoverCard>
|
||||
)
|
||||
|
||||
const hoverCard = screen.getByTestId('hover-card-root')
|
||||
expect(hoverCard).toBeDefined()
|
||||
expect(hoverCard).toHaveAttribute('data-slot', 'hover-card')
|
||||
expect(screen.getByText('Test content')).toBeDefined()
|
||||
})
|
||||
|
||||
it('should pass through props to root component', () => {
|
||||
render(
|
||||
<HoverCard openDelay={500}>
|
||||
<div>Test content</div>
|
||||
</HoverCard>
|
||||
)
|
||||
|
||||
const hoverCard = screen.getByTestId('hover-card-root')
|
||||
expect(hoverCard).toHaveAttribute('openDelay', '500')
|
||||
})
|
||||
})
|
||||
|
||||
describe('HoverCardTrigger', () => {
|
||||
it('should render trigger component', () => {
|
||||
render(
|
||||
<HoverCardTrigger>
|
||||
<span>Hover me</span>
|
||||
</HoverCardTrigger>
|
||||
)
|
||||
|
||||
const trigger = screen.getByTestId('hover-card-trigger')
|
||||
expect(trigger).toBeDefined()
|
||||
expect(trigger).toHaveAttribute('data-slot', 'hover-card-trigger')
|
||||
expect(screen.getByText('Hover me')).toBeDefined()
|
||||
})
|
||||
|
||||
it('should pass through props to trigger component', () => {
|
||||
render(
|
||||
<HoverCardTrigger disabled>
|
||||
<span>Disabled trigger</span>
|
||||
</HoverCardTrigger>
|
||||
)
|
||||
|
||||
const trigger = screen.getByTestId('hover-card-trigger')
|
||||
expect(trigger).toHaveAttribute('disabled')
|
||||
})
|
||||
})
|
||||
|
||||
describe('HoverCardContent', () => {
|
||||
it('should render content with default props', () => {
|
||||
render(
|
||||
<HoverCardContent>
|
||||
<div>Content here</div>
|
||||
</HoverCardContent>
|
||||
)
|
||||
|
||||
const portal = screen.getByTestId('hover-card-portal')
|
||||
expect(portal).toHaveAttribute('data-slot', 'hover-card-portal')
|
||||
|
||||
const content = screen.getByTestId('hover-card-content')
|
||||
expect(content).toBeDefined()
|
||||
expect(content).toHaveAttribute('data-slot', 'hover-card-content')
|
||||
expect(content).toHaveAttribute('data-align', 'center')
|
||||
expect(content).toHaveAttribute('data-side-offset', '4')
|
||||
expect(screen.getByText('Content here')).toBeDefined()
|
||||
})
|
||||
|
||||
it('should render content with custom props', () => {
|
||||
render(
|
||||
<HoverCardContent align="start" sideOffset={8} className="custom-class">
|
||||
<div>Custom content</div>
|
||||
</HoverCardContent>
|
||||
)
|
||||
|
||||
const content = screen.getByTestId('hover-card-content')
|
||||
expect(content).toHaveAttribute('data-align', 'start')
|
||||
expect(content).toHaveAttribute('data-side-offset', '8')
|
||||
expect(content.className).toContain('custom-class')
|
||||
})
|
||||
|
||||
it('should apply default styling classes', () => {
|
||||
render(
|
||||
<HoverCardContent>
|
||||
<div>Content</div>
|
||||
</HoverCardContent>
|
||||
)
|
||||
|
||||
const content = screen.getByTestId('hover-card-content')
|
||||
expect(content.className).toContain('bg-main-view')
|
||||
expect(content.className).toContain('text-main-view-fg/70')
|
||||
expect(content.className).toContain('rounded-md')
|
||||
expect(content.className).toContain('border')
|
||||
expect(content.className).toContain('shadow-md')
|
||||
})
|
||||
|
||||
it('should merge custom className with default classes', () => {
|
||||
render(
|
||||
<HoverCardContent className="my-custom-class">
|
||||
<div>Content</div>
|
||||
</HoverCardContent>
|
||||
)
|
||||
|
||||
const content = screen.getByTestId('hover-card-content')
|
||||
expect(content.className).toContain('bg-main-view')
|
||||
expect(content.className).toContain('my-custom-class')
|
||||
})
|
||||
|
||||
it('should pass through additional props', () => {
|
||||
render(
|
||||
<HoverCardContent data-testprop="test-value">
|
||||
<div>Content</div>
|
||||
</HoverCardContent>
|
||||
)
|
||||
|
||||
const content = screen.getByTestId('hover-card-content')
|
||||
expect(content).toHaveAttribute('data-testprop', 'test-value')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Integration', () => {
|
||||
it('should render complete hover card structure', () => {
|
||||
render(
|
||||
<HoverCard>
|
||||
<HoverCardTrigger>
|
||||
<button>Trigger</button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
<div>Hover content</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('hover-card-root')).toBeDefined()
|
||||
expect(screen.getByTestId('hover-card-trigger')).toBeDefined()
|
||||
expect(screen.getByTestId('hover-card-portal')).toBeDefined()
|
||||
expect(screen.getByTestId('hover-card-content')).toBeDefined()
|
||||
expect(screen.getByText('Trigger')).toBeDefined()
|
||||
expect(screen.getByText('Hover content')).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
93
web-app/src/components/ui/__tests__/sonner.test.tsx
Normal file
93
web-app/src/components/ui/__tests__/sonner.test.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { Toaster } from '../sonner'
|
||||
|
||||
// Mock sonner
|
||||
vi.mock('sonner', () => ({
|
||||
Toaster: ({ className, expand, richColors, closeButton, ...props }: any) => (
|
||||
<div
|
||||
data-testid="toaster"
|
||||
className={className}
|
||||
{...props}
|
||||
{...(expand !== undefined && { 'data-expand': expand })}
|
||||
{...(richColors !== undefined && { 'data-rich-colors': richColors })}
|
||||
{...(closeButton !== undefined && { 'data-close-button': closeButton })}
|
||||
>
|
||||
Toaster Component
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('Toaster Component', () => {
|
||||
it('should render toaster component', () => {
|
||||
render(<Toaster />)
|
||||
|
||||
const toaster = screen.getByTestId('toaster')
|
||||
expect(toaster).toBeDefined()
|
||||
expect(screen.getByText('Toaster Component')).toBeDefined()
|
||||
})
|
||||
|
||||
it('should apply default className', () => {
|
||||
render(<Toaster />)
|
||||
|
||||
const toaster = screen.getByTestId('toaster')
|
||||
expect(toaster).toHaveClass('toaster', 'group')
|
||||
})
|
||||
|
||||
it('should pass through additional props', () => {
|
||||
render(<Toaster position="top-right" duration={5000} />)
|
||||
|
||||
const toaster = screen.getByTestId('toaster')
|
||||
expect(toaster).toHaveAttribute('position', 'top-right')
|
||||
expect(toaster).toHaveAttribute('duration', '5000')
|
||||
})
|
||||
|
||||
it('should maintain default className with additional props', () => {
|
||||
render(<Toaster position="bottom-left" />)
|
||||
|
||||
const toaster = screen.getByTestId('toaster')
|
||||
expect(toaster).toHaveClass('toaster', 'group')
|
||||
expect(toaster).toHaveAttribute('position', 'bottom-left')
|
||||
})
|
||||
|
||||
it('should handle custom expand prop', () => {
|
||||
render(<Toaster expand />)
|
||||
|
||||
const toaster = screen.getByTestId('toaster')
|
||||
expect(toaster).toHaveAttribute('data-expand', 'true')
|
||||
})
|
||||
|
||||
it('should handle custom richColors prop', () => {
|
||||
render(<Toaster richColors />)
|
||||
|
||||
const toaster = screen.getByTestId('toaster')
|
||||
expect(toaster).toHaveAttribute('data-rich-colors', 'true')
|
||||
})
|
||||
|
||||
it('should handle custom closeButton prop', () => {
|
||||
render(<Toaster closeButton />)
|
||||
|
||||
const toaster = screen.getByTestId('toaster')
|
||||
expect(toaster).toHaveAttribute('data-close-button', 'true')
|
||||
})
|
||||
|
||||
it('should handle multiple props', () => {
|
||||
render(
|
||||
<Toaster
|
||||
position="top-center"
|
||||
duration={3000}
|
||||
expand
|
||||
richColors
|
||||
closeButton
|
||||
/>
|
||||
)
|
||||
|
||||
const toaster = screen.getByTestId('toaster')
|
||||
expect(toaster).toHaveClass('toaster', 'group')
|
||||
expect(toaster).toHaveAttribute('position', 'top-center')
|
||||
expect(toaster).toHaveAttribute('duration', '3000')
|
||||
expect(toaster).toHaveAttribute('data-expand', 'true')
|
||||
expect(toaster).toHaveAttribute('data-rich-colors', 'true')
|
||||
expect(toaster).toHaveAttribute('data-close-button', 'true')
|
||||
})
|
||||
})
|
||||
36
web-app/src/constants/__tests__/windows.test.ts
Normal file
36
web-app/src/constants/__tests__/windows.test.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { windowKey } from '../windows'
|
||||
|
||||
describe('windows constants', () => {
|
||||
it('should export correct window keys', () => {
|
||||
expect(windowKey).toBeDefined()
|
||||
expect(typeof windowKey).toBe('object')
|
||||
})
|
||||
|
||||
it('should have logsAppWindow key', () => {
|
||||
expect(windowKey.logsAppWindow).toBe('logs-app-window')
|
||||
})
|
||||
|
||||
it('should have logsWindowLocalApiServer key', () => {
|
||||
expect(windowKey.logsWindowLocalApiServer).toBe('logs-window-local-api-server')
|
||||
})
|
||||
|
||||
it('should have systemMonitorWindow key', () => {
|
||||
expect(windowKey.systemMonitorWindow).toBe('system-monitor-window')
|
||||
})
|
||||
|
||||
it('should have all required keys', () => {
|
||||
const expectedKeys = ['logsAppWindow', 'logsWindowLocalApiServer', 'systemMonitorWindow']
|
||||
const actualKeys = Object.keys(windowKey)
|
||||
|
||||
expect(actualKeys).toEqual(expect.arrayContaining(expectedKeys))
|
||||
expect(actualKeys.length).toBe(expectedKeys.length)
|
||||
})
|
||||
|
||||
it('should have string values for all keys', () => {
|
||||
Object.values(windowKey).forEach(value => {
|
||||
expect(typeof value).toBe('string')
|
||||
expect(value.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
124
web-app/src/containers/__tests__/AvatarEmoji.test.tsx
Normal file
124
web-app/src/containers/__tests__/AvatarEmoji.test.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { AvatarEmoji } from '../AvatarEmoji'
|
||||
|
||||
describe('AvatarEmoji Component', () => {
|
||||
it('should return null when no avatar is provided', () => {
|
||||
const { container } = render(<AvatarEmoji />)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('should return null when avatar is undefined', () => {
|
||||
const { container } = render(<AvatarEmoji avatar={undefined} />)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('should render image when avatar is a custom image path', () => {
|
||||
render(<AvatarEmoji avatar="/images/custom-avatar.png" />)
|
||||
|
||||
const img = screen.getByRole('img')
|
||||
expect(img).toBeDefined()
|
||||
expect(img).toHaveAttribute('src', '/images/custom-avatar.png')
|
||||
expect(img).toHaveAttribute('alt', 'Custom avatar')
|
||||
})
|
||||
|
||||
it('should apply default image className', () => {
|
||||
render(<AvatarEmoji avatar="/images/avatar.jpg" />)
|
||||
|
||||
const img = screen.getByRole('img')
|
||||
expect(img).toHaveClass('w-5', 'h-5', 'object-contain')
|
||||
})
|
||||
|
||||
it('should apply custom image className', () => {
|
||||
render(
|
||||
<AvatarEmoji
|
||||
avatar="/images/avatar.jpg"
|
||||
imageClassName="w-10 h-10 rounded-full"
|
||||
/>
|
||||
)
|
||||
|
||||
const img = screen.getByRole('img')
|
||||
expect(img).toHaveClass('w-10', 'h-10', 'rounded-full')
|
||||
expect(img).not.toHaveClass('w-5', 'h-5', 'object-contain')
|
||||
})
|
||||
|
||||
it('should render emoji as text span', () => {
|
||||
render(<AvatarEmoji avatar="🤖" />)
|
||||
|
||||
const span = screen.getByText('🤖')
|
||||
expect(span.tagName).toBe('SPAN')
|
||||
})
|
||||
|
||||
it('should apply default text className for emoji', () => {
|
||||
render(<AvatarEmoji avatar="😊" />)
|
||||
|
||||
const span = screen.getByText('😊')
|
||||
expect(span).toHaveClass('text-base')
|
||||
})
|
||||
|
||||
it('should apply custom text className for emoji', () => {
|
||||
render(
|
||||
<AvatarEmoji
|
||||
avatar="🎯"
|
||||
textClassName="text-lg font-bold"
|
||||
/>
|
||||
)
|
||||
|
||||
const span = screen.getByText('🎯')
|
||||
expect(span).toHaveClass('text-lg', 'font-bold')
|
||||
expect(span).not.toHaveClass('text-base')
|
||||
})
|
||||
|
||||
it('should render text content as span', () => {
|
||||
render(<AvatarEmoji avatar="AI" />)
|
||||
|
||||
const span = screen.getByText('AI')
|
||||
expect(span.tagName).toBe('SPAN')
|
||||
expect(span).toHaveClass('text-base')
|
||||
})
|
||||
|
||||
it('should handle React node as avatar', () => {
|
||||
const customNode = <div data-testid="custom-node">Custom</div>
|
||||
render(<AvatarEmoji avatar={customNode} />)
|
||||
|
||||
const span = screen.getByText('Custom')
|
||||
expect(span.closest('span')).toHaveClass('text-base')
|
||||
expect(screen.getByTestId('custom-node')).toBeDefined()
|
||||
})
|
||||
|
||||
it('should not treat non-image paths as custom images', () => {
|
||||
render(<AvatarEmoji avatar="/api/data" />)
|
||||
|
||||
const span = screen.getByText('/api/data')
|
||||
expect(span.tagName).toBe('SPAN')
|
||||
expect(screen.queryByRole('img')).toBeNull()
|
||||
})
|
||||
|
||||
it('should not treat relative paths as custom images', () => {
|
||||
render(<AvatarEmoji avatar="images/avatar.png" />)
|
||||
|
||||
const span = screen.getByText('images/avatar.png')
|
||||
expect(span.tagName).toBe('SPAN')
|
||||
expect(screen.queryByRole('img')).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle different image extensions', () => {
|
||||
const extensions = ['.png', '.jpg', '.jpeg', '.gif', '.svg']
|
||||
|
||||
extensions.forEach((ext, index) => {
|
||||
const { unmount } = render(<AvatarEmoji avatar={`/images/avatar${ext}`} />)
|
||||
|
||||
const img = screen.getByRole('img')
|
||||
expect(img).toHaveAttribute('src', `/images/avatar${ext}`)
|
||||
|
||||
unmount()
|
||||
})
|
||||
})
|
||||
|
||||
it('should maintain accessibility for custom images', () => {
|
||||
render(<AvatarEmoji avatar="/images/user-avatar.png" />)
|
||||
|
||||
const img = screen.getByRole('img')
|
||||
expect(img).toHaveAttribute('alt', 'Custom avatar')
|
||||
})
|
||||
})
|
||||
189
web-app/src/hooks/__tests__/useTheme.test.ts
Normal file
189
web-app/src/hooks/__tests__/useTheme.test.ts
Normal file
@ -0,0 +1,189 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { checkOSDarkMode } from '../useTheme'
|
||||
|
||||
// Mock Tauri API
|
||||
vi.mock('@tauri-apps/api/window', () => ({
|
||||
getCurrentWindow: () => ({
|
||||
setTheme: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
Theme: {
|
||||
Dark: 'dark',
|
||||
Light: 'light',
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock localStorage
|
||||
vi.mock('@/constants/localStorage', () => ({
|
||||
localStorageKey: {
|
||||
theme: 'theme',
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock zustand persist
|
||||
vi.mock('zustand/middleware', () => ({
|
||||
persist: (fn: any) => fn,
|
||||
createJSONStorage: () => ({
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('useTheme', () => {
|
||||
let originalMatchMedia: any
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Mock window.matchMedia
|
||||
originalMatchMedia = window.matchMedia
|
||||
window.matchMedia = vi.fn()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original matchMedia
|
||||
window.matchMedia = originalMatchMedia
|
||||
})
|
||||
|
||||
describe('checkOSDarkMode', () => {
|
||||
it('should return true when OS prefers dark mode', () => {
|
||||
vi.mocked(window.matchMedia).mockReturnValue({
|
||||
matches: true,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
} as any)
|
||||
|
||||
const result = checkOSDarkMode()
|
||||
expect(result).toBe(true)
|
||||
expect(window.matchMedia).toHaveBeenCalledWith('(prefers-color-scheme: dark)')
|
||||
})
|
||||
|
||||
it('should return false when OS prefers light mode', () => {
|
||||
vi.mocked(window.matchMedia).mockReturnValue({
|
||||
matches: false,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
} as any)
|
||||
|
||||
const result = checkOSDarkMode()
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('should return falsy when matchMedia is not available', () => {
|
||||
const originalMatchMedia = window.matchMedia
|
||||
// @ts-ignore
|
||||
window.matchMedia = null
|
||||
|
||||
const result = checkOSDarkMode()
|
||||
expect(result).toBeFalsy()
|
||||
|
||||
// Restore
|
||||
window.matchMedia = originalMatchMedia
|
||||
})
|
||||
})
|
||||
|
||||
describe('useTheme hook basic functionality', () => {
|
||||
beforeEach(() => {
|
||||
// Default to light mode
|
||||
vi.mocked(window.matchMedia).mockReturnValue({
|
||||
matches: false,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
} as any)
|
||||
})
|
||||
|
||||
it('should have the expected interface', async () => {
|
||||
const { useTheme } = await import('../useTheme')
|
||||
const { result } = renderHook(() => useTheme())
|
||||
|
||||
expect(result.current).toHaveProperty('activeTheme')
|
||||
expect(result.current).toHaveProperty('isDark')
|
||||
expect(result.current).toHaveProperty('setTheme')
|
||||
expect(result.current).toHaveProperty('setIsDark')
|
||||
expect(typeof result.current.setTheme).toBe('function')
|
||||
expect(typeof result.current.setIsDark).toBe('function')
|
||||
})
|
||||
|
||||
it('should initialize with auto theme', async () => {
|
||||
const { useTheme } = await import('../useTheme')
|
||||
const { result } = renderHook(() => useTheme())
|
||||
|
||||
expect(result.current.activeTheme).toBe('auto')
|
||||
expect(typeof result.current.isDark).toBe('boolean')
|
||||
})
|
||||
|
||||
it('should allow setting isDark directly', async () => {
|
||||
const { useTheme } = await import('../useTheme')
|
||||
const { result } = renderHook(() => useTheme())
|
||||
|
||||
act(() => {
|
||||
result.current.setIsDark(true)
|
||||
})
|
||||
|
||||
expect(result.current.isDark).toBe(true)
|
||||
|
||||
act(() => {
|
||||
result.current.setIsDark(false)
|
||||
})
|
||||
|
||||
expect(result.current.isDark).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle theme changes', async () => {
|
||||
const { useTheme } = await import('../useTheme')
|
||||
const { result } = renderHook(() => useTheme())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.setTheme('dark')
|
||||
})
|
||||
|
||||
expect(result.current.activeTheme).toBe('dark')
|
||||
expect(result.current.isDark).toBe(true)
|
||||
|
||||
await act(async () => {
|
||||
await result.current.setTheme('light')
|
||||
})
|
||||
|
||||
expect(result.current.activeTheme).toBe('light')
|
||||
expect(result.current.isDark).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle auto theme with OS preference', async () => {
|
||||
// Mock OS dark preference
|
||||
vi.mocked(window.matchMedia).mockReturnValue({
|
||||
matches: true,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
} as any)
|
||||
|
||||
const { useTheme } = await import('../useTheme')
|
||||
const { result } = renderHook(() => useTheme())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.setTheme('auto')
|
||||
})
|
||||
|
||||
expect(result.current.activeTheme).toBe('auto')
|
||||
expect(result.current.isDark).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle auto theme with light OS preference', async () => {
|
||||
// Mock OS light preference
|
||||
vi.mocked(window.matchMedia).mockReturnValue({
|
||||
matches: false,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
} as any)
|
||||
|
||||
const { useTheme } = await import('../useTheme')
|
||||
const { result } = renderHook(() => useTheme())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.setTheme('auto')
|
||||
})
|
||||
|
||||
expect(result.current.activeTheme).toBe('auto')
|
||||
expect(result.current.isDark).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
265
web-app/src/services/__tests__/analytic.test.ts
Normal file
265
web-app/src/services/__tests__/analytic.test.ts
Normal file
@ -0,0 +1,265 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { updateDistinctId, getAppDistinctId } from '../analytic'
|
||||
|
||||
// Mock window.core API
|
||||
const mockGetAppConfigurations = vi.fn()
|
||||
const mockUpdateAppConfiguration = vi.fn()
|
||||
|
||||
const mockCore = {
|
||||
api: {
|
||||
getAppConfigurations: mockGetAppConfigurations,
|
||||
updateAppConfiguration: mockUpdateAppConfiguration,
|
||||
},
|
||||
}
|
||||
|
||||
// Setup global window mock
|
||||
Object.defineProperty(window, 'core', {
|
||||
writable: true,
|
||||
value: mockCore,
|
||||
})
|
||||
|
||||
describe('analytic service', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('updateDistinctId', () => {
|
||||
it('should update distinct id in app configuration', async () => {
|
||||
const mockConfiguration = {
|
||||
distinct_id: 'old-id',
|
||||
other_setting: 'value',
|
||||
}
|
||||
|
||||
mockGetAppConfigurations.mockResolvedValue(mockConfiguration)
|
||||
mockUpdateAppConfiguration.mockResolvedValue(undefined)
|
||||
|
||||
await updateDistinctId('new-distinct-id')
|
||||
|
||||
expect(mockGetAppConfigurations).toHaveBeenCalledTimes(1)
|
||||
expect(mockUpdateAppConfiguration).toHaveBeenCalledWith({
|
||||
configuration: {
|
||||
distinct_id: 'new-distinct-id',
|
||||
other_setting: 'value',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle when configuration has no existing distinct_id', async () => {
|
||||
const mockConfiguration = {
|
||||
other_setting: 'value',
|
||||
}
|
||||
|
||||
mockGetAppConfigurations.mockResolvedValue(mockConfiguration)
|
||||
mockUpdateAppConfiguration.mockResolvedValue(undefined)
|
||||
|
||||
await updateDistinctId('first-distinct-id')
|
||||
|
||||
expect(mockUpdateAppConfiguration).toHaveBeenCalledWith({
|
||||
configuration: {
|
||||
distinct_id: 'first-distinct-id',
|
||||
other_setting: 'value',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle empty string as distinct id', async () => {
|
||||
const mockConfiguration = {
|
||||
distinct_id: 'old-id',
|
||||
}
|
||||
|
||||
mockGetAppConfigurations.mockResolvedValue(mockConfiguration)
|
||||
mockUpdateAppConfiguration.mockResolvedValue(undefined)
|
||||
|
||||
await updateDistinctId('')
|
||||
|
||||
expect(mockUpdateAppConfiguration).toHaveBeenCalledWith({
|
||||
configuration: {
|
||||
distinct_id: '',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle UUID format distinct id', async () => {
|
||||
const mockConfiguration = {}
|
||||
const uuidId = '550e8400-e29b-41d4-a716-446655440000'
|
||||
|
||||
mockGetAppConfigurations.mockResolvedValue(mockConfiguration)
|
||||
mockUpdateAppConfiguration.mockResolvedValue(undefined)
|
||||
|
||||
await updateDistinctId(uuidId)
|
||||
|
||||
expect(mockUpdateAppConfiguration).toHaveBeenCalledWith({
|
||||
configuration: {
|
||||
distinct_id: uuidId,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle API errors gracefully', async () => {
|
||||
mockGetAppConfigurations.mockRejectedValue(new Error('API Error'))
|
||||
|
||||
await expect(updateDistinctId('test-id')).rejects.toThrow('API Error')
|
||||
expect(mockUpdateAppConfiguration).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle update configuration errors', async () => {
|
||||
const mockConfiguration = { distinct_id: 'old-id' }
|
||||
|
||||
mockGetAppConfigurations.mockResolvedValue(mockConfiguration)
|
||||
mockUpdateAppConfiguration.mockRejectedValue(new Error('Update Error'))
|
||||
|
||||
await expect(updateDistinctId('new-id')).rejects.toThrow('Update Error')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAppDistinctId', () => {
|
||||
it('should return distinct id from app configuration', async () => {
|
||||
const mockConfiguration = {
|
||||
distinct_id: 'test-distinct-id',
|
||||
other_setting: 'value',
|
||||
}
|
||||
|
||||
mockGetAppConfigurations.mockResolvedValue(mockConfiguration)
|
||||
|
||||
const result = await getAppDistinctId()
|
||||
|
||||
expect(result).toBe('test-distinct-id')
|
||||
expect(mockGetAppConfigurations).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should return undefined when distinct_id is not set', async () => {
|
||||
const mockConfiguration = {
|
||||
other_setting: 'value',
|
||||
}
|
||||
|
||||
mockGetAppConfigurations.mockResolvedValue(mockConfiguration)
|
||||
|
||||
const result = await getAppDistinctId()
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return empty string if distinct_id is empty', async () => {
|
||||
const mockConfiguration = {
|
||||
distinct_id: '',
|
||||
}
|
||||
|
||||
mockGetAppConfigurations.mockResolvedValue(mockConfiguration)
|
||||
|
||||
const result = await getAppDistinctId()
|
||||
|
||||
expect(result).toBe('')
|
||||
})
|
||||
|
||||
it('should handle null configuration', async () => {
|
||||
mockGetAppConfigurations.mockResolvedValue(null)
|
||||
|
||||
await expect(getAppDistinctId()).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('should handle undefined configuration', async () => {
|
||||
mockGetAppConfigurations.mockResolvedValue(undefined)
|
||||
|
||||
await expect(getAppDistinctId()).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
mockGetAppConfigurations.mockRejectedValue(new Error('Get Config Error'))
|
||||
|
||||
await expect(getAppDistinctId()).rejects.toThrow('Get Config Error')
|
||||
})
|
||||
|
||||
it('should handle different types of distinct_id values', async () => {
|
||||
// Test with UUID
|
||||
mockGetAppConfigurations.mockResolvedValue({
|
||||
distinct_id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
})
|
||||
|
||||
let result = await getAppDistinctId()
|
||||
expect(result).toBe('550e8400-e29b-41d4-a716-446655440000')
|
||||
|
||||
// Test with simple string
|
||||
mockGetAppConfigurations.mockResolvedValue({
|
||||
distinct_id: 'user123',
|
||||
})
|
||||
|
||||
result = await getAppDistinctId()
|
||||
expect(result).toBe('user123')
|
||||
|
||||
// Test with numeric string
|
||||
mockGetAppConfigurations.mockResolvedValue({
|
||||
distinct_id: '12345',
|
||||
})
|
||||
|
||||
result = await getAppDistinctId()
|
||||
expect(result).toBe('12345')
|
||||
})
|
||||
})
|
||||
|
||||
describe('integration tests', () => {
|
||||
it('should update and retrieve distinct id', async () => {
|
||||
const newId = 'integration-test-id'
|
||||
const mockConfiguration = { other_setting: 'value' }
|
||||
|
||||
// Mock get configuration for update
|
||||
mockGetAppConfigurations.mockResolvedValueOnce(mockConfiguration)
|
||||
mockUpdateAppConfiguration.mockResolvedValue(undefined)
|
||||
|
||||
// Mock get configuration for retrieval
|
||||
mockGetAppConfigurations.mockResolvedValueOnce({
|
||||
...mockConfiguration,
|
||||
distinct_id: newId,
|
||||
})
|
||||
|
||||
// Update the distinct id
|
||||
await updateDistinctId(newId)
|
||||
|
||||
// Retrieve the distinct id
|
||||
const retrievedId = await getAppDistinctId()
|
||||
|
||||
expect(retrievedId).toBe(newId)
|
||||
expect(mockGetAppConfigurations).toHaveBeenCalledTimes(2)
|
||||
expect(mockUpdateAppConfiguration).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle when window.core is undefined', async () => {
|
||||
const originalCore = window.core
|
||||
|
||||
// Temporarily remove core
|
||||
Object.defineProperty(window, 'core', {
|
||||
writable: true,
|
||||
value: undefined,
|
||||
})
|
||||
|
||||
await expect(updateDistinctId('test')).rejects.toThrow()
|
||||
await expect(getAppDistinctId()).rejects.toThrow()
|
||||
|
||||
// Restore core
|
||||
Object.defineProperty(window, 'core', {
|
||||
writable: true,
|
||||
value: originalCore,
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle when window.core.api is undefined', async () => {
|
||||
const originalCore = window.core
|
||||
|
||||
// Set core without api
|
||||
Object.defineProperty(window, 'core', {
|
||||
writable: true,
|
||||
value: {},
|
||||
})
|
||||
|
||||
await expect(updateDistinctId('test')).rejects.toThrow()
|
||||
await expect(getAppDistinctId()).rejects.toThrow()
|
||||
|
||||
// Restore core
|
||||
Object.defineProperty(window, 'core', {
|
||||
writable: true,
|
||||
value: originalCore,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
247
web-app/src/services/__tests__/events.test.ts
Normal file
247
web-app/src/services/__tests__/events.test.ts
Normal file
@ -0,0 +1,247 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { EventEmitter } from '../events'
|
||||
|
||||
describe('EventEmitter', () => {
|
||||
let eventEmitter: EventEmitter
|
||||
|
||||
beforeEach(() => {
|
||||
eventEmitter = new EventEmitter()
|
||||
})
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create an instance with empty handlers map', () => {
|
||||
expect(eventEmitter).toBeInstanceOf(EventEmitter)
|
||||
expect(eventEmitter['handlers']).toBeInstanceOf(Map)
|
||||
expect(eventEmitter['handlers'].size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('on method', () => {
|
||||
it('should register a handler for a new event', () => {
|
||||
const handler = vi.fn()
|
||||
|
||||
eventEmitter.on('test-event', handler)
|
||||
|
||||
expect(eventEmitter['handlers'].has('test-event')).toBe(true)
|
||||
expect(eventEmitter['handlers'].get('test-event')).toContain(handler)
|
||||
})
|
||||
|
||||
it('should add multiple handlers for the same event', () => {
|
||||
const handler1 = vi.fn()
|
||||
const handler2 = vi.fn()
|
||||
|
||||
eventEmitter.on('test-event', handler1)
|
||||
eventEmitter.on('test-event', handler2)
|
||||
|
||||
const handlers = eventEmitter['handlers'].get('test-event')
|
||||
expect(handlers).toHaveLength(2)
|
||||
expect(handlers).toContain(handler1)
|
||||
expect(handlers).toContain(handler2)
|
||||
})
|
||||
|
||||
it('should handle multiple different events', () => {
|
||||
const handler1 = vi.fn()
|
||||
const handler2 = vi.fn()
|
||||
|
||||
eventEmitter.on('event1', handler1)
|
||||
eventEmitter.on('event2', handler2)
|
||||
|
||||
expect(eventEmitter['handlers'].has('event1')).toBe(true)
|
||||
expect(eventEmitter['handlers'].has('event2')).toBe(true)
|
||||
expect(eventEmitter['handlers'].get('event1')).toContain(handler1)
|
||||
expect(eventEmitter['handlers'].get('event2')).toContain(handler2)
|
||||
})
|
||||
|
||||
it('should allow the same handler to be registered multiple times', () => {
|
||||
const handler = vi.fn()
|
||||
|
||||
eventEmitter.on('test-event', handler)
|
||||
eventEmitter.on('test-event', handler)
|
||||
|
||||
const handlers = eventEmitter['handlers'].get('test-event')
|
||||
expect(handlers).toHaveLength(2)
|
||||
expect(handlers![0]).toBe(handler)
|
||||
expect(handlers![1]).toBe(handler)
|
||||
})
|
||||
})
|
||||
|
||||
describe('off method', () => {
|
||||
it('should remove a handler from an existing event', () => {
|
||||
const handler = vi.fn()
|
||||
|
||||
eventEmitter.on('test-event', handler)
|
||||
expect(eventEmitter['handlers'].get('test-event')).toContain(handler)
|
||||
|
||||
eventEmitter.off('test-event', handler)
|
||||
expect(eventEmitter['handlers'].get('test-event')).not.toContain(handler)
|
||||
})
|
||||
|
||||
it('should do nothing when trying to remove handler from non-existent event', () => {
|
||||
const handler = vi.fn()
|
||||
|
||||
// Should not throw an error
|
||||
expect(() => {
|
||||
eventEmitter.off('non-existent-event', handler)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should do nothing when trying to remove non-existent handler', () => {
|
||||
const handler1 = vi.fn()
|
||||
const handler2 = vi.fn()
|
||||
|
||||
eventEmitter.on('test-event', handler1)
|
||||
|
||||
// Should not throw an error
|
||||
expect(() => {
|
||||
eventEmitter.off('test-event', handler2)
|
||||
}).not.toThrow()
|
||||
|
||||
// Original handler should still be there
|
||||
expect(eventEmitter['handlers'].get('test-event')).toContain(handler1)
|
||||
})
|
||||
|
||||
it('should remove only the first occurrence of a handler', () => {
|
||||
const handler = vi.fn()
|
||||
|
||||
eventEmitter.on('test-event', handler)
|
||||
eventEmitter.on('test-event', handler)
|
||||
|
||||
expect(eventEmitter['handlers'].get('test-event')).toHaveLength(2)
|
||||
|
||||
eventEmitter.off('test-event', handler)
|
||||
|
||||
expect(eventEmitter['handlers'].get('test-event')).toHaveLength(1)
|
||||
expect(eventEmitter['handlers'].get('test-event')).toContain(handler)
|
||||
})
|
||||
|
||||
it('should remove correct handler when multiple handlers exist', () => {
|
||||
const handler1 = vi.fn()
|
||||
const handler2 = vi.fn()
|
||||
const handler3 = vi.fn()
|
||||
|
||||
eventEmitter.on('test-event', handler1)
|
||||
eventEmitter.on('test-event', handler2)
|
||||
eventEmitter.on('test-event', handler3)
|
||||
|
||||
eventEmitter.off('test-event', handler2)
|
||||
|
||||
const handlers = eventEmitter['handlers'].get('test-event')
|
||||
expect(handlers).toHaveLength(2)
|
||||
expect(handlers).toContain(handler1)
|
||||
expect(handlers).not.toContain(handler2)
|
||||
expect(handlers).toContain(handler3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('emit method', () => {
|
||||
it('should call all handlers for an event', () => {
|
||||
const handler1 = vi.fn()
|
||||
const handler2 = vi.fn()
|
||||
|
||||
eventEmitter.on('test-event', handler1)
|
||||
eventEmitter.on('test-event', handler2)
|
||||
|
||||
eventEmitter.emit('test-event', 'test-data')
|
||||
|
||||
expect(handler1).toHaveBeenCalledWith('test-data')
|
||||
expect(handler2).toHaveBeenCalledWith('test-data')
|
||||
})
|
||||
|
||||
it('should do nothing when emitting non-existent event', () => {
|
||||
// Should not throw an error
|
||||
expect(() => {
|
||||
eventEmitter.emit('non-existent-event', 'data')
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should pass arguments to handlers', () => {
|
||||
const handler = vi.fn()
|
||||
const testData = { message: 'test', number: 42 }
|
||||
|
||||
eventEmitter.on('test-event', handler)
|
||||
eventEmitter.emit('test-event', testData)
|
||||
|
||||
expect(handler).toHaveBeenCalledWith(testData)
|
||||
})
|
||||
|
||||
it('should call handlers in the order they were added', () => {
|
||||
const callOrder: number[] = []
|
||||
const handler1 = vi.fn(() => callOrder.push(1))
|
||||
const handler2 = vi.fn(() => callOrder.push(2))
|
||||
const handler3 = vi.fn(() => callOrder.push(3))
|
||||
|
||||
eventEmitter.on('test-event', handler1)
|
||||
eventEmitter.on('test-event', handler2)
|
||||
eventEmitter.on('test-event', handler3)
|
||||
|
||||
eventEmitter.emit('test-event', null)
|
||||
|
||||
expect(callOrder).toEqual([1, 2, 3])
|
||||
})
|
||||
|
||||
it('should handle null and undefined arguments', () => {
|
||||
const handler = vi.fn()
|
||||
|
||||
eventEmitter.on('test-event', handler)
|
||||
|
||||
eventEmitter.emit('test-event', null)
|
||||
expect(handler).toHaveBeenCalledWith(null)
|
||||
|
||||
eventEmitter.emit('test-event', undefined)
|
||||
expect(handler).toHaveBeenCalledWith(undefined)
|
||||
})
|
||||
|
||||
it('should not affect other events', () => {
|
||||
const handler1 = vi.fn()
|
||||
const handler2 = vi.fn()
|
||||
|
||||
eventEmitter.on('event1', handler1)
|
||||
eventEmitter.on('event2', handler2)
|
||||
|
||||
eventEmitter.emit('event1', 'data1')
|
||||
|
||||
expect(handler1).toHaveBeenCalledWith('data1')
|
||||
expect(handler2).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('integration tests', () => {
|
||||
it('should support complete event lifecycle', () => {
|
||||
const handler1 = vi.fn()
|
||||
const handler2 = vi.fn()
|
||||
|
||||
// Register handlers
|
||||
eventEmitter.on('lifecycle-event', handler1)
|
||||
eventEmitter.on('lifecycle-event', handler2)
|
||||
|
||||
// Emit event
|
||||
eventEmitter.emit('lifecycle-event', 'test-data')
|
||||
expect(handler1).toHaveBeenCalledWith('test-data')
|
||||
expect(handler2).toHaveBeenCalledWith('test-data')
|
||||
|
||||
// Remove one handler
|
||||
eventEmitter.off('lifecycle-event', handler1)
|
||||
|
||||
// Emit again
|
||||
eventEmitter.emit('lifecycle-event', 'test-data-2')
|
||||
expect(handler1).toHaveBeenCalledTimes(1) // Still only called once
|
||||
expect(handler2).toHaveBeenCalledTimes(2) // Called twice
|
||||
expect(handler2).toHaveBeenLastCalledWith('test-data-2')
|
||||
})
|
||||
|
||||
it('should handle complex data types', () => {
|
||||
const handler = vi.fn()
|
||||
const complexData = {
|
||||
array: [1, 2, 3],
|
||||
object: { nested: true },
|
||||
function: () => 'test',
|
||||
symbol: Symbol('test'),
|
||||
}
|
||||
|
||||
eventEmitter.on('complex-event', handler)
|
||||
eventEmitter.emit('complex-event', complexData)
|
||||
|
||||
expect(handler).toHaveBeenCalledWith(complexData)
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user