mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-03 12:27:41 +08:00
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com> Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
792 lines
32 KiB
TypeScript
792 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 }))
|
|
)
|
|
),
|
|
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 })
|
|
}
|
|
}
|