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() 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('does not reload providers when the parent callback identity changes', async () => { const firstRuntimeChange = vi.fn() const secondRuntimeChange = vi.fn() const { rerender } = render( ) await act(async () => { await Promise.resolve() }) expect(window.api.listAIProviders).toHaveBeenCalledOnce() expect(window.api.getAIRuntimeConfig).toHaveBeenCalledOnce() expect(firstRuntimeChange).toHaveBeenCalledWith(runtime) await act(async () => { rerender() 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() 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( ) 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( ) 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') }) })