mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-21 21:47:00 +08:00
refactor(ui): 统一迁移原生按钮、输入框及交互控件
This commit is contained in:
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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('已清理全部可恢复缓存和检索记录')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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(['产'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
|
||||
@@ -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('已开启防撤回')
|
||||
})
|
||||
})
|
||||
@@ -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'
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user