test: add missing unit tests

This commit is contained in:
Louis 2025-07-12 22:46:27 +07:00
parent c2790d9181
commit 03bcd02002
No known key found for this signature in database
GPG Key ID: 44FA9F4D33C37DE2
8 changed files with 1174 additions and 0 deletions

View 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()
})
})
})

View 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()
})
})
})

View 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')
})
})

View 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)
})
})
})

View 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')
})
})

View 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)
})
})
})

View 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,
})
})
})
})

View 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)
})
})
})