refactor(ui): 统一迁移原生按钮、输入框及交互控件

This commit is contained in:
Wxw-Gu
2026-08-20 10:18:59 +08:00
parent e9cc996ebb
commit 65830d70fe
87 changed files with 3685 additions and 2768 deletions
@@ -0,0 +1,64 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import { AccountOverview } from '../../src/renderer/src/features/settings/account-database/AccountOverview'
import { TooltipProvider } from '../../src/renderer/src/components/ui'
describe('account database controls', () => {
it('keeps overview actions and disabled state connected', async () => {
const user = userEvent.setup()
const onCheck = vi.fn()
const onOpenDirectory = vi.fn()
const onCopyDirectory = vi.fn()
const onSwitchAccount = vi.fn()
render(
<TooltipProvider>
<AccountOverview
selfInfo={{
wxid: 'wxid_fixture',
nickname: '测试账号',
accountRoot: '/fixture/account'
}}
connectionStatus="success"
lastCheckedLabel="刚刚"
isChecking={false}
onCheck={onCheck}
onOpenDirectory={onOpenDirectory}
onCopyDirectory={onCopyDirectory}
onSwitchAccount={onSwitchAccount}
/>
</TooltipProvider>
)
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(onCheck).toHaveBeenCalledOnce()
expect(onOpenDirectory).toHaveBeenCalledOnce()
expect(onSwitchAccount).toHaveBeenCalledOnce()
expect(onCopyDirectory).toHaveBeenCalledOnce()
})
it('disables directory actions when no account root is available', () => {
render(
<TooltipProvider>
<AccountOverview
selfInfo={null}
connectionStatus="unavailable"
lastCheckedLabel="尚未检测"
isChecking
onCheck={vi.fn()}
onOpenDirectory={vi.fn()}
onCopyDirectory={vi.fn()}
onSwitchAccount={vi.fn()}
/>
</TooltipProvider>
)
expect(screen.getByRole('button', { name: '正在检测...' })).toBeDisabled()
expect(screen.getByRole('button', { name: '打开账号目录' })).toBeDisabled()
expect(screen.getByRole('button', { name: '复制账号目录' })).toBeDisabled()
})
})
@@ -0,0 +1,78 @@
import { act, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AgentHubWorkspace } from '../../src/renderer/src/features/agent-hub/AgentHubWorkspace'
import type { AgentHubStatus } from '../../src/shared/agent-hub'
const onlineStatus: AgentHubStatus = {
hub: 'online',
connector: 'online',
updatedAt: 1,
dataApi: 'online',
databaseReady: true,
accountId: 'fixture-agent'
}
describe('Agent Hub controls', () => {
beforeEach(() => {
window.api = {
getAgentHubStatus: vi.fn().mockResolvedValue(onlineStatus),
getAgentHubLogs: vi.fn().mockResolvedValue([
{ id: 1, timestamp: 1, source: 'system', level: 'info', message: '系统就绪' },
{
id: 2,
timestamp: 2,
source: 'wechat-connector',
level: 'warn',
message: '等待连接'
}
]),
onAgentHubStatus: vi.fn(() => () => undefined),
onAgentHubLog: vi.fn(() => () => undefined),
copyText: vi.fn().mockResolvedValue(undefined),
clearAgentHubLogs: vi.fn().mockResolvedValue({ success: true }),
startAgentHubLogin: vi.fn().mockResolvedValue({ status: onlineStatus }),
cancelAgentHubLogin: vi.fn().mockResolvedValue({ status: onlineStatus }),
disconnectAgentHub: vi.fn().mockResolvedValue({
status: { ...onlineStatus, connector: 'disconnected' }
})
} as typeof window.api
})
it('filters, copies, and clears logs through shared controls', async () => {
const user = userEvent.setup()
render(<AgentHubWorkspace />)
await screen.findByText('系统就绪')
await user.click(screen.getByRole('combobox', { name: '筛选日志来源' }))
await user.click(screen.getByRole('option', { name: '系统', exact: true }))
expect(screen.getByText('系统就绪')).toBeInTheDocument()
expect(screen.queryByText('等待连接')).not.toBeInTheDocument()
await user.click(screen.getByRole('button', { name: '复制日志' }))
expect(window.api.copyText).toHaveBeenCalledWith(expect.stringContaining('系统就绪'))
expect(window.api.copyText).toHaveBeenCalledWith(expect.not.stringContaining('等待连接'))
await user.click(screen.getByRole('button', { name: '清空' }))
expect(window.api.clearAgentHubLogs).toHaveBeenCalledOnce()
expect(screen.getByText(/暂无运行日志/)).toBeInTheDocument()
expect(screen.getByRole('button', { name: '复制日志' })).toBeDisabled()
})
it('keeps reconnect and destructive disconnect actions separate', async () => {
const user = userEvent.setup()
render(<AgentHubWorkspace />)
await screen.findByText('微信机器人已连接')
await user.click(screen.getByRole('button', { name: '重新扫码登录' }))
expect(window.api.startAgentHubLogin).toHaveBeenCalledOnce()
const disconnect = screen.getByRole('button', { name: '断开连接' })
expect(disconnect).toHaveClass('bg-destructive')
await user.click(disconnect)
await act(async () => {
await Promise.resolve()
})
expect(window.api.disconnectAgentHub).toHaveBeenCalledOnce()
})
})
+113 -1
View File
@@ -1,7 +1,11 @@
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 { AIRuntimeModelConfig } from '../../src/shared/ai-provider'
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',
@@ -11,6 +15,37 @@ const runtime: AIRuntimeModelConfig = {
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()
@@ -62,4 +97,81 @@ describe('AI model settings', () => {
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')
})
})
@@ -156,6 +156,50 @@ describe('AISearchWorkspace regression coverage before decomposition', () => {
void user
})
it('keeps result actions and model settings callbacks unchanged after UI migration', async () => {
const onOpenAISettings = vi.fn()
const user = userEvent.setup()
const view = renderWorkspace({ onOpenAISettings })
await submitQuery()
await screen.findByText('测试搜索答案')
await user.click(screen.getByRole('button', { name: '复制摘要' }))
expect(api.copyText).toHaveBeenCalledWith('测试搜索答案')
view.rerender(
<AISearchWorkspace
{...(makeProps({
onOpenAISettings,
aiModelConfig: {
configured: false,
providerName: '',
model: '',
modelName: '',
status: 'unconfigured' as const
}
}) as never)}
/>
)
await user.click(screen.getByRole('button', { name: '配置模型' }))
expect(onOpenAISettings).toHaveBeenCalledOnce()
})
it('exposes the diagnostics action as an expanded control and keeps log access unchanged', async () => {
api.getSettings.mockResolvedValue({ settings: { debugEnabled: true } })
api.getAppLogPath.mockResolvedValue('/fixture/app.log')
renderWorkspace()
const user = userEvent.setup()
const diagnostics = await screen.findByRole('button', { name: '诊断日志' })
expect(diagnostics).toHaveAttribute('aria-expanded', 'false')
await user.click(diagnostics)
expect(diagnostics).toHaveAttribute('aria-expanded', 'true')
expect(screen.getByText('/fixture/app.log')).toBeInTheDocument()
await user.click(screen.getByRole('button', { name: '打开日志文件夹' }))
expect(api.revealAppLog).toHaveBeenCalledOnce()
})
it('renders a failed retrieval without producing a Summary', async () => {
api.runAiSearch.mockResolvedValue(
makeSearchResult({
@@ -0,0 +1,178 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import { ApiRequestTester } from '../../src/renderer/src/features/api-center/components/ApiRequestTester'
import { ReaderSkillOverview } from '../../src/renderer/src/features/api-center/components/ReaderSkillOverview'
import type { ApiEndpoint } from '../../src/renderer/src/features/api-center/model/types'
const requestEndpoint: ApiEndpoint = {
id: 'report',
method: 'POST',
path: '/v1/report',
name: '测试接口',
description: '测试统一表单控件',
parameters: [
{
key: 'talker',
label: '会话标识',
required: true,
placeholder: '群昵称'
}
],
body: true
}
describe('API Center unified controls', () => {
it('uses the unified menu for Reader Skill actions and restores trigger focus', async () => {
const user = userEvent.setup()
const onOpenFolder = vi.fn()
const onOpenGithub = vi.fn()
render(
<ReaderSkillOverview
skill={{
available: true,
version: 'v1.2',
source: 'bundled',
githubUrl: 'https://example.com/tracememo'
}}
service={{ running: true, host: '127.0.0.1', port: 6131 }}
dbReady
target="codex"
onTargetChange={vi.fn()}
onPreview={vi.fn()}
onOpenFolder={onOpenFolder}
onOpenGithub={onOpenGithub}
onStart={vi.fn()}
onCopyInstruction={vi.fn()}
onCopyVerification={vi.fn()}
/>
)
const trigger = screen.getByRole('button', { name: '更多' })
await user.click(trigger)
await user.click(screen.getByRole('menuitem', { name: '打开本地文件夹' }))
expect(onOpenFolder).toHaveBeenCalledOnce()
await user.click(trigger)
expect(screen.getByRole('menuitem', { name: '查看 GitHub 最新版本' })).toBeVisible()
await user.keyboard('{Escape}')
expect(screen.queryByRole('menu')).not.toBeInTheDocument()
await waitFor(() => expect(trigger).toHaveFocus())
expect(onOpenGithub).not.toHaveBeenCalled()
})
it('exposes the Agent target as a real single-select control', async () => {
const user = userEvent.setup()
const onTargetChange = vi.fn()
render(
<ReaderSkillOverview
skill={{
available: true,
version: 'v1.2',
source: 'bundled',
githubUrl: 'https://example.com/tracememo'
}}
service={{ running: true, host: '127.0.0.1', port: 6131 }}
dbReady
target="codex"
onTargetChange={onTargetChange}
onPreview={vi.fn()}
onOpenFolder={vi.fn()}
onOpenGithub={vi.fn()}
onStart={vi.fn()}
onCopyInstruction={vi.fn()}
onCopyVerification={vi.fn()}
/>
)
expect(screen.getByRole('radio', { name: 'Codex' })).toBeChecked()
await user.click(screen.getByRole('radio', { name: 'Claude Code' }))
expect(onTargetChange).toHaveBeenCalledWith('claude-code')
})
it('preserves request editing and action callbacks with unified form controls', async () => {
const user = userEvent.setup()
const onParams = vi.fn()
const onBody = vi.fn()
const onClear = vi.fn()
const onCopyCurl = vi.fn(async () => undefined)
const onSend = vi.fn()
render(
<ApiRequestTester
endpoint={requestEndpoint}
settings={{ apiEnabled: true, apiHost: '127.0.0.1', apiPort: 6131 }}
service={{ running: true, host: '127.0.0.1', port: 6131 }}
params={{}}
body="{}"
state="idle"
error=""
onParams={onParams}
onBody={onBody}
onSend={onSend}
onClear={onClear}
onCopyCurl={onCopyCurl}
/>
)
fireEvent.change(screen.getByPlaceholderText('群昵称'), {
target: { value: '产品群' }
})
expect(onParams).toHaveBeenLastCalledWith({ talker: '产品群' })
fireEvent.change(screen.getByLabelText('JSON 请求体'), {
target: { value: '{"range":"today"}' }
})
expect(onBody).toHaveBeenLastCalledWith('{"range":"today"}')
await user.click(screen.getByRole('button', { name: '清空' }))
await user.click(screen.getByRole('button', { name: '复制 curl' }))
await user.click(screen.getByRole('button', { name: '发送请求' }))
expect(onClear).toHaveBeenCalledOnce()
expect(onCopyCurl).toHaveBeenCalledOnce()
expect(onSend).toHaveBeenCalledOnce()
})
it('keeps the send action disabled and exposes loading state accessibly', () => {
const { rerender } = render(
<ApiRequestTester
endpoint={requestEndpoint}
settings={{ apiEnabled: true, apiHost: '127.0.0.1', apiPort: 6131 }}
service={{ running: false, host: '127.0.0.1', port: 6131 }}
params={{}}
body="{}"
state="idle"
error=""
onParams={vi.fn()}
onBody={vi.fn()}
onSend={vi.fn()}
onClear={vi.fn()}
onCopyCurl={vi.fn(async () => undefined)}
/>
)
expect(screen.getByRole('button', { name: '请先启动本地 API 服务' })).toBeDisabled()
rerender(
<ApiRequestTester
endpoint={requestEndpoint}
settings={{ apiEnabled: true, apiHost: '127.0.0.1', apiPort: 6131 }}
service={{ running: true, host: '127.0.0.1', port: 6131 }}
params={{}}
body="{}"
state="loading"
error=""
onParams={vi.fn()}
onBody={vi.fn()}
onSend={vi.fn()}
onClear={vi.fn()}
onCopyCurl={vi.fn(async () => undefined)}
/>
)
const sending = screen.getByRole('button', { name: '发送中…' })
expect(sending).toBeDisabled()
expect(sending).toHaveAttribute('aria-busy', 'true')
})
})
+11
View File
@@ -27,6 +27,7 @@ describe('CacheCleanupPage', () => {
updatedAt: Date.now()
})
api.openKnowledgeDirectory.mockResolvedValue({ success: true })
api.clearCache.mockResolvedValue({ items: [], totalBytes: 0, updatedAt: Date.now() })
})
it('opens the real knowledge directory without clearing it', async () => {
@@ -39,4 +40,14 @@ describe('CacheCleanupPage', () => {
expect(api.clearCache).not.toHaveBeenCalled()
expect(onNotice).toHaveBeenCalledWith('已打开知识库文件夹')
})
it('keeps the clear-all scope and notice unchanged', async () => {
const onNotice = vi.fn()
render(<CacheCleanupPage onNotice={onNotice} />)
await userEvent.click(await screen.findByRole('button', { name: '清理全部' }))
await waitFor(() => expect(api.clearCache).toHaveBeenCalledWith('all'))
expect(onNotice).toHaveBeenCalledWith('已清理全部可恢复缓存和检索记录')
})
})
+91 -3
View File
@@ -1,8 +1,11 @@
import { render, screen } from '@testing-library/react'
import type { ReactElement } from 'react'
import { render, screen, type RenderResult } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import { ChatHeader } from '../../src/renderer/src/components/chat/ChatHeader'
import { ChatStatusBar } from '../../src/renderer/src/components/chat/ChatStatusBar'
import { ExportMenu } from '../../src/renderer/src/components/chat/ExportMenu'
import { TooltipProvider } from '../../src/renderer/src/components/ui'
import type { Contact } from '../../src/shared/types'
const contact: Contact = {
@@ -12,11 +15,15 @@ const contact: Contact = {
type: 'group'
}
function renderWithTooltip(element: ReactElement): RenderResult {
return render(<TooltipProvider>{element}</TooltipProvider>)
}
describe('chat menus', () => {
it('runs the selected export range and closes the menu', async () => {
const user = userEvent.setup()
const onExport = vi.fn()
render(<ExportMenu disabled={false} onExport={onExport} />)
renderWithTooltip(<ExportMenu disabled={false} onExport={onExport} />)
const trigger = screen.getByRole('button', { name: '导出' })
await user.click(trigger)
@@ -30,7 +37,7 @@ describe('chat menus', () => {
it('closes the chat More menu with Escape and invokes refresh data once', async () => {
const user = userEvent.setup()
const onRefreshData = vi.fn()
render(
renderWithTooltip(
<ChatHeader
contact={contact}
isGroupChat
@@ -57,4 +64,85 @@ describe('chat menus', () => {
await user.click(screen.getByRole('menuitem', { name: '刷新数据' }))
expect(onRefreshData).toHaveBeenCalledOnce()
})
it('keeps header search and actions wired to their existing callbacks', async () => {
const user = userEvent.setup()
const onContentFilterChange = vi.fn()
const onRefresh = vi.fn()
const onOpenAiSettings = vi.fn()
renderWithTooltip(
<ChatHeader
contact={contact}
isGroupChat
loadedCount={8}
filteredCount={2}
contentFilter=""
isAiLoading={false}
onContentFilterChange={onContentFilterChange}
onRefresh={onRefresh}
onRefreshData={vi.fn()}
onTestSend={vi.fn()}
onOpenAiSettings={onOpenAiSettings}
/>
)
await user.click(screen.getByRole('button', { name: '搜索当前聊天' }))
const searchInput = screen.getByRole('textbox', { name: '搜索当前聊天内容' })
expect(searchInput).toHaveFocus()
await user.type(searchInput, '测试')
expect(onContentFilterChange).toHaveBeenCalledTimes(2)
expect(screen.getByRole('button', { name: '发送消息' })).toBeDisabled()
await user.click(screen.getByRole('button', { name: '关闭搜索' }))
expect(onContentFilterChange).toHaveBeenLastCalledWith('')
expect(screen.queryByRole('textbox', { name: '搜索当前聊天内容' })).not.toBeInTheDocument()
await user.click(screen.getByRole('button', { name: '刷新聊天记录' }))
await user.click(screen.getByRole('button', { name: '生成 AI 日报' }))
expect(onRefresh).toHaveBeenCalledOnce()
expect(onOpenAiSettings).toHaveBeenCalledOnce()
})
it('preserves status switch, reload, and jump callbacks', async () => {
const user = userEvent.setup()
const onShowAvatarChange = vi.fn()
const onReloadAvatars = vi.fn()
const onJumpToLatest = vi.fn()
renderWithTooltip(
<ChatStatusBar
count={5}
showAvatar={false}
isAtLatest={false}
isReloadingAvatars={false}
onShowAvatarChange={onShowAvatarChange}
onReloadAvatars={onReloadAvatars}
onJumpToLatest={onJumpToLatest}
/>
)
await user.click(screen.getByRole('switch', { name: '显示头像' }))
await user.click(screen.getByRole('button', { name: '重新加载头像' }))
await user.click(screen.getByRole('button', { name: '跳转到最新消息' }))
expect(onShowAvatarChange).toHaveBeenCalledWith(true)
expect(onReloadAvatars).toHaveBeenCalledOnce()
expect(onJumpToLatest).toHaveBeenCalledOnce()
})
it('keeps loading and latest status actions disabled', () => {
renderWithTooltip(
<ChatStatusBar
count={5}
showAvatar
isAtLatest
isReloadingAvatars
onShowAvatarChange={vi.fn()}
onReloadAvatars={vi.fn()}
onJumpToLatest={vi.fn()}
/>
)
expect(screen.getByRole('button', { name: '加载中' })).toBeDisabled()
expect(screen.getByRole('button', { name: '已是最新消息' })).toBeDisabled()
})
})
@@ -2,19 +2,29 @@ import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import { ConversationSidebarHeader } from '../../src/renderer/src/components/conversation/ConversationSidebarHeader'
import { TooltipProvider } from '../../src/renderer/src/components/ui'
describe('ConversationSidebarHeader', () => {
it('refreshes the conversation list from the archive header', async () => {
const onRefresh = vi.fn()
render(
function renderHeader(
overrides: Partial<React.ComponentProps<typeof ConversationSidebarHeader>> = {}
): void {
render(
<TooltipProvider>
<ConversationSidebarHeader
totalCount={1306}
searchValue=""
onSearchChange={vi.fn()}
refreshing={false}
onRefresh={onRefresh}
onRefresh={vi.fn()}
{...overrides}
/>
)
</TooltipProvider>
)
}
describe('ConversationSidebarHeader', () => {
it('refreshes the conversation list from the archive header', async () => {
const onRefresh = vi.fn()
renderHeader({ onRefresh })
expect(screen.getByText('1306 个会话')).toBeInTheDocument()
await userEvent.click(screen.getByRole('button', { name: '刷新会话列表' }))
@@ -22,17 +32,20 @@ describe('ConversationSidebarHeader', () => {
})
it('disables the refresh button while contacts are loading', () => {
render(
<ConversationSidebarHeader
totalCount={12}
searchValue="测试"
onSearchChange={vi.fn()}
refreshing
onRefresh={vi.fn()}
/>
)
renderHeader({ totalCount: 12, searchValue: '测试', refreshing: true })
expect(screen.getByRole('button', { name: '正在刷新会话列表' })).toBeDisabled()
expect(screen.getByDisplayValue('测试')).toBeInTheDocument()
})
it('keeps the controlled conversation search callback and search semantics', async () => {
const onSearchChange = vi.fn()
renderHeader({ onSearchChange })
const search = screen.getByRole('searchbox', { name: '搜索会话' })
await userEvent.type(search, '产品')
expect(onSearchChange).toHaveBeenCalled()
expect(onSearchChange.mock.calls[0]).toEqual(['产'])
})
})
+29 -26
View File
@@ -2,6 +2,7 @@ import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import { ConversationSidebar } from '../../src/renderer/src/components/conversation/ConversationSidebar'
import { TooltipProvider } from '../../src/renderer/src/components/ui'
vi.mock('@tanstack/react-virtual', () => ({
useVirtualizer: ({ count }: { count: number }) => ({
@@ -19,32 +20,34 @@ vi.mock('@tanstack/react-virtual', () => ({
describe('ConversationSidebar', () => {
it('keeps official accounts in their own collapsible section', async () => {
render(
<ConversationSidebar
contacts={[
{
m_nsUsrName: 'gh_fixture',
m_nsNickName: '测试公众号',
md5: 'official-md5',
type: 'user',
isOfficialAccount: true
},
{
m_nsUsrName: 'wxid_fixture',
m_nsNickName: '测试联系人',
md5: 'contact-md5',
type: 'user'
}
]}
selectedContact={null}
onSelectContact={vi.fn()}
onSearch={vi.fn()}
onContentFilter={vi.fn()}
width={320}
selfInfo={null}
dbReady
onOpenSettings={vi.fn()}
onRefresh={vi.fn(async () => undefined)}
/>
<TooltipProvider>
<ConversationSidebar
contacts={[
{
m_nsUsrName: 'gh_fixture',
m_nsNickName: '测试公众号',
md5: 'official-md5',
type: 'user',
isOfficialAccount: true
},
{
m_nsUsrName: 'wxid_fixture',
m_nsNickName: '测试联系人',
md5: 'contact-md5',
type: 'user'
}
]}
selectedContact={null}
onSelectContact={vi.fn()}
onSearch={vi.fn()}
onContentFilter={vi.fn()}
width={320}
selfInfo={null}
dbReady
onOpenSettings={vi.fn()}
onRefresh={vi.fn(async () => undefined)}
/>
</TooltipProvider>
)
expect(screen.getByRole('button', { name: '公众号 (1)' })).toBeInTheDocument()
+45 -4
View File
@@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event'
import type { ComponentProps } from 'react'
import { describe, expect, it, vi } from 'vitest'
import { DatabaseConnectionPage } from '../../src/renderer/src/components/DatabaseConnectionPage'
import { TooltipProvider } from '../../src/renderer/src/components/ui'
function renderPage(
overrides: Partial<ComponentProps<typeof DatabaseConnectionPage>> = {}
@@ -65,7 +66,14 @@ function renderPage(
onClearKey: vi.fn(),
...overrides
}
return { props, ...render(<DatabaseConnectionPage {...props} />) }
return {
props,
...render(
<TooltipProvider>
<DatabaseConnectionPage {...props} />
</TooltipProvider>
)
}
}
describe('DatabaseConnectionPage', () => {
@@ -119,7 +127,11 @@ describe('DatabaseConnectionPage', () => {
it('keeps connect disabled until a valid 64-character key is supplied', () => {
const { rerender, props } = renderPage()
expect(screen.getByRole('button', { name: '连接数据库' })).toBeDisabled()
rerender(<DatabaseConnectionPage {...props} dbKey={'a'.repeat(64)} />)
rerender(
<TooltipProvider>
<DatabaseConnectionPage {...props} dbKey={'a'.repeat(64)} />
</TooltipProvider>
)
expect(screen.getByRole('button', { name: '连接数据库' })).toBeEnabled()
})
@@ -175,11 +187,41 @@ describe('DatabaseConnectionPage', () => {
expect(onCopyDiagnostics).toHaveBeenCalledOnce()
expect(onGuideNext).toHaveBeenCalledOnce()
rerender(<DatabaseConnectionPage {...props} mode="automatic" guideStep={2} />)
rerender(
<TooltipProvider>
<DatabaseConnectionPage {...props} mode="automatic" guideStep={2} />
</TooltipProvider>
)
expect(screen.getByRole('button', { name: '我已准备好' })).toBeEnabled()
expect(screen.getByRole('button', { name: '返回上一步' })).toBeEnabled()
expect(screen.getByRole('button', { name: '取消并重新检查' })).toBeEnabled()
})
it('keeps mode, account and key visibility callbacks unchanged', async () => {
const onModeChange = vi.fn()
const onSelectAccount = vi.fn()
const onToggleDbKey = vi.fn()
const { props, rerender } = renderPage({
mode: 'automatic',
onModeChange,
onSelectAccount,
onToggleDbKey
})
await userEvent.click(screen.getByRole('tab', { name: /高级用户/ }))
expect(onModeChange).toHaveBeenCalledWith('manual')
await userEvent.click(screen.getByRole('button', { name: /脱敏账号 A/ }))
expect(onSelectAccount).toHaveBeenCalledWith(props.accounts[0])
rerender(
<TooltipProvider>
<DatabaseConnectionPage {...props} mode="manual" />
</TooltipProvider>
)
await userEvent.click(screen.getByRole('button', { name: '显示密钥' }))
expect(onToggleDbKey).toHaveBeenCalledOnce()
})
it('provides the official Visual C++ runtime download on Windows', () => {
renderPage({
status: '当前 Windows 缺少 Microsoft Visual C++ 运行库',
@@ -191,5 +233,4 @@ describe('DatabaseConnectionPage', () => {
'https://aka.ms/vc14/vc_redist.x64.exe'
)
})
})
@@ -0,0 +1,130 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import { DatabaseKeyAutoDetect } from '../../src/renderer/src/features/settings/database-key/DatabaseKeyAutoDetect'
import { DatabaseKeyDiagnostics } from '../../src/renderer/src/features/settings/database-key/DatabaseKeyDiagnostics'
import { DatabaseKeyDangerZone } from '../../src/renderer/src/features/settings/database-key/DatabaseKeyDangerZone'
import { DatabaseKeyEditor } from '../../src/renderer/src/features/settings/database-key/DatabaseKeyEditor'
import { DatabaseKeyStatus } from '../../src/renderer/src/features/settings/database-key/DatabaseKeyStatus'
import type { DatabaseKeyState } from '../../src/renderer/src/features/settings/database-key/types'
import { TooltipProvider } from '../../src/renderer/src/components/ui'
const state: DatabaseKeyState = {
status: 'saved',
saved: true,
encryptionAvailable: true,
autoPhase: 0,
environment: {
platform: 'win32',
osVersion: 'Windows 11',
appVersion: '2.2.2',
wechatVersion: '4.1.0',
dataStructureVersion: 'WCDB',
dataDirectoryDetected: true,
diagnosticSummary: 'fixture',
autoDetectSupported: true,
wechatRunning: true,
accountIdentified: true,
dbConnected: true,
encryptionAvailable: true
}
}
describe('database key controls', () => {
it('keeps auto-detect and refresh callbacks separate', async () => {
const user = userEvent.setup()
const onDetect = vi.fn()
const onRefresh = vi.fn()
render(
<DatabaseKeyAutoDetect
state={{ ...state, status: 'auto-detect-error', error: '没有找到候选密钥' }}
disabled={false}
onDetect={onDetect}
onRefresh={onRefresh}
/>
)
await user.click(screen.getByRole('button', { name: '自动获取密钥' }))
await user.click(screen.getByRole('button', { name: '刷新前置状态' }))
expect(onDetect).toHaveBeenCalledOnce()
expect(onRefresh).toHaveBeenCalledOnce()
})
it('keeps status validation and diagnostics copy actions available', async () => {
const user = userEvent.setup()
const onValidate = vi.fn()
const onCopy = vi.fn()
const { rerender } = render(
<DatabaseKeyStatus
state={state}
dbReady
selfInfo={{ wxid: 'wxid_fixture', nickname: '测试账号', accountRoot: 'fixture-account' }}
disabled={false}
onValidate={onValidate}
/>
)
await user.click(screen.getByRole('button', { name: '重新验证' }))
expect(onValidate).toHaveBeenCalledOnce()
rerender(
<DatabaseKeyDiagnostics
state={state}
input={'a'.repeat(64)}
wxid="wxid_fixture"
accountRoot="fixture-account"
onCopy={onCopy}
/>
)
await user.click(screen.getByText('查看密钥诊断'))
await user.click(screen.getByRole('button', { name: '复制诊断信息' }))
expect(onCopy).toHaveBeenCalledOnce()
})
it('uses shared button variants for editor actions without legacy visual classes', async () => {
const user = userEvent.setup()
const onValidate = vi.fn()
const onSave = vi.fn()
render(
<TooltipProvider>
<DatabaseKeyEditor
value={'a'.repeat(64)}
disabled={false}
canSave
onChange={vi.fn()}
onPaste={vi.fn()}
onValidate={onValidate}
onSave={onSave}
/>
</TooltipProvider>
)
const validate = screen.getByRole('button', { name: '验证密钥' })
const save = screen.getByRole('button', { name: '保存密钥' })
expect(validate).not.toHaveClass('database-key-secondary')
expect(save).not.toHaveClass('database-key-primary')
await user.click(validate)
await user.click(save)
expect(onValidate).toHaveBeenCalledOnce()
expect(onSave).toHaveBeenCalledOnce()
})
it('restores focus to the actual destructive dialog trigger', async () => {
const user = userEvent.setup()
render(
<DatabaseKeyDangerZone
disabled={false}
onClear={vi.fn()}
onReplace={vi.fn()}
onReturnToLogin={vi.fn()}
/>
)
const clearKey = screen.getByRole('button', { name: '清除密钥' })
await user.click(clearKey)
expect(screen.getByRole('alertdialog', { name: '确认清除数据库密钥?' })).toBeVisible()
await user.keyboard('{Escape}')
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
expect(clearKey).toHaveFocus()
})
})
+102 -2
View File
@@ -1,9 +1,12 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { AutoDetectImageKeySection } from '../../src/renderer/src/features/settings/image-decryption/AutoDetectImageKeySection'
import { ImageDecryptStatus } from '../../src/renderer/src/features/settings/image-decryption/ImageDecryptStatus'
import { ImageTestSection } from '../../src/renderer/src/features/settings/image-decryption/ImageTestSection'
import { initialImageDecryptionState } from '../../src/renderer/src/features/settings/image-decryption/imageDecryptionReducer'
import type { ImageBatchTestState } from '../../src/renderer/src/features/settings/image-decryption/types'
import type { ImageDecryptionStatus } from '../../src/shared/image-decryption'
const emptyBatchTest: ImageBatchTestState = {
running: false,
@@ -12,11 +15,103 @@ const emptyBatchTest: ImageBatchTestState = {
items: []
}
const imageStatus: ImageDecryptionStatus = {
configured: true,
saved: true,
encryptionAvailable: true,
source: 'secure-storage',
resourceRoot: '/fixture/images',
platform: 'darwin',
autoDetectSupported: true,
wechatRunning: true,
accountIdentified: true,
cacheState: 'normal',
decoder: {
installed: true,
available: true,
source: 'bundled',
selected: false
},
resources: {
imageIndex: { state: 'available', detail: '正常' },
imageDirectory: { state: 'available', detail: '正常' },
thumbnail: { state: 'available', detail: '正常' },
original: { state: 'available', detail: '正常' },
sticker: { state: 'available', detail: '正常' },
video: { state: 'available', detail: '正常' }
}
}
afterEach(() => {
vi.restoreAllMocks()
})
describe('ImageTestSection', () => {
it('uses shared buttons for status validation and automatic key actions', async () => {
const user = userEvent.setup()
const onValidate = vi.fn()
const onDetect = vi.fn()
const onSave = vi.fn()
const configuredState = {
...initialImageDecryptionState,
config: {
success: true,
configured: true,
saved: true,
encryptionAvailable: true,
source: 'secure-storage' as const,
resourceRoot: '/fixture/images'
},
status: imageStatus
}
const { rerender } = render(
<ImageDecryptStatus
state={configuredState}
selfInfo={null}
disabled={false}
onValidate={onValidate}
/>
)
const validate = screen.getByRole('button', { name: '重新验证' })
expect(validate).not.toHaveClass('database-key-secondary')
await user.click(validate)
expect(onValidate).toHaveBeenCalledOnce()
rerender(
<AutoDetectImageKeySection
state={{ ...configuredState, autoPhase: 'success', autoAccount: '测试账号' }}
disabled={false}
canSave
onDetect={onDetect}
onSave={onSave}
/>
)
const detect = screen.getByRole('button', { name: '开始自动获取' })
const save = screen.getByRole('button', { name: '保存图片密钥' })
expect(detect).not.toHaveClass('database-key-secondary')
expect(save).not.toHaveClass('database-key-primary')
await user.click(detect)
await user.click(save)
expect(onDetect).toHaveBeenCalledOnce()
expect(onSave).toHaveBeenCalledOnce()
})
it('keeps automatic detection disabled while scanning', () => {
render(
<AutoDetectImageKeySection
state={{ ...initialImageDecryptionState, status: imageStatus, autoPhase: 'scanning' }}
disabled={false}
canSave={false}
onDetect={vi.fn()}
onSave={vi.fn()}
/>
)
expect(screen.getByRole('button', { name: '扫描中…' })).toBeDisabled()
})
it('keeps the file step successful when decryption fails and copies diagnostics', async () => {
const onCopyLog = vi.fn()
render(
@@ -88,6 +183,7 @@ describe('ImageTestSection', () => {
it('filters groups and contacts before starting a batch test', async () => {
const onBatchTest = vi.fn()
const onSelect = vi.fn()
vi.spyOn(window, 'confirm').mockReturnValue(true)
render(
<ImageTestSection
@@ -111,7 +207,7 @@ describe('ImageTestSection', () => {
batchTest={emptyBatchTest}
disabled={false}
canSave={false}
onSelect={vi.fn()}
onSelect={onSelect}
onTest={vi.fn()}
onBatchTest={onBatchTest}
onStopBatchTest={vi.fn()}
@@ -120,9 +216,13 @@ describe('ImageTestSection', () => {
/>
)
await userEvent.click(screen.getByRole('button', { name: '群聊 1' }))
await userEvent.click(screen.getByRole('radio', { name: '群聊 1' }))
expect(screen.getByRole('radio', { name: '群聊 1' })).toBeChecked()
await userEvent.click(screen.getByRole('combobox', { name: '选择会话' }))
expect(screen.getByRole('option', { name: '测试群聊' })).toBeInTheDocument()
expect(screen.queryByRole('option', { name: '测试联系人' })).not.toBeInTheDocument()
await userEvent.click(screen.getByRole('option', { name: '测试群聊' }))
expect(onSelect).toHaveBeenCalledWith('group-md5')
await userEvent.click(screen.getByRole('button', { name: '测试筛选结果(1' }))
expect(window.confirm).toHaveBeenCalledWith(expect.stringContaining('测试 1 个会话'))
@@ -187,6 +187,21 @@ describe('PersonalWechatSendDialog', () => {
expect(screen.queryByRole('radio', { name: '文字' })).not.toBeInTheDocument()
})
it('supports segmented keyboard navigation for the message type', async () => {
const user = userEvent.setup()
render(<PersonalWechatSendDialog contact={contact} isGroupChat onClose={vi.fn()} />)
await screen.findByText('技术交流群')
const imageMode = screen.getByRole('radio', { name: '图片' })
const voiceMode = screen.getByRole('radio', { name: '语音' })
imageMode.focus()
await user.keyboard('{ArrowRight}')
expect(voiceMode).toHaveFocus()
await user.keyboard(' ')
expect(voiceMode).toBeChecked()
expect(screen.getByRole('textbox', { name: '要生成的文字' })).toBeVisible()
})
it('switches to image mode and sends only the selected image', async () => {
render(<PersonalWechatSendDialog contact={contact} isGroupChat onClose={vi.fn()} />)
await screen.findByText('技术交流群')
@@ -262,6 +277,8 @@ describe('PersonalWechatSendDialog', () => {
await user.click(screen.getByRole('button', { name: '选择图片' }))
await user.click(screen.getByRole('button', { name: '测试发送图片到群聊' }))
expect(screen.getByRole('button', { name: '正在发送…' })).toBeDisabled()
expect(screen.getByRole('radio', { name: '图片' })).toBeDisabled()
expect(screen.getByRole('radio', { name: '语音' })).toBeDisabled()
await user.keyboard('{Escape}')
expect(screen.getByRole('dialog', { name: '个人微信测试发送' })).toBeVisible()
+78 -11
View File
@@ -1,4 +1,4 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ReportGroupMemberSelector } from '../../src/renderer/src/components/reports/ReportGroupMemberSelector'
@@ -9,6 +9,10 @@ import { ReportInfoPanel } from '../../src/renderer/src/components/reports/Repor
import { ReportToolbar } from '../../src/renderer/src/components/reports/ReportToolbar'
import { ReportHistorySidebar } from '../../src/renderer/src/components/reports/ReportHistorySidebar'
import { ModelSummary } from '../../src/renderer/src/components/reports/ModelSummary'
import { MessageTypeSelector } from '../../src/renderer/src/components/reports/MessageTypeSelector'
import { ReportDensitySelector } from '../../src/renderer/src/components/reports/ReportDensitySelector'
import { ReportRangeSelector } from '../../src/renderer/src/components/reports/ReportRangeSelector'
import { SUMMARY_TYPE_OPTIONS } from '../../src/renderer/src/utils/group-report'
import type { Contact } from '../../src/shared/types'
import type { GeneratedReportRecord } from '../../src/shared/report-history'
@@ -106,6 +110,44 @@ describe('daily report controls', () => {
})
})
it('uses unified option and checkbox controls while preserving report selection rules', async () => {
const user = userEvent.setup()
const onRangeChange = vi.fn()
const onTypesChange = vi.fn()
render(
<>
<ReportRangeSelector
value="today"
messageCount={4}
rangeState={{ status: 'success', error: '' }}
disabled={false}
onChange={onRangeChange}
/>
<MessageTypeSelector
value={['text']}
counts={Object.fromEntries(SUMMARY_TYPE_OPTIONS.map((option) => [option.value, 1]))}
disabled={false}
onChange={onTypesChange}
/>
<ReportDensitySelector />
</>
)
expect(screen.getByRole('radio', { name: '今天' })).toBeChecked()
await user.click(screen.getByRole('radio', { name: '近 7 天' }))
expect(onRangeChange).toHaveBeenCalledWith('7days')
expect(screen.getAllByRole('checkbox')).toHaveLength(SUMMARY_TYPE_OPTIONS.length)
const textCheckbox = screen.getAllByRole('checkbox')[0]
expect(textCheckbox).toBeChecked()
expect(textCheckbox).toBeDisabled()
await user.click(screen.getAllByRole('checkbox')[1])
expect(onTypesChange).toHaveBeenCalledWith(['text', 'image'])
expect(screen.getByRole('radio', { name: /标准/ })).toBeChecked()
screen.getAllByRole('radio', { name: /简洁|标准|深度/ }).forEach((item) => {
expect(item).toBeDisabled()
})
})
it('shows a separate voice progress bar only when voice is selected', () => {
const progress = { processed: 2, total: 3, succeeded: 2, failed: 0 }
const { rerender } = render(
@@ -203,6 +245,7 @@ describe('daily report controls', () => {
})
it('allows switching models and retrying only the model step', async () => {
const user = userEvent.setup()
const onRetry = vi.fn()
window.api.listAIProviders = vi.fn(async () => ({
success: true,
@@ -246,7 +289,10 @@ describe('daily report controls', () => {
)
const retryButton = await screen.findByRole('button', { name: '使用所选模型重新生成' })
const retryModel = screen.getByRole('combobox', { name: '切换模型' })
await user.click(retryModel)
expect(screen.getByRole('option', { name: '备用服务 · 备用模型' })).toBeVisible()
await user.click(screen.getByRole('option', { name: '备用服务 · 备用模型' }))
fireEvent.click(retryButton)
expect(onRetry).toHaveBeenCalledWith(
expect.objectContaining({ providerId: 'provider-2', model: 'model-2' })
@@ -254,7 +300,8 @@ describe('daily report controls', () => {
expect(screen.getByText(/从第三步继续/)).toBeVisible()
})
it('selects separate text-summary and image-understanding models with the 10-minute cache rule', () => {
it('selects separate text-summary and image-understanding models with the 10-minute cache rule', async () => {
const user = userEvent.setup()
const onTextModelChange = vi.fn()
const onVisionModelChange = vi.fn()
const textModels = [
@@ -283,6 +330,14 @@ describe('daily report controls', () => {
modelName: 'GPT-5.6 Sol',
configured: true as const,
status: 'connected' as const
},
{
providerId: 'vision-provider',
providerName: 'Vision',
model: 'vision-model',
modelName: 'Vision Model',
configured: true as const,
status: 'connected' as const
}
]
render(
@@ -299,13 +354,14 @@ describe('daily report controls', () => {
const textSelect = screen.getByRole('combobox', { name: '文字总结模型' })
const visionSelect = screen.getByRole('combobox', { name: '图片理解模型' })
expect(textSelect).toHaveValue('deepseek::deepseek-chat')
expect(visionSelect).toHaveValue('sol-provider::gpt-5.6-sol')
expect(screen.getAllByRole('option', { name: 'OpenAI · GPT-5.6 Sol' })).toHaveLength(2)
fireEvent.change(textSelect, { target: { value: 'openai::gpt-5.6-sol' } })
fireEvent.change(visionSelect, { target: { value: 'sol-provider::gpt-5.6-sol' } })
expect(textSelect).toHaveTextContent('DeepSeek · DeepSeek Chat')
expect(visionSelect).toHaveTextContent('OpenAI · GPT-5.6 Sol')
await user.click(textSelect)
await user.click(screen.getByRole('option', { name: 'OpenAI · GPT-5.6 Sol' }))
await user.click(visionSelect)
await user.click(screen.getByRole('option', { name: 'Vision · Vision Model' }))
expect(onTextModelChange).toHaveBeenCalledWith(textModels[1])
expect(onVisionModelChange).toHaveBeenCalledWith(visionModels[0])
expect(onVisionModelChange).toHaveBeenCalledWith(visionModels[1])
expect(screen.getByText(/图片识别缓存 10 分钟/)).toBeVisible()
})
@@ -676,12 +732,17 @@ describe('daily report controls', () => {
})
it('loads and displays group nickname, WeChat nickname, and remark separately', async () => {
const user = userEvent.setup()
render(<ReportGroupMemberSelector sourceContact={groupContact} />)
await waitFor(() => expect(screen.getAllByText('群内昵称一')).toHaveLength(2))
expect(screen.getByText('微信昵称一')).toBeVisible()
expect(screen.getByText('通讯录备注一')).toBeVisible()
expect(screen.getByText('wxid-one')).toBeVisible()
await user.click(screen.getByRole('combobox', { name: '选择群成员' }))
await user.click(screen.getByRole('option', { name: '群内昵称二' }))
expect(screen.getByText('微信昵称二')).toBeVisible()
expect(screen.getByText('wxid-two')).toBeVisible()
})
it('cancels report deletion and restores focus to the delete trigger', async () => {
@@ -774,7 +835,8 @@ describe('daily report controls', () => {
)
})
it('offers the classic default plus three mobile and two desktop report templates', () => {
it('offers the classic default plus three mobile and two desktop report templates', async () => {
const user = userEvent.setup()
const onChange = vi.fn()
render(<ReportTemplateSelector value="v1" onChange={onChange} />)
@@ -792,8 +854,13 @@ describe('daily report controls', () => {
const previewButtons = screen.getAllByRole('button', { name: '查看版式' })
expect(previewButtons).toHaveLength(6)
fireEvent.click(previewButtons[2])
fireEvent.click(screen.getByRole('button', { name: '选择此模板' }))
await user.click(previewButtons[2])
let dialog = screen.getByRole('dialog', { name: 'AI Magazine' })
await user.click(within(dialog).getAllByRole('button', { name: '关闭' })[1])
expect(document.activeElement).toBe(previewButtons[2])
await user.click(previewButtons[2])
dialog = screen.getByRole('dialog', { name: 'AI Magazine' })
await user.click(within(dialog).getByRole('button', { name: '选择此模板' }))
expect(onChange).toHaveBeenCalledWith('mobile-magazine')
})
@@ -0,0 +1,74 @@
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AboutPage } from '../../src/renderer/src/features/settings/pages/AboutPage'
import { AdvancedPage } from '../../src/renderer/src/features/settings/pages/AdvancedPage'
import { RecallProtectionPage } from '../../src/renderer/src/features/settings/pages/RecallProtectionPage'
const api = {
checkAppUpdate: vi.fn(),
downloadAppUpdate: vi.fn(),
getAppUpdateState: vi.fn(),
getSettings: vi.fn(),
installAppUpdate: vi.fn(),
onAppUpdateState: vi.fn(),
revealAppLog: vi.fn(),
setSettings: vi.fn()
}
describe('basic settings pages', () => {
beforeEach(() => {
vi.clearAllMocks()
Object.defineProperty(window, 'api', { configurable: true, value: api })
api.getAppUpdateState.mockResolvedValue({ status: 'idle', currentVersion: '2.2.2' })
api.onAppUpdateState.mockReturnValue(vi.fn())
api.checkAppUpdate.mockResolvedValue(undefined)
api.revealAppLog.mockResolvedValue(undefined)
api.getSettings.mockResolvedValue({ settings: { debugEnabled: false } })
api.setSettings.mockResolvedValue({ settings: { debugEnabled: true } })
})
it('keeps update and log actions connected to the desktop APIs', async () => {
const user = userEvent.setup()
render(<AboutPage onNotice={vi.fn()} />)
await screen.findByText('v2.2.2')
await user.click(screen.getByRole('button', { name: '检查更新' }))
await user.click(screen.getByRole('button', { name: '打开诊断日志目录' }))
await waitFor(() => expect(api.checkAppUpdate).toHaveBeenCalledOnce())
expect(api.revealAppLog).toHaveBeenCalledOnce()
})
it('uses the shared switch without changing the debug settings contract', async () => {
const user = userEvent.setup()
const onNotice = vi.fn()
render(<AdvancedPage onNotice={onNotice} />)
const debugSwitch = await screen.findByRole('switch', { name: '显示诊断日志' })
expect(debugSwitch).not.toBeChecked()
await user.click(debugSwitch)
await waitFor(() => expect(api.setSettings).toHaveBeenCalledWith({ debugEnabled: true }))
expect(debugSwitch).toBeChecked()
expect(onNotice).toHaveBeenCalledWith('已开启调试日志')
})
it('uses the shared switch without changing the recall protection settings contract', async () => {
api.getSettings.mockResolvedValue({ settings: { recallProtectionEnabled: false } })
api.setSettings.mockResolvedValue({ settings: { recallProtectionEnabled: true } })
const user = userEvent.setup()
const onNotice = vi.fn()
render(<RecallProtectionPage onNotice={onNotice} />)
const recallSwitch = await screen.findByRole('switch', { name: '开启防撤回' })
expect(recallSwitch).not.toBeChecked()
await user.click(recallSwitch)
await waitFor(() =>
expect(api.setSettings).toHaveBeenCalledWith({ recallProtectionEnabled: true })
)
expect(recallSwitch).toBeChecked()
expect(onNotice).toHaveBeenCalledWith('已开启防撤回')
})
})
+46 -2
View File
@@ -16,6 +16,8 @@ const listVoices = vi.fn()
const openApiKeys = vi.fn()
const getRuntimeStatus = vi.fn()
const onRuntimeProgress = vi.fn(() => vi.fn())
const openRuntimeDirectory = vi.fn()
const removeRuntime = vi.fn()
const response = {
success: true,
@@ -82,9 +84,22 @@ describe('TextToSpeechPage', () => {
platform: 'darwin',
architecture: 'arm64',
supported: true,
removable: true
removable: true,
directory: '/tmp/personal-wechat-runtime'
})
onRuntimeProgress.mockReset().mockReturnValue(vi.fn())
openRuntimeDirectory.mockReset().mockResolvedValue({ success: true })
removeRuntime.mockReset().mockResolvedValue({
version: 'v0.0.18',
state: 'missing',
downloadedBytes: 0,
totalBytes: 100,
progress: 0,
platform: 'darwin',
architecture: 'arm64',
supported: true,
removable: false
})
Object.defineProperty(window, 'api', {
configurable: true,
value: {
@@ -93,7 +108,9 @@ describe('TextToSpeechPage', () => {
listTextToSpeechVoices: listVoices,
openFishAudioApiKeys: openApiKeys,
getPersonalWechatRuntimeStatus: getRuntimeStatus,
onPersonalWechatRuntimeProgress: onRuntimeProgress
onPersonalWechatRuntimeProgress: onRuntimeProgress,
openPersonalWechatRuntimeDirectory: openRuntimeDirectory,
removePersonalWechatRuntime: removeRuntime
}
})
})
@@ -136,6 +153,33 @@ describe('TextToSpeechPage', () => {
await waitFor(() => expect(openApiKeys).toHaveBeenCalledTimes(1))
})
it('keeps model selection and API key actions on their existing settings callbacks', async () => {
const user = userEvent.setup()
render(<TextToSpeechPage onNotice={vi.fn()} />)
const apiKeyInput = await screen.findByPlaceholderText('已安全保存;输入新 Key 可替换')
expect(apiKeyInput).toHaveAttribute('type', 'password')
await user.click(screen.getByRole('button', { name: '显示' }))
expect(apiKeyInput).toHaveAttribute('type', 'text')
await user.click(screen.getByRole('combobox', { name: '合成模型' }))
await user.click(screen.getByRole('option', { name: 's2.1-pro', exact: true }))
await waitFor(() => expect(saveSettings).toHaveBeenCalledWith({ model: 's2.1-pro' }))
await user.click(screen.getByRole('button', { name: '清除应用内 Key' }))
await waitFor(() => expect(saveSettings).toHaveBeenCalledWith({ clearApiKey: true }))
})
it('keeps runtime directory and refresh actions wired to the existing API', async () => {
const user = userEvent.setup()
render(<TextToSpeechPage onNotice={vi.fn()} />)
await user.click(await screen.findByRole('button', { name: '打开目录' }))
expect(openRuntimeDirectory).toHaveBeenCalledOnce()
await user.click(screen.getByRole('button', { name: '重新检测' }))
expect(getRuntimeStatus).toHaveBeenCalledTimes(2)
})
it('opens supported versions from both triggers and restores focus after closing', async () => {
const user = userEvent.setup()
render(<TextToSpeechPage onNotice={vi.fn()} />)
@@ -146,6 +146,14 @@ describe('voice recognition settings', () => {
conversationIds: ['group-b'],
range: 'recent_30_days'
})
await userEvent.click(screen.getByRole('combobox', { name: '语音转写时间范围' }))
await userEvent.click(screen.getByRole('option', { name: '所选会话全部历史' }))
await waitFor(() =>
expect(window.api.getVoiceBatchConversationSummaries).toHaveBeenCalledWith({
conversationIds: ['group-b'],
range: 'selected_history'
})
)
await userEvent.click(screen.getByRole('checkbox', { name: /群聊 B/ }))
await userEvent.click(screen.getByRole('tab', { name: /联系人 1/ }))
await userEvent.click(await screen.findByRole('checkbox', { name: /联系人 A/ }))
@@ -157,7 +165,7 @@ describe('voice recognition settings', () => {
await waitFor(() =>
expect(window.api.startVoiceBatch).toHaveBeenCalledWith({
conversationIds: ['group-b', 'contact-a'],
range: 'recent_30_days'
range: 'selected_history'
})
)
})