mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-22 05:56:58 +08:00
统一按钮、输入框、选择控件和开关的交互状态 优化日报、导出、设置、Agent Hub 和 API Center 的UI 完善组件、单元及 Electron E2E 测试
195 lines
7.1 KiB
TypeScript
195 lines
7.1 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'
|
|
|
|
const runtime: AIRuntimeModelConfig = {
|
|
providerName: 'Not configured',
|
|
model: '',
|
|
modelName: 'No model selected',
|
|
configured: false,
|
|
status: 'untested'
|
|
}
|
|
|
|
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)
|
|
} 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')
|
|
})
|
|
|
|
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')
|
|
})
|
|
})
|