mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-04 03:01:42 +08:00
208 lines
7.7 KiB
TypeScript
208 lines
7.7 KiB
TypeScript
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||
import userEvent from '@testing-library/user-event'
|
||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||
import { AIImageUnderstandingTest } from '../../src/renderer/src/features/settings/ai-model/AIImageUnderstandingTest'
|
||
import { AIProviderCard } from '../../src/renderer/src/features/settings/ai-model/AIProviderCard'
|
||
import { AIModelPage } from '../../src/renderer/src/features/settings/pages/AIModelPage'
|
||
import type { AIProviderSummary, AIRuntimeModelConfig } from '../../src/shared/ai-provider'
|
||
import type { AIVisionTestState } from '../../src/renderer/src/features/settings/ai-model/types'
|
||
import type { SystemOcrCapability } from '../../src/shared/system-ocr'
|
||
|
||
const runtime: AIRuntimeModelConfig = {
|
||
providerName: 'Not configured',
|
||
model: '',
|
||
modelName: 'No model selected',
|
||
configured: false,
|
||
status: 'untested'
|
||
}
|
||
|
||
const systemOcrCapability: SystemOcrCapability = {
|
||
available: true,
|
||
engine: 'windows-system-ocr',
|
||
platform: 'win32',
|
||
arch: 'x64',
|
||
runtimeVersion: '1.2.0',
|
||
language: 'zh-Hans-CN',
|
||
message: '本地图片文字识别可用(Windows 系统 OCR,zh-Hans-CN)。'
|
||
}
|
||
|
||
const provider: AIProviderSummary = {
|
||
id: 'fixture-provider',
|
||
name: '本地假服务',
|
||
type: 'openai-compatible',
|
||
baseUrl: 'http://127.0.0.1:1/v1',
|
||
auth: { type: 'none' },
|
||
models: [
|
||
{
|
||
id: 'fixture-model',
|
||
name: '固定响应模型',
|
||
capabilities: { chat: true, vision: true, ocr: true, longContext: true }
|
||
}
|
||
],
|
||
defaultModel: 'fixture-model',
|
||
advanced: { timeoutMs: 5000, extraHeaders: {} },
|
||
hasApiKey: true,
|
||
isDefault: false,
|
||
status: 'connected'
|
||
}
|
||
|
||
const visionState: AIVisionTestState = {
|
||
status: 'ready',
|
||
prompt: '描述图片',
|
||
image: {
|
||
dataUrl: 'data:image/png;base64,fixture',
|
||
fileName: 'fixture.png',
|
||
mimeType: 'image/png',
|
||
size: 1024
|
||
}
|
||
}
|
||
|
||
describe('AI model settings', () => {
|
||
const scrollIntoView = vi.fn()
|
||
|
||
beforeEach(() => {
|
||
Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
|
||
configurable: true,
|
||
value: scrollIntoView
|
||
})
|
||
window.api = {
|
||
listAIProviders: vi.fn().mockResolvedValue({ success: true, providers: [] }),
|
||
getAIRuntimeConfig: vi.fn().mockResolvedValue(runtime),
|
||
getSystemOcrCapability: vi.fn().mockResolvedValue(systemOcrCapability)
|
||
} as typeof window.api
|
||
})
|
||
|
||
it('reveals the new provider editor and labels model identity fields', async () => {
|
||
render(<AIModelPage onRuntimeChange={vi.fn()} onNotice={vi.fn()} />)
|
||
|
||
await act(async () => {
|
||
await Promise.resolve()
|
||
})
|
||
fireEvent.click(screen.getByRole('button', { name: '添加供应商' }))
|
||
|
||
expect(scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth', block: 'start' })
|
||
expect(screen.getByLabelText('供应商名称')).toHaveFocus()
|
||
expect(screen.getByLabelText('模型名称')).toHaveAttribute('placeholder', '例如:DeepSeek Chat')
|
||
expect(screen.getByLabelText('模型 ID')).toHaveAttribute('placeholder', '例如:deepseek-chat')
|
||
expect(screen.getByRole('combobox', { name: '思考模式' })).toHaveTextContent('跟随模型默认')
|
||
})
|
||
|
||
it('reveals and focuses the provider editor when editing an existing provider', async () => {
|
||
vi.mocked(window.api.listAIProviders).mockResolvedValue({
|
||
success: true,
|
||
providers: [provider]
|
||
})
|
||
render(<AIModelPage onRuntimeChange={vi.fn()} onNotice={vi.fn()} />)
|
||
|
||
await act(async () => {
|
||
await Promise.resolve()
|
||
})
|
||
await userEvent.click(screen.getByRole('button', { name: '编辑' }))
|
||
|
||
expect(scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth', block: 'start' })
|
||
expect(screen.getByLabelText('供应商名称')).toHaveFocus()
|
||
expect(screen.getByLabelText('供应商名称')).toHaveValue(provider.name)
|
||
})
|
||
|
||
it('does not reload providers when the parent callback identity changes', async () => {
|
||
const firstRuntimeChange = vi.fn()
|
||
const secondRuntimeChange = vi.fn()
|
||
const { rerender } = render(
|
||
<AIModelPage onRuntimeChange={firstRuntimeChange} onNotice={vi.fn()} />
|
||
)
|
||
|
||
await act(async () => {
|
||
await Promise.resolve()
|
||
})
|
||
expect(window.api.listAIProviders).toHaveBeenCalledOnce()
|
||
expect(window.api.getAIRuntimeConfig).toHaveBeenCalledOnce()
|
||
expect(firstRuntimeChange).toHaveBeenCalledWith(runtime)
|
||
|
||
await act(async () => {
|
||
rerender(<AIModelPage onRuntimeChange={secondRuntimeChange} onNotice={vi.fn()} />)
|
||
await Promise.resolve()
|
||
})
|
||
|
||
expect(window.api.listAIProviders).toHaveBeenCalledOnce()
|
||
expect(window.api.getAIRuntimeConfig).toHaveBeenCalledOnce()
|
||
expect(secondRuntimeChange).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('keeps preset selection and derived vision capabilities intact', async () => {
|
||
const user = userEvent.setup()
|
||
render(<AIModelPage onRuntimeChange={vi.fn()} onNotice={vi.fn()} />)
|
||
|
||
await act(async () => {
|
||
await Promise.resolve()
|
||
})
|
||
await user.click(screen.getByRole('button', { name: '添加供应商' }))
|
||
await user.click(screen.getByRole('combobox', { name: '快速模板' }))
|
||
await user.click(screen.getByRole('option', { name: 'OpenAI', exact: true }))
|
||
|
||
expect(screen.getByLabelText('供应商名称')).toHaveValue('OpenAI')
|
||
expect(screen.getByLabelText('模型名称')).toHaveValue('gpt-4o-mini')
|
||
const vision = screen.getByRole('checkbox', { name: '图片理解 gpt-4o-mini' })
|
||
const ocr = screen.getByRole('checkbox', { name: '图片文字识别 gpt-4o-mini' })
|
||
expect(vision).not.toBeChecked()
|
||
expect(ocr).not.toBeChecked()
|
||
await user.click(vision)
|
||
expect(vision).toBeChecked()
|
||
expect(ocr).toBeChecked()
|
||
expect(screen.getByRole('radio', { name: '默认 gpt-4o-mini' })).toBeChecked()
|
||
})
|
||
|
||
it('uses shared button variants for provider card actions', async () => {
|
||
const user = userEvent.setup()
|
||
const onEdit = vi.fn()
|
||
const onTest = vi.fn()
|
||
const onDefault = vi.fn()
|
||
const onDelete = vi.fn()
|
||
render(
|
||
<AIProviderCard
|
||
provider={provider}
|
||
testing={false}
|
||
onEdit={onEdit}
|
||
onTest={onTest}
|
||
onDefault={onDefault}
|
||
onDelete={onDelete}
|
||
/>
|
||
)
|
||
|
||
await user.click(screen.getByRole('button', { name: '编辑' }))
|
||
await user.click(screen.getByRole('button', { name: '测试连接' }))
|
||
await user.click(screen.getByRole('button', { name: '设为默认' }))
|
||
await user.click(screen.getByRole('button', { name: '删除' }))
|
||
expect(onEdit).toHaveBeenCalledOnce()
|
||
expect(onTest).toHaveBeenCalledOnce()
|
||
expect(onDefault).toHaveBeenCalledOnce()
|
||
expect(onDelete).toHaveBeenCalledOnce()
|
||
expect(screen.getByRole('button', { name: '删除' })).toHaveClass('bg-destructive')
|
||
})
|
||
|
||
it('keeps vision file input native while using shared prompt and action controls', async () => {
|
||
const user = userEvent.setup()
|
||
const onPromptChange = vi.fn()
|
||
const onTest = vi.fn()
|
||
const onClear = vi.fn()
|
||
render(
|
||
<AIImageUnderstandingTest
|
||
runtime={{ ...runtime, configured: true }}
|
||
provider={provider}
|
||
state={visionState}
|
||
onSelectImage={vi.fn()}
|
||
onPromptChange={onPromptChange}
|
||
onTest={onTest}
|
||
onClear={onClear}
|
||
/>
|
||
)
|
||
|
||
expect(screen.getByRole('img', { name: '图片理解测试预览' })).toBeInTheDocument()
|
||
expect(screen.getByLabelText('识别提示词')).toHaveValue('描述图片')
|
||
await user.click(screen.getByRole('button', { name: '移除图片' }))
|
||
await user.click(screen.getByRole('button', { name: '开始识别' }))
|
||
expect(onClear).toHaveBeenCalledOnce()
|
||
expect(onTest).toHaveBeenCalledOnce()
|
||
expect(screen.getByRole('button', { name: '开始识别' })).not.toHaveClass('database-key-primary')
|
||
})
|
||
})
|