Files
CherryHQ-cherry-studio/tests/renderer.setup.ts
2026-07-03 11:39:33 +08:00

802 lines
32 KiB
TypeScript

import '@testing-library/jest-dom/vitest'
import { createRequire } from 'node:module'
import { expect, vi } from 'vitest'
const require = createRequire(import.meta.url)
const bufferModule = require('buffer')
if (!bufferModule.SlowBuffer) {
bufferModule.SlowBuffer = bufferModule.Buffer
}
// Mock LoggerService globally for renderer tests
vi.mock('@logger', async () => {
const { MockRendererLoggerService, mockRendererLoggerService } = await import('./__mocks__/RendererLoggerService')
return {
LoggerService: MockRendererLoggerService,
loggerService: mockRendererLoggerService
}
})
// Mock PreferenceService globally for renderer tests
vi.mock('@data/PreferenceService', async () => {
const { MockPreferenceService } = await import('./__mocks__/renderer/PreferenceService')
return MockPreferenceService
})
// Mock DataApiService globally for renderer tests
vi.mock('@data/DataApiService', async () => {
const { MockDataApiService } = await import('./__mocks__/renderer/DataApiService')
return MockDataApiService
})
// Mock CacheService globally for renderer tests
vi.mock('@data/CacheService', async () => {
const { MockCacheService } = await import('./__mocks__/renderer/CacheService')
return MockCacheService
})
// Mock useDataApi hooks globally for renderer tests
vi.mock('@data/hooks/useDataApi', async () => {
const { MockUseDataApi } = await import('./__mocks__/renderer/useDataApi')
return MockUseDataApi
})
// Mock usePreference hooks globally for renderer tests
vi.mock('@data/hooks/usePreference', async () => {
const { MockUsePreference } = await import('./__mocks__/renderer/usePreference')
return MockUsePreference
})
// Mock useCache hooks globally for renderer tests
vi.mock('@data/hooks/useCache', async () => {
const { MockUseCache } = await import('./__mocks__/renderer/useCache')
return MockUseCache
})
// Mock PreferenceService globally for renderer tests
vi.mock('@data/PreferenceService', async () => {
const { MockPreferenceService } = await import('./__mocks__/renderer/PreferenceService')
return MockPreferenceService
})
// Mock DataApiService globally for renderer tests
vi.mock('@data/DataApiService', async () => {
const { MockDataApiService } = await import('./__mocks__/renderer/DataApiService')
return MockDataApiService
})
// Mock CacheService globally for renderer tests
vi.mock('@data/CacheService', async () => {
const { MockCacheService } = await import('./__mocks__/renderer/CacheService')
return MockCacheService
})
// Mock useDataApi hooks globally for renderer tests
vi.mock('@data/hooks/useDataApi', async () => {
const { MockUseDataApi } = await import('./__mocks__/renderer/useDataApi')
return MockUseDataApi
})
// Mock usePreference hooks globally for renderer tests
vi.mock('@data/hooks/usePreference', async () => {
const { MockUsePreference } = await import('./__mocks__/renderer/usePreference')
return MockUsePreference
})
// Mock useCache hooks globally for renderer tests
vi.mock('@data/hooks/useCache', async () => {
const { MockUseCache } = await import('./__mocks__/renderer/useCache')
return MockUseCache
})
// Mock uuid globally for renderer tests
let uuidCounter = 0
vi.mock('uuid', () => ({
v4: () => 'test-uuid-' + ++uuidCounter
}))
vi.mock('@iconify/react', () => {
const React = require('react')
return {
Icon: ({ icon, ...props }: { icon?: string }) =>
React.createElement('span', { ...props, 'data-icon': icon, 'data-testid': 'iconify-icon' })
}
})
vi.mock('axios', () => {
const defaultAxiosMock = {
get: vi.fn().mockResolvedValue({ data: {} }), // Mocking axios GET request
post: vi.fn().mockResolvedValue({ data: {} }) // Mocking axios POST request
// You can add other axios methods like put, delete etc. as needed
}
const isAxiosError = (error: unknown): error is { isAxiosError?: boolean } =>
Boolean((error as { isAxiosError?: boolean } | undefined)?.isAxiosError)
return {
default: defaultAxiosMock,
isAxiosError
}
})
// Mock ResizeObserver for jsdom environment
vi.stubGlobal(
'ResizeObserver',
class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}
)
vi.stubGlobal('electron', {
ipcRenderer: {
on: vi.fn(),
send: vi.fn(),
invoke: vi.fn().mockResolvedValue(undefined)
}
})
vi.stubGlobal('api', {
file: {
read: vi.fn().mockResolvedValue('[]'),
writeWithId: vi.fn().mockResolvedValue(undefined)
}
})
// Markdown stylesheet import is a side-effect no-op in tests
vi.mock('@cherrystudio/ui/components/composites/markdown/styles', () => ({}))
// Mock @cherrystudio/ui globally for renderer tests
vi.mock('@cherrystudio/ui', () => {
const React = require('react')
const SelectContext = React.createContext({ value: undefined, onValueChange: undefined })
const PopoverContext = React.createContext({ open: false, onOpenChange: undefined })
const ContextMenuContext = React.createContext({ open: false, onOpenChange: undefined })
const DropdownMenuOpenContext = React.createContext(null)
return {
// Markdown — `@cherrystudio/ui` barrel re-exports composites/markdown (#16228).
// Lightweight stand-ins so tests mounting real ChatMarkdown still surface text.
Markdown: ({ children }) => React.createElement('div', null, children),
StreamingMarkdown: ({ children }) => React.createElement('div', null, children),
withChatPlugins: () => [],
withMath: (plugins) => plugins ?? [],
withMermaid: (plugins) => plugins ?? [],
withFullMarkdown: (plugins) => plugins ?? [],
defaultMarkdownPlugins: [],
useMarkdownBlockContext: () => ({ content: '' }),
createSlugger: () => ({ slug: (value) => String(value ?? '') }),
extractTextFromNode: () => '',
ReorderableList: ({ items, renderItem, getId }) =>
React.createElement(
React.Fragment,
null,
items.map((item, index) =>
React.createElement('div', { key: getId(item) }, renderItem(item, index, { dragging: false }))
)
),
Sortable: ({ items, itemKey, renderItem, className }) => {
const getKey = typeof itemKey === 'function' ? itemKey : (item) => item[itemKey]
return React.createElement(
'div',
{ className },
items.map((item) =>
React.createElement('div', { key: getKey(item) }, renderItem(item, { dragging: false, overlay: false }))
)
)
},
NormalTooltip: ({ children }) => children,
Button: ({ children, onPress, disabled, isDisabled, loading, startContent, asChild, ...props }) => {
const buttonProps = { ...props, onClick: onPress ?? props.onClick, disabled: disabled || isDisabled || loading }
if (asChild && React.isValidElement(children)) {
const childProps = children.props || {}
return React.cloneElement(children, {
...buttonProps,
...childProps,
className: [buttonProps.className, childProps.className].filter(Boolean).join(' ') || undefined,
style: { ...buttonProps.style, ...childProps.style },
onClick: (...args) => {
buttonProps.onClick?.(...args)
childProps.onClick?.(...args)
},
onKeyDown: (...args) => {
buttonProps.onKeyDown?.(...args)
childProps.onKeyDown?.(...args)
}
})
}
return React.createElement('button', buttonProps, startContent, children)
},
Input: ({ hasError, 'aria-invalid': ariaInvalid, className, list, ...props }) =>
React.createElement('input', {
...props,
list,
'aria-invalid': ariaInvalid,
className: [
className,
hasError || ariaInvalid ? 'ant-input-status-error' : undefined,
list && ariaInvalid ? 'ant-select-status-error' : undefined
]
.filter(Boolean)
.join(' ')
}),
Textarea: {
Input: ({ hasError, 'aria-invalid': ariaInvalid, className, onValueChange, onChange, ...props }) =>
React.createElement('textarea', {
...props,
'aria-invalid': ariaInvalid,
className: [className, hasError || ariaInvalid ? 'ant-input-status-error' : undefined]
.filter(Boolean)
.join(' '),
onChange: (event: React.ChangeEvent<HTMLTextAreaElement>) => {
onChange?.(event)
onValueChange?.(event.target.value)
}
})
},
Accordion: ({ children, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'accordion' }, children),
AccordionItem: ({ children, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'accordion-item' }, children),
AccordionTrigger: ({ children, disabled, ...props }) =>
React.createElement(
'button',
{ ...props, type: 'button', disabled, 'data-testid': 'accordion-trigger' },
children
),
AccordionContent: ({ children, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'accordion-content' }, children),
DropdownMenu: ({ children, onOpenChange }) =>
React.createElement(
DropdownMenuOpenContext.Provider,
{ value: onOpenChange ?? null },
React.createElement('div', null, children)
),
DropdownMenuTrigger: ({ children }) => {
const onOpenChange = React.use(DropdownMenuOpenContext)
return React.createElement('span', { onClick: () => onOpenChange?.(true) }, children)
},
DropdownMenuContent: ({ children }) => React.createElement('div', null, children),
DropdownMenuSeparator: () => React.createElement('hr'),
DropdownMenuSub: ({ children }) => React.createElement('div', null, children),
DropdownMenuSubContent: ({ children }) => React.createElement('div', null, children),
DropdownMenuSubTrigger: ({ children }) => React.createElement('div', null, children),
DropdownMenuCheckboxItem: ({ children, disabled, onCheckedChange }) =>
React.createElement('button', { type: 'button', disabled, onClick: onCheckedChange }, children),
DropdownMenuItem: ({ children, disabled, onSelect }) =>
React.createElement('button', { type: 'button', disabled, onClick: onSelect }, children),
ContextMenu: ({ children, defaultOpen = false, open: controlledOpen, onOpenChange, ...props }) => {
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
const open = controlledOpen ?? uncontrolledOpen
const handleOpenChange = (nextOpen: boolean) => {
if (controlledOpen === undefined) {
setUncontrolledOpen(nextOpen)
}
onOpenChange?.(nextOpen)
}
return React.createElement(
ContextMenuContext.Provider,
{ value: { open, onOpenChange: handleOpenChange } },
React.createElement('div', { ...props, 'data-testid': 'context-menu' }, children)
)
},
ContextMenuTrigger: ({ children, asChild, ...props }) => {
const context = React.useContext(ContextMenuContext)
const triggerProps = {
...props,
'data-testid': 'context-menu-trigger',
onContextMenu: (event: React.MouseEvent) => {
props.onContextMenu?.(event)
if (!event.defaultPrevented && !props.disabled) {
context.onOpenChange?.(true)
event.preventDefault()
}
}
}
if (asChild && React.isValidElement(children)) {
const childProps = children.props || {}
return React.cloneElement(children, {
...triggerProps,
...childProps,
onContextMenu: (event: React.MouseEvent) => {
childProps.onContextMenu?.(event)
if (!event.defaultPrevented) {
triggerProps.onContextMenu(event)
}
}
})
}
return React.createElement('div', triggerProps, children)
},
ContextMenuContent: ({ children, ...props }) => {
const context = React.useContext(ContextMenuContext)
return context.open
? React.createElement('div', { ...props, 'data-testid': 'context-menu-content' }, children)
: null
},
ContextMenuItem: ({ children, onSelect, ...props }) => {
const context = React.useContext(ContextMenuContext)
return React.createElement(
'button',
{
...props,
type: 'button',
onClick: (event: React.MouseEvent) => {
onSelect?.(event)
context.onOpenChange?.(false)
},
'data-testid': 'context-menu-item'
},
children
)
},
ContextMenuItemContent: ({ badge, children, icon, shortcut, ...props }) =>
React.createElement(
React.Fragment,
null,
React.createElement('span', { ...props }, icon, children),
badge,
shortcut ? React.createElement('span', null, shortcut) : null
),
ContextMenuSeparator: (props) => React.createElement('div', { ...props, 'data-testid': 'context-menu-separator' }),
ContextMenuSub: ({ children, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'context-menu-sub' }, children),
ContextMenuSubTrigger: ({ children, ...props }) =>
React.createElement('button', { ...props, type: 'button', 'data-testid': 'context-menu-sub-trigger' }, children),
ContextMenuSubContent: ({ children, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'context-menu-sub-content' }, children),
ImagePreviewContextMenu: ({ actions = [], children, context, item }) =>
React.createElement(
'div',
{ 'data-testid': 'image-preview-context-menu' },
children,
actions.map((action) =>
React.createElement(
'button',
{
disabled: action.disabled,
key: action.id,
onClick: () => action.onSelect?.(item, context),
type: 'button'
},
action.icon,
action.label
)
)
),
ImagePreviewDialog: ({ activeIndex = 0, items = [], labels = {}, onOpenChange, open, toolbarActions = [] }) =>
open
? React.createElement(
'div',
{ 'data-testid': 'image-preview-dialog' },
items[activeIndex]
? React.createElement('img', {
alt: items[activeIndex].alt,
src: items[activeIndex].src
})
: null,
toolbarActions.map((action) =>
React.createElement(
'button',
{
disabled: action.disabled,
key: action.id,
onClick: () =>
action.onSelect?.(items[activeIndex], {
close: () => onOpenChange?.(false),
index: activeIndex,
items,
resetTransform: vi.fn(),
transform: { flipX: false, flipY: false, rotate: 0, scale: 1 }
}),
type: 'button'
},
action.icon,
action.label
)
),
React.createElement(
'button',
{ 'aria-label': labels.close, onClick: () => onOpenChange?.(false), type: 'button' },
labels.close
)
)
: null,
ImagePreviewImage: ({ item, ...props }) =>
React.createElement('img', { ...props, alt: item?.alt, src: item?.src, 'data-testid': 'image-preview-image' }),
ImagePreviewToolbar: ({ actions = [], context, item, labels = {}, onClose }) =>
React.createElement(
'div',
{ 'data-testid': 'image-preview-toolbar' },
actions.map((action) =>
React.createElement(
'button',
{
disabled: action.disabled,
key: action.id,
onClick: () => action.onSelect?.(item, context),
type: 'button'
},
action.icon,
action.label
)
),
React.createElement('button', { 'aria-label': labels.close, onClick: onClose, type: 'button' }, labels.close)
),
ImagePreviewTrigger: ({ alt, dialogProps: _dialogProps, item, items: _items, ...props }) =>
React.createElement('img', { ...props, alt: alt ?? item?.alt, src: item?.src }),
Dialog: ({ children, onOpenChange: _onOpenChange, open, ...props }) =>
open ? React.createElement('div', { ...props, role: 'dialog', 'data-testid': 'dialog' }, children) : null,
DialogContent: ({ children, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'dialog-content' }, children),
DialogHeader: ({ children, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'dialog-header' }, children),
DialogTitle: ({ children, ...props }) =>
React.createElement('h2', { ...props, 'data-testid': 'dialog-title' }, children),
DialogDescription: ({ children, ...props }) =>
React.createElement('p', { ...props, 'data-testid': 'dialog-description' }, children),
DialogFooter: ({ children, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'dialog-footer' }, children),
Form: ({ children }) => React.createElement(React.Fragment, null, children),
Label: ({ children, ...props }) => React.createElement('label', props, children),
FieldError: ({ children, errors, ...props }) => {
const errorMessage = children ?? errors?.find((error) => error?.message)?.message
return errorMessage ? React.createElement('div', { ...props, role: 'alert' }, errorMessage) : null
},
Popover: ({ children, open = false, onOpenChange, ...props }) =>
React.createElement(
PopoverContext.Provider,
{ value: { open, onOpenChange } },
React.createElement('div', { ...props, 'data-testid': 'popover' }, children)
),
PopoverAnchor: ({ children, asChild, ...props }) => {
if (asChild && React.isValidElement(children)) {
return React.cloneElement(children, { ...props, ...children.props })
}
return React.createElement('div', props, children)
},
PopoverTrigger: ({ children, asChild, ...props }) => {
const context = React.useContext(PopoverContext)
const triggerProps = {
...props,
'data-testid': 'popover-trigger',
onClick: (event: React.MouseEvent) => {
props.onClick?.(event)
context.onOpenChange?.(!context.open)
}
}
if (asChild && React.isValidElement(children)) {
return React.cloneElement(children, { ...triggerProps, ...children.props })
}
return React.createElement('div', triggerProps, children)
},
PopoverContent: ({ children, align, side, sideOffset, ...props }) => {
const context = React.useContext(PopoverContext)
return context.open ? React.createElement('div', { ...props, 'data-testid': 'popover-content' }, children) : null
},
MenuList: ({ children, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'menu-list' }, children),
MenuDivider: (props) => React.createElement('div', { ...props, 'data-testid': 'menu-divider' }),
MenuItem: ({ children, icon, label, onClick, ...props }) =>
React.createElement(
'button',
{ ...props, type: 'button', onClick, 'data-testid': 'menu-item' },
icon,
label,
children
),
Badge: ({ children, ...props }) => React.createElement('span', { ...props, 'data-testid': 'badge' }, children),
Separator: (props) => React.createElement('hr', { ...props, 'data-testid': 'separator' }),
Scrollbar: ({ children, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'scrollbar' }, children),
Dropzone: ({ children, getFilesFromEvent: _getFilesFromEvent, onDrop: _onDrop, maxFiles: _maxFiles, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'dropzone' }, children),
DropzoneEmptyState: ({ children }) => React.createElement(React.Fragment, null, children),
Kbd: ({ children, ...props }) => React.createElement('kbd', { ...props }, children),
Checkbox: ({ checked, onCheckedChange, ...props }) =>
React.createElement('input', {
...props,
checked,
type: 'checkbox',
'data-slot': 'checkbox',
onChange: (event: React.ChangeEvent<HTMLInputElement>) => onCheckedChange?.(event.target.checked)
}),
RadioGroup: ({ children, value, onValueChange, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'radio-group', 'data-value': value }, children),
RadioGroupItem: ({ value, ...props }) =>
React.createElement('input', { ...props, type: 'radio', value, 'data-testid': 'radio-group-item' }),
Slider: ({ value, defaultValue, onValueChange, onValueCommit, ...props }) =>
React.createElement('input', {
...props,
type: 'range',
value: value?.[0] ?? defaultValue?.[0] ?? 0,
'data-testid': 'slider',
onChange: (event: React.ChangeEvent<HTMLInputElement>) => onValueChange?.([Number(event.target.value)]),
onMouseUp: (event: React.MouseEvent<HTMLInputElement>) =>
onValueCommit?.([Number((event.target as HTMLInputElement).value)])
}),
SegmentedControl: ({ options = [], value, onValueChange, ...props }) =>
React.createElement(
'div',
{ ...props, 'data-testid': 'segmented-control', 'data-value': value },
options.map((option) =>
React.createElement(
'button',
{
key: option.value,
type: 'button',
disabled: option.disabled,
onClick: () => onValueChange?.(option.value)
},
option.label
)
)
),
Select: ({ children, value, onValueChange, ...props }) => {
return React.createElement(
SelectContext.Provider,
{ value: { value, onValueChange } },
React.createElement('div', { ...props, 'data-testid': 'select', 'data-value': value }, children)
)
},
SelectTrigger: ({ children, ...props }) =>
React.createElement('button', { ...props, type: 'button', 'data-testid': 'select-trigger' }, children),
SelectValue: ({ children, placeholder, ...props }) =>
React.createElement('span', { ...props, 'data-testid': 'select-value' }, children ?? placeholder),
SelectContent: ({ children, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'select-content' }, children),
SelectItem: ({ children, value, ...props }) => {
const context = React.useContext(SelectContext)
return React.createElement(
'button',
{
...props,
type: 'button',
'data-testid': 'select-item',
'data-value': value,
onClick: () => context.onValueChange?.(value)
},
children
)
},
Combobox: ({ options = [], value, onChange, onValueChange, placeholder, disabled, ...props }) =>
React.createElement(
'select',
{
...props,
disabled,
value: value ?? '',
'data-testid': 'combobox',
onChange: (event: React.ChangeEvent<HTMLSelectElement>) => {
onChange?.(event.target.value)
onValueChange?.(event.target.value)
}
},
React.createElement('option', { value: '' }, placeholder),
options.map((option) =>
React.createElement(
'option',
{ key: option.value, value: option.value, disabled: option.disabled },
option.label
)
)
),
Tooltip: ({ children, title, content, mouseEnterDelay, classNames, className, ...props }) => {
// Support both old (title) and new (content) API
const tooltipText = content || title
// Mirror the real Tooltip: the trigger wrapper carries classNames.placeholder.
const wrapperClassName = [className, classNames?.placeholder].filter(Boolean).join(' ') || undefined
return React.createElement(
'div',
{
...props,
...(wrapperClassName && { className: wrapperClassName }),
'data-testid': 'tooltip',
...(tooltipText && { 'data-title': tooltipText }),
'data-mouse-enter-delay': mouseEnterDelay,
className: classNames?.placeholder
},
children,
tooltipText ? React.createElement('div', { 'data-testid': 'tooltip-content' }, tooltipText) : null
)
},
CircularProgress: ({ value, renderLabel, showLabel, ...props }) =>
React.createElement(
'div',
{ ...props, 'data-testid': 'circular-progress', 'data-value': value },
showLabel ? (renderLabel ? renderLabel(value) : value) : null
),
CustomTag: ({
children,
icon,
color,
size = 12,
style,
tooltip,
closable,
onClose,
onClick,
onContextMenu,
disabled,
inactive,
className,
...props
}) => {
const actualColor = inactive ? '#aaaaaa' : color
const tag = React.createElement(
'div',
{
...props,
className,
style: {
padding: `${size / 3}px ${closable ? size * 1.8 : size * 0.8}px ${size / 3}px ${size * 0.8}px`,
color: actualColor,
backgroundColor: actualColor + '20',
fontSize: `${size}px`,
lineHeight: 1,
cursor: disabled ? 'not-allowed' : onClick ? 'pointer' : 'auto',
...style
},
onClick: disabled ? undefined : onClick,
onContextMenu: disabled ? undefined : onContextMenu
},
icon,
children,
closable
? React.createElement(
'button',
{
type: 'button',
onClick: (event) => {
event.stopPropagation()
onClose?.()
}
},
'x'
)
: null
)
return tooltip
? React.createElement(
'div',
{ 'data-testid': 'tooltip', 'data-title': tooltip },
tag,
React.createElement('div', { 'data-testid': 'tooltip-content' }, tooltip)
)
: tag
},
Spinner: ({ text, ...props }) => React.createElement('div', { ...props, 'data-testid': 'spinner' }, text),
CodeEditor: ({ children, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'code-editor' }, children),
Flex: ({ children, ...props }) => React.createElement('div', { ...props, 'data-testid': 'flex' }, children),
ExpandableText: ({ children, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'expandable-text' }, children),
// Add other commonly used UI components
Box: ({ children, ...props }) => React.createElement('div', { ...props, 'data-testid': 'box' }, children),
Center: ({ children, ...props }) => React.createElement('div', { ...props, 'data-testid': 'center' }, children),
ColFlex: ({ children, ...props }) => React.createElement('div', { ...props, 'data-testid': 'col-flex' }, children),
RowFlex: ({ children, ...props }) => React.createElement('div', { ...props, 'data-testid': 'row-flex' }, children),
SpaceBetweenRowFlex: ({ children, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'space-between-row-flex' }, children),
Ellipsis: ({ children, ...props }) => React.createElement('div', { ...props, 'data-testid': 'ellipsis' }, children),
TextBadge: ({ children, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'text-badge' }, children),
Badge: ({ children, ...props }) => React.createElement('span', { ...props, 'data-testid': 'badge' }, children),
EmptyState: ({ title, description, actionLabel, onAction, secondaryLabel, onSecondary, ...props }) =>
React.createElement(
'div',
{ ...props, 'data-testid': 'empty-state' },
title ? React.createElement('div', {}, title) : null,
description ? React.createElement('div', {}, description) : null,
actionLabel && onAction
? React.createElement('button', { type: 'button', onClick: onAction }, actionLabel)
: null,
secondaryLabel && onSecondary
? React.createElement('button', { type: 'button', onClick: onSecondary }, secondaryLabel)
: null
),
Alert: ({ children, message, description, type, ...props }) =>
React.createElement(
'div',
{ ...props, role: 'alert', 'data-testid': 'alert', 'data-type': type },
message,
description,
children
),
EditableNumber: ({ value, onChange, disabled, ...props }) =>
React.createElement('input', {
...props,
type: 'number',
value: value ?? '',
disabled,
'data-testid': 'editable-number',
onChange: (event: React.ChangeEvent<HTMLInputElement>) =>
onChange?.(event.target.value === '' ? null : event.target.valueAsNumber)
}),
Skeleton: ({ children, ...props }) => React.createElement('div', { ...props, 'data-testid': 'skeleton' }, children),
EmptyState: ({ children, title, description, preset, ...props }) =>
React.createElement(
'div',
{ ...props, 'data-testid': 'empty-state', 'data-preset': preset },
title ? React.createElement('div', {}, title) : null,
description ? React.createElement('div', {}, description) : null,
children
),
HelpTooltip: ({ children, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'help-tooltip' }, children),
InfoTooltip: ({ children, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'info-tooltip' }, children),
Scrollbar: ({ children, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'scrollbar' }, children),
Avatar: ({ children, src, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'avatar' }, src ? null : children),
AvatarImage: ({ src, ...props }) =>
React.createElement('img', { ...props, src, alt: '', 'data-testid': 'avatar-image' }),
AvatarFallback: ({ children, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'avatar-fallback' }, children),
EmojiAvatar: ({ children, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'emoji-avatar' }, children),
EmojiIcon: ({ emoji, className, fluid, fontSize }) =>
React.createElement(
'div',
{
className,
'data-testid': 'emoji-icon',
...(fluid !== undefined ? { 'data-fluid': String(fluid) } : {}),
...(fontSize !== undefined ? { 'data-font-size': String(fontSize) } : {})
},
React.createElement('span', { 'aria-hidden': 'true', 'data-testid': 'emoji-icon-background' }, emoji || '⭐️'),
emoji
),
Switch: ({ isSelected, onValueChange, ...props }) =>
React.createElement('input', {
...props,
type: 'checkbox',
checked: isSelected,
onChange: (e) => onValueChange?.(e.target.checked),
'data-testid': 'switch'
}),
// Popover primitives — Radix-style trigger / content split
Popover: ({ children, ...props }) => React.createElement('div', { ...props, 'data-testid': 'popover' }, children),
PopoverTrigger: ({ children, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'popover-trigger' }, children),
PopoverContent: ({ children, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'popover-content' }, children),
Skeleton: ({ children, ...props }) => React.createElement('div', { ...props, 'data-testid': 'skeleton' }, children),
// Icon registry stubs
PROVIDER_ICON_CATALOG: {},
MODEL_ICON_CATALOG: {},
resolveProviderIcon: () => undefined,
resolveModelIcon: () => undefined,
resolveModelToProviderIcon: () => undefined,
resolveIcon: () => undefined
}
})
if (typeof globalThis.localStorage === 'undefined' || typeof (globalThis.localStorage as any).getItem !== 'function') {
let store = new Map<string, string>()
const localStorageMock = {
getItem: (key: string) => store.get(key) ?? null,
setItem: (key: string, value: string) => {
store.set(key, String(value))
},
removeItem: (key: string) => {
store.delete(key)
},
clear: () => {
store.clear()
},
key: (index: number) => Array.from(store.keys())[index] ?? null,
get length() {
return store.size
}
}
vi.stubGlobal('localStorage', localStorageMock)
if (typeof window !== 'undefined') {
Object.defineProperty(window, 'localStorage', { value: localStorageMock })
}
}