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'
})
)
})
+343 -11
View File
@@ -16,8 +16,12 @@ test('APP-01 first launch renders a usable connection screen without uncaught er
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize({ width: 820, height: 600 })
await expect(fixture.page.getByRole('heading', { name: 'TraceMemo(迹忆)' })).toBeVisible()
await expect(fixture.page.getByRole('main')).not.toBeEmpty()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
@@ -132,7 +136,12 @@ test('CHAT-01 archive More menu is keyboard-safe and keeps the page usable', asy
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize({ width: 1000, height: 650 })
await fixture.setWindowContentSize({ width: 820, height: 600 })
const conversationSearch = fixture.page.getByRole('searchbox', { name: '搜索会话' })
await conversationSearch.fill('产品')
await expect(fixture.page.getByText('产品测试群', { exact: true })).toBeVisible()
await conversationSearch.fill('')
await fixture.page.getByRole('button', { name: '刷新会话列表' }).click()
await fixture.page.getByText('产品测试群', { exact: true }).click()
const moreButton = fixture.page.getByRole('button', { name: '更多' })
await moreButton.click()
@@ -144,6 +153,24 @@ test('CHAT-01 archive More menu is keyboard-safe and keeps the page usable', asy
await moreButton.click()
await fixture.page.getByRole('menuitem', { name: '刷新数据' }).click()
await expect(fixture.page.getByRole('heading', { name: '产品测试群' })).toBeVisible()
await fixture.page.getByRole('button', { name: '搜索当前聊天' }).click()
const searchInput = fixture.page.getByRole('textbox', { name: '搜索当前聊天内容' })
await expect(searchInput).toBeFocused()
await searchInput.fill('测试')
await expect(searchInput).toHaveValue('测试')
expect(
await fixture.page
.locator('.chat-archive-header')
.evaluate((element) => element.scrollWidth <= element.clientWidth)
).toBe(true)
await fixture.page.getByRole('button', { name: '关闭搜索' }).click()
await expect(searchInput).toHaveCount(0)
const avatarSwitch = fixture.page.getByRole('switch', { name: '显示头像' })
await expect(avatarSwitch).toBeChecked()
await avatarSwitch.click()
await expect(avatarSwitch).not.toBeChecked()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
@@ -159,7 +186,7 @@ test('CHAT-02 personal WeChat send dialog is keyboard-safe and fits the viewport
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize({ width: 1000, height: 650 })
await fixture.setWindowContentSize({ width: 820, height: 600 })
await fixture.page.getByText('产品测试群', { exact: true }).click()
const trigger = fixture.page.getByRole('button', { name: '发送消息' })
await trigger.click()
@@ -177,9 +204,20 @@ test('CHAT-02 personal WeChat send dialog is keyboard-safe and fits the viewport
const bounds = await dialog.boundingBox()
expect(bounds).not.toBeNull()
expect(bounds!.y).toBeGreaterThanOrEqual(0)
expect(bounds!.y + bounds!.height).toBeLessThanOrEqual(650)
expect(bounds!.y + bounds!.height).toBeLessThanOrEqual(600)
expect(pageErrors).toEqual([])
const imageMode = dialog.getByRole('radio', { name: '图片' })
const voiceMode = dialog.getByRole('radio', { name: '语音' })
await imageMode.focus()
await fixture.page.keyboard.press('ArrowRight')
await expect(voiceMode).toBeFocused()
await fixture.page.keyboard.press('Space')
await expect(voiceMode).toBeChecked()
expect(await dialog.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(
true
)
await fixture.page.keyboard.press('Escape')
await expect(dialog).toHaveCount(0)
await expect(trigger).toBeFocused()
@@ -218,7 +256,7 @@ test('SETTINGS-01 supported WeChat versions dialog is keyboard-safe and fits the
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize({ width: 1000, height: 650 })
await fixture.setWindowContentSize({ width: 820, height: 600 })
await fixture.page
.getByRole('navigation', { name: '一级导航' })
.getByRole('button', { name: '设置' })
@@ -240,6 +278,278 @@ test('SETTINGS-01 supported WeChat versions dialog is keyboard-safe and fits the
await fixture.page.keyboard.press('Escape')
await expect(dialog).toHaveCount(0)
await expect(trigger).toBeFocused()
const modelSelect = fixture.page.getByRole('combobox', { name: '合成模型' })
await modelSelect.scrollIntoViewIfNeeded()
await modelSelect.click()
await expect(fixture.page.getByRole('option', { name: 's2.1-pro', exact: true })).toBeVisible()
await fixture.page.keyboard.press('Escape')
await expect(modelSelect).toBeFocused()
await expect(fixture.page.getByRole('searchbox', { name: '按音色名称搜索' })).toBeDisabled()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('SETTINGS-02 basic settings controls fit a narrow viewport and keep their semantics', async () => {
const fixture = await launchTestApp()
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize({ width: 820, height: 600 })
await fixture.page
.getByRole('navigation', { name: '一级导航' })
.getByRole('button', { name: '设置' })
.click()
await expect(fixture.page.getByRole('heading', { name: '账号与数据库' })).toBeVisible()
const autoLoginSwitch = fixture.page.getByRole('switch', { name: '启动时自动连接数据库' })
await expect(autoLoginSwitch).toBeVisible()
await autoLoginSwitch.click()
await expect(autoLoginSwitch).toBeChecked()
await fixture.page.getByRole('button', { name: '缓存与清理' }).click()
await expect(fixture.page.getByRole('heading', { name: '缓存与清理' })).toBeVisible()
await expect(fixture.page.getByRole('button', { name: '清理全部' })).toBeEnabled()
await fixture.page.getByRole('button', { name: '高级' }).click()
const debugSwitch = fixture.page.getByRole('switch', { name: '显示诊断日志' })
await expect(debugSwitch).toBeVisible()
await debugSwitch.click()
await expect(debugSwitch).toBeChecked()
await fixture.page.getByRole('button', { name: '防撤回' }).click()
const recallSwitch = fixture.page.getByRole('switch', { name: '开启防撤回' })
await expect(recallSwitch).toBeVisible()
await recallSwitch.click()
await expect(recallSwitch).toBeChecked()
await fixture.page.getByRole('button', { name: '关于' }).click()
await expect(fixture.page.getByRole('button', { name: '检查更新' })).toBeVisible()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('SETTINGS-03 database key actions keep destructive confirmation keyboard-safe', async () => {
const fixture = await launchTestApp()
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize({ width: 820, height: 600 })
await fixture.page
.getByRole('navigation', { name: '一级导航' })
.getByRole('button', { name: '设置' })
.click()
await fixture.page.getByRole('button', { name: '数据库密钥' }).click()
await expect(fixture.page.getByRole('heading', { name: '数据库密钥' })).toBeVisible()
await expect(fixture.page.getByRole('button', { name: '重新验证' })).toBeVisible()
const clearKey = fixture.page.getByRole('button', { name: '清除密钥' })
await clearKey.scrollIntoViewIfNeeded()
await clearKey.click()
const dialog = fixture.page.getByRole('alertdialog', { name: '确认清除数据库密钥?' })
await expect(dialog).toBeVisible()
await fixture.page.keyboard.press('Escape')
await expect(dialog).toHaveCount(0)
await expect(clearKey).toBeFocused()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('SETTINGS-04 image decryption controls keep filtering and selection keyboard-safe', async () => {
const fixture = await launchTestApp()
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize({ width: 820, height: 600 })
await fixture.page
.getByRole('navigation', { name: '一级导航' })
.getByRole('button', { name: '设置' })
.click()
await fixture.page.getByRole('button', { name: '图片解密' }).click()
await expect(fixture.page.getByRole('heading', { name: '图片解密', exact: true })).toBeVisible()
const filterGroup = fixture.page.getByRole('radiogroup', { name: '会话类型筛选' })
await filterGroup.scrollIntoViewIfNeeded()
const allFilter = filterGroup.getByRole('radio', { name: '全部 3' })
await expect(allFilter).toBeChecked()
await allFilter.focus()
await fixture.page.keyboard.press('ArrowRight')
const groupFilter = filterGroup.getByRole('radio', { name: '群聊 2' })
await expect(groupFilter).toBeFocused()
await fixture.page.keyboard.press('Space')
await expect(groupFilter).toBeChecked()
await fixture.page.getByRole('searchbox', { name: '搜索会话' }).fill('产品')
const conversationSelect = fixture.page.getByRole('combobox', { name: '选择会话' })
await conversationSelect.click()
await expect(fixture.page.getByRole('option', { name: '产品测试群' })).toBeVisible()
await expect(fixture.page.getByRole('option', { name: '折叠群聊样本' })).toHaveCount(0)
await fixture.page.keyboard.press('Escape')
await expect(conversationSelect).toBeFocused()
await conversationSelect.click()
await fixture.page.getByRole('option', { name: '产品测试群' }).click()
await expect(conversationSelect).toContainText('产品测试群')
const testAction = fixture.page.getByRole('button', { name: '测试图片解析' })
await expect(testAction).toBeEnabled()
await testAction.click()
await expect(fixture.page.getByText('图片可以读取')).toBeVisible()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(
await fixture.page
.locator('.settings-page-scroll')
.evaluate((element) => element.scrollWidth <= element.clientWidth)
).toBe(true)
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('SETTINGS-05 AI model editor keeps provider and capability semantics', async () => {
const fixture = await launchTestApp()
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize({ width: 820, height: 600 })
await fixture.page
.getByRole('navigation', { name: '一级导航' })
.getByRole('button', { name: '设置' })
.click()
await fixture.page.getByRole('button', { name: 'AI 模型' }).click()
await expect(fixture.page.getByRole('heading', { name: 'AI 模型' })).toBeVisible()
await expect(fixture.page.getByRole('button', { name: '当前默认' })).toBeDisabled()
await fixture.page.getByRole('button', { name: '添加供应商' }).click()
await expect(fixture.page.getByRole('heading', { name: '新增供应商' })).toBeVisible()
await expect(fixture.page.getByLabel('供应商名称')).toBeFocused()
await fixture.page.getByLabel('供应商 ID').fill('fixture-new-provider')
const presetSelect = fixture.page.getByRole('combobox', { name: '快速模板' })
await presetSelect.click()
await fixture.page.getByRole('option', { name: 'OpenAI', exact: true }).click()
await expect(fixture.page.getByLabel('供应商名称')).toHaveValue('OpenAI')
await expect(fixture.page.getByLabel('模型名称')).toHaveValue('gpt-4o-mini')
const providerType = fixture.page.getByRole('combobox', { name: '供应商类型' })
await providerType.click()
await expect(fixture.page.getByRole('option', { name: 'Anthropic Messages' })).toBeVisible()
await fixture.page.keyboard.press('Escape')
await expect(providerType).toBeFocused()
const vision = fixture.page.getByRole('checkbox', { name: '图片理解 gpt-4o-mini' })
const ocr = fixture.page.getByRole('checkbox', { name: '图片文字识别 gpt-4o-mini' })
await vision.click()
await expect(vision).toBeChecked()
await expect(ocr).toBeChecked()
await expect(fixture.page.getByRole('radio', { name: '默认 gpt-4o-mini' })).toBeChecked()
await fixture.page.getByText('高级设置', { exact: true }).click()
await expect(fixture.page.getByLabel('请求超时(ms')).toHaveValue('120000')
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(
await fixture.page
.locator('.settings-page-scroll')
.evaluate((element) => element.scrollWidth <= element.clientWidth)
).toBe(true)
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('SETTINGS-07 voice recognition controls keep selection semantics at a narrow viewport', async () => {
const fixture = await launchTestApp()
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize({ width: 820, height: 600 })
await fixture.page
.getByRole('navigation', { name: '一级导航' })
.getByRole('button', { name: '设置' })
.click()
await fixture.page.getByRole('button', { name: '语音转文字' }).click()
await expect(fixture.page.getByRole('heading', { name: '语音转文字' })).toBeVisible()
const categoryTabs = fixture.page.getByRole('tablist', { name: '会话类别' })
await categoryTabs.scrollIntoViewIfNeeded()
await expect(categoryTabs.getByRole('tab', { name: /群聊/ })).toHaveAttribute(
'data-state',
'active'
)
const conversation = fixture.page.getByRole('checkbox', { name: /产品测试群/ })
await conversation.click()
await expect(conversation).toBeChecked()
const rangeSelect = fixture.page.getByRole('combobox', { name: '语音转写时间范围' })
await rangeSelect.click()
await expect(fixture.page.getByRole('option', { name: '所选会话全部历史' })).toBeVisible()
await fixture.page.keyboard.press('Escape')
await expect(rangeSelect).toBeFocused()
await expect(fixture.page.getByRole('button', { name: '开始转写' })).toBeDisabled()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(
await fixture.page
.locator('.settings-page-scroll')
.evaluate((element) => element.scrollWidth <= element.clientWidth)
).toBe(true)
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('AGENT-01 Agent Hub controls stay usable in the offline narrow layout', async () => {
const fixture = await launchTestApp()
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize({ width: 820, height: 600 })
await fixture.page.getByRole('button', { name: 'Agent' }).click()
await expect(fixture.page.getByRole('heading', { name: 'Agent Hub' })).toBeVisible()
await expect(fixture.page.getByText('Agent Hub 未运行')).toBeVisible()
await expect(fixture.page.getByRole('button', { name: '扫码登录微信机器人' })).toBeDisabled()
const logSource = fixture.page.getByRole('combobox', { name: '筛选日志来源' })
await logSource.scrollIntoViewIfNeeded()
await logSource.click()
await expect(fixture.page.getByRole('option', { name: '微信连接器' })).toBeVisible()
await fixture.page.keyboard.press('Escape')
await expect(logSource).toBeFocused()
await expect(fixture.page.getByRole('button', { name: '复制日志' })).toBeDisabled()
await fixture.page.getByRole('button', { name: '清空' }).click()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(
await fixture.page
.locator('.agent-hub-workspace')
.evaluate((element) => element.scrollWidth <= element.clientWidth)
).toBe(true)
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
@@ -263,6 +573,15 @@ test('API-01 manages the local API token and previews the Reader Skill safely',
await fixture.page.getByRole('button', { name: '重新生成 Token' }).click()
await expect(fixture.page.getByText('Token 已生成')).toBeVisible()
const moreTrigger = fixture.page
.locator('#api-reader-skill')
.getByRole('button', { name: '更多' })
await moreTrigger.click()
await expect(fixture.page.getByRole('menuitem', { name: '打开本地文件夹' })).toBeVisible()
await fixture.page.keyboard.press('Escape')
await expect(fixture.page.getByRole('menu')).toHaveCount(0)
await expect(moreTrigger).toBeFocused()
const previewTrigger = fixture.page
.locator('#api-reader-skill')
.getByRole('button', { name: '预览 Skill' })
@@ -337,7 +656,7 @@ test('LAYOUT-01 core workspaces fit a narrow desktop viewport without page error
try {
await fixture.setWindowContentSize({ width: 820, height: 600 })
const navigation = fixture.page.getByRole('navigation', { name: '一级导航' })
for (const pageName of ['档案', '问问微信', '日报', '导出', '设置']) {
for (const pageName of ['档案', '问问微信', '日报', '导出', 'API', '设置']) {
await navigation.getByRole('button', { name: pageName }).click()
await expect(fixture.page.locator('main.app-shell-main')).not.toBeEmpty()
expect(
@@ -436,13 +755,20 @@ test('MEDIA-02 MEDIA-04 return accurate unsupported and HTTP 403 reasons', async
test('ASK-01 uses the local fixed AI service and keeps evidence in the UI', async () => {
const fixture = await launchTestApp()
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize({ width: 820, height: 600 })
await fixture.page.getByRole('button', { name: '问问微信' }).click()
await fixture.page.getByPlaceholder(/例如:技术交流群/).fill('测试群讨论了什么?')
await fixture.page.getByRole('button', { name: '开始分析' }).click()
await expect(fixture.page.getByText(/固定假回答:测试数据中的核心流程正常/)).toBeVisible({
timeout: 15_000
})
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
@@ -470,12 +796,18 @@ test('REPORT-01 REPORT-02 generates a fixed report with non-empty local assets',
await expect(fixture.page.getByRole('heading', { name: '生成群聊日报' })).toBeVisible()
const textModel = fixture.page.getByRole('combobox', { name: '文字总结模型' })
const visionModel = fixture.page.getByRole('combobox', { name: '图片理解模型' })
await expect(textModel).toHaveValue('fixture-provider::fixture-model')
await expect(visionModel).toHaveValue('fixture-provider::fixture-vision-model')
await expect(textModel.locator('option')).toHaveCount(2)
await expect(visionModel.locator('option')).toHaveCount(1)
await expect(textModel).toContainText('固定响应模型')
await expect(visionModel).toContainText('固定图片识别模型')
await textModel.click()
await expect(fixture.page.getByRole('option')).toHaveCount(2)
await fixture.page.keyboard.press('Escape')
await expect(textModel).toBeFocused()
await visionModel.click()
await expect(fixture.page.getByRole('option')).toHaveCount(1)
await fixture.page.keyboard.press('Escape')
await expect(visionModel).toBeFocused()
await fixture.page.locator('.report-source-item').filter({ hasText: '产品测试群' }).click()
await fixture.page.getByRole('button', { name: '近 7 天' }).click()
await fixture.page.getByRole('radio', { name: '近 7 天' }).click()
const generate = fixture.page.getByRole('button', { name: '开始生成日报' })
await expect(generate).toBeEnabled()
await generate.click()
@@ -530,7 +862,7 @@ test('REPORT-03 report failure is retryable and leaves other pages usable', asyn
await fixture.page.getByRole('button', { name: '日报' }).click()
await fixture.page.getByRole('button', { name: '开始生成日报' }).click()
await fixture.page.locator('.report-source-item').filter({ hasText: '产品测试群' }).click()
await fixture.page.getByRole('button', { name: '近 7 天' }).click()
await fixture.page.getByRole('radio', { name: '近 7 天' }).click()
await fixture.page.getByRole('button', { name: '开始生成日报' }).click()
await expect(fixture.page.getByText(/本地假服务错误 401/).first()).toBeVisible()
await expect(fixture.page.getByRole('button', { name: '使用所选模型重新生成' })).toBeEnabled()
+69 -4
View File
@@ -445,6 +445,33 @@ handle('voice:downloadModel', () => ({ success: true, status: voiceModelStatus('
handle('voice:cancelModelDownload', () => ({ success: true }))
handle('voice:removeModel', () => voiceModelStatus())
handle('voice:openModelDirectory', () => ({ success: true }))
handle('voice:getBatchProgress', () => ({
accountIdentity: 'fixture-account',
state: 'idle',
total: 0,
processed: 0,
cached: 0,
succeeded: 0,
failed: 0,
elapsedMs: 0,
estimatedRemainingMs: null
}))
handle('voice:getBatchConversationSummaries', (request) =>
request.conversationIds.map((conversationId, index) => ({
conversationId,
voiceMessageCount: index + 3
}))
)
handle('voice:getBatchPreflight', (request) => ({
accountIdentity: 'fixture-account',
conversationCount: request.conversationIds.length,
voiceMessageCount: request.conversationIds.length * 3,
cachedCount: 0,
pendingCount: request.conversationIds.length * 3,
failedCount: 0,
estimatedDurationMs: null,
modelReady: false
}))
handle('voice:recognize', () => ({ success: true, transcript: '固定脱敏转写文本', language: 'zh' }))
handle('voice:cancelRecognition', () => ({ success: true }))
handle('db:getSticker', (url) =>
@@ -612,8 +639,35 @@ handle('app-log:write', (entry) => {
})
handle('app-log:getPath', () => path.join(userData, 'logs', 'e2e.log'))
handle('app-log:reveal', () => undefined)
handle('cache:getSummary', () => ({ bootstrapBytes: 0, electronBytes: 0, totalBytes: 0 }))
handle('cache:clear', () => ({ bootstrapBytes: 0, electronBytes: 0, totalBytes: 0 }))
const cacheSummary = () => ({
items: [
{
id: 'bootstrap',
label: '启动缓存',
description: '联系人和会话的本地启动快照',
sizeBytes: 4096,
fileCount: 2
},
{
id: 'electron',
label: '界面缓存',
description: 'Electron 页面资源和网络缓存',
sizeBytes: 2 * 1024 * 1024,
fileCount: 12
},
{
id: 'knowledge',
label: '本地知识库索引',
description: '问问微信使用的本地检索索引',
sizeBytes: 12 * 1024 * 1024,
fileCount: 8
}
],
totalBytes: 14 * 1024 * 1024 + 4096,
updatedAt: fixtureNowMs
})
handle('cache:getSummary', cacheSummary)
handle('cache:clear', cacheSummary)
handle('api:getStatus', () => ({ running: false, host: settings.apiHost, port: settings.apiPort }))
handle('api:tokenStatus', () => ({
success: true,
@@ -660,6 +714,19 @@ handle('api:toggle', (enabled) => ({
host: settings.apiHost,
port: settings.apiPort
}))
const agentHubStatus = () => ({
hub: 'offline',
connector: 'disconnected',
updatedAt: fixtureNowMs,
dataApi: 'online',
databaseReady: true
})
handle('agent-hub:getStatus', agentHubStatus)
handle('agent-hub:getLogs', () => [])
handle('agent-hub:clearLogs', () => ({ success: true }))
handle('agent-hub:startLogin', () => ({ status: agentHubStatus() }))
handle('agent-hub:cancelLogin', () => ({ status: agentHubStatus() }))
handle('agent-hub:disconnect', () => ({ status: agentHubStatus() }))
handle('image:getConfig', () => ({
success: true,
configured: true,
@@ -734,8 +801,6 @@ handle('accounts:discover', (inputPath) =>
]
}
)
handle('agent-hub:getStatus', () => ({ state: 'disconnected', connected: false }))
handle('agent-hub:getLogs', () => [])
handle('app-update:getState', () => ({ status: 'idle', currentVersion: '2.2.0' }))
for (const channel of [
+513 -6
View File
@@ -49,6 +49,33 @@ test('ARCH-01 archive page visual @visual', async () => {
}
})
test('CHAT-01 archive search controls visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page.getByText('产品测试群', { exact: true }).click()
await fixture.page.getByRole('button', { name: '搜索当前聊天' }).click()
const searchInput = fixture.page.getByRole('textbox', { name: '搜索当前聊天内容' })
await searchInput.fill('测试')
await expect(searchInput).toHaveValue('测试')
expect(
await fixture.page
.locator('.chat-archive-header')
.evaluate((element) => element.scrollWidth <= element.clientWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('chat-content-search.png', {
animations: 'disabled',
caret: 'hide'
})
} finally {
await fixture.close()
}
})
test('ASK-01 AI Search idle page visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow })
const pageErrors: Error[] = []
@@ -100,6 +127,79 @@ test('ASK-03 AI Search result page visual @visual', async () => {
}
})
test('ASK-04 AI Search idle page dark visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow, appearanceTheme: 'dark' })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page.getByRole('button', { name: '问问微信' }).click()
await expect(fixture.page.getByRole('heading', { name: '问问你的微信' })).toBeVisible()
await expect(fixture.page.locator('html')).toHaveAttribute('data-theme', 'dark')
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('ai-search-idle-page-dark.png', {
animations: 'disabled',
caret: 'hide'
})
} finally {
await fixture.close()
}
})
test('ASK-05 AI Search result page dark visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow, appearanceTheme: 'dark' })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page.getByRole('button', { name: '问问微信' }).click()
await fixture.page.getByPlaceholder(/例如:技术交流群/).fill('测试群讨论了什么?')
await fixture.page.getByRole('button', { name: '开始分析' }).click()
await expect(fixture.page.getByText(/固定假回答:测试数据中的核心流程正常/)).toBeVisible({
timeout: 15_000
})
await expect(fixture.page.locator('html')).toHaveAttribute('data-theme', 'dark')
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('ai-search-result-page-dark.png', {
animations: 'disabled',
caret: 'hide'
})
} finally {
await fixture.close()
}
})
test('API-00 Reader Skill page visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page.getByRole('button', { name: 'API' }).click()
await expect(fixture.page.getByRole('heading', { name: 'TraceMemo Reader' })).toBeVisible()
await expect(fixture.page.getByText('API Token', { exact: true })).toBeVisible()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('api-center-page.png', {
animations: 'disabled',
caret: 'hide'
})
} finally {
await fixture.close()
}
})
test('API-01 Reader Skill preview visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow })
const pageErrors: Error[] = []
@@ -128,22 +228,29 @@ test('API-01 Reader Skill preview visual @visual', async () => {
}
})
test('CHAT-02 personal WeChat send dialog visual @visual', async () => {
test.skip(process.platform !== 'darwin', 'Personal WeChat sending is currently macOS-only')
test('API-02 Agent target segmented control visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page.getByText('产品测试群', { exact: true }).click()
await fixture.page.getByRole('button', { name: '发送消息' }).click()
await expect(fixture.page.getByRole('dialog', { name: '个人微信测试发送' })).toBeVisible()
await fixture.page.getByRole('button', { name: 'API' }).click()
const targetGroup = fixture.page.getByRole('radiogroup', { name: '选择目标 Agent' })
await targetGroup.evaluate((element) => element.scrollIntoView({ block: 'center' }))
const codexTarget = fixture.page.getByRole('radio', { name: 'Codex' })
await expect(codexTarget).toBeChecked()
await codexTarget.focus()
await fixture.page.keyboard.press('ArrowRight')
const claudeTarget = fixture.page.getByRole('radio', { name: 'Claude Code' })
await expect(claudeTarget).toBeFocused()
await fixture.page.keyboard.press('Space')
await expect(claudeTarget).toBeChecked()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('personal-wechat-send-dialog.png', {
await expect(fixture.page).toHaveScreenshot('api-agent-target.png', {
animations: 'disabled',
caret: 'hide'
})
@@ -152,6 +259,124 @@ test('CHAT-02 personal WeChat send dialog visual @visual', async () => {
}
})
test('REPORT-00 report configuration controls visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page.getByRole('button', { name: '日报' }).click()
await fixture.page.getByRole('button', { name: '开始生成日报' }).click()
await fixture.page.locator('.report-source-item').filter({ hasText: '产品测试群' }).click()
await expect(fixture.page.getByRole('heading', { name: '生成群聊日报' })).toBeVisible()
await expect(fixture.page.getByRole('radiogroup', { name: '总结范围' })).toBeVisible()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('report-config-page.png', {
animations: 'disabled',
caret: 'hide'
})
const modelHeading = fixture.page.getByRole('heading', { name: '模型配置' })
await modelHeading.scrollIntoViewIfNeeded()
await expect(modelHeading).toBeVisible()
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('report-model-controls.png', {
animations: 'disabled',
caret: 'hide'
})
const templateSection = fixture.page.getByRole('heading', { name: '日报模板' }).locator('..')
await templateSection.getByRole('button', { name: '查看版式' }).first().click()
const templateDialog = fixture.page.getByRole('dialog', { name: '经典日报' })
await expect(templateDialog).toBeVisible()
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('report-template-dialog.png', {
animations: 'disabled',
caret: 'hide'
})
} finally {
await fixture.close()
}
})
test('REPORT-01 report retry controls visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow, aiFailure: '401' })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page.getByRole('button', { name: '日报' }).click()
await fixture.page.getByRole('button', { name: '开始生成日报' }).click()
await fixture.page.locator('.report-source-item').filter({ hasText: '产品测试群' }).click()
await fixture.page.getByRole('radio', { name: '近 7 天' }).click()
await fixture.page.getByRole('button', { name: '开始生成日报' }).click()
await expect(fixture.page.getByText(/本地假服务错误 401/).first()).toBeVisible()
await expect(fixture.page.getByRole('combobox', { name: '切换模型' })).toBeVisible()
await expect(fixture.page.getByRole('button', { name: '使用所选模型重新生成' })).toBeEnabled()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('report-retry-controls.png', {
animations: 'disabled',
caret: 'hide'
})
} finally {
await fixture.close()
}
})
for (const appearanceTheme of ['light', 'dark'] as const) {
test(`CHAT-02 personal WeChat send dialog ${appearanceTheme} visual @visual`, async () => {
test.skip(process.platform !== 'darwin', 'Personal WeChat sending is currently macOS-only')
const fixture = await launchTestApp({ now: visualNow, appearanceTheme })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page.getByText('产品测试群', { exact: true }).click()
await fixture.page.getByRole('button', { name: '发送消息' }).click()
const dialog = fixture.page.getByRole('dialog', { name: '个人微信测试发送' })
await expect(dialog).toBeVisible()
await expect(fixture.page.locator('html')).toHaveAttribute('data-theme', appearanceTheme)
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await fixture.page.mouse.move(0, 0)
await fixture.page.waitForTimeout(700)
await clearScreenshotFocus(fixture.page)
const screenshotName =
appearanceTheme === 'light'
? 'personal-wechat-send-dialog.png'
: 'personal-wechat-send-dialog-dark.png'
await expect(fixture.page).toHaveScreenshot(screenshotName, {
animations: 'disabled',
caret: 'hide'
})
await dialog.getByRole('radio', { name: '语音' }).click()
await expect(dialog.getByRole('textbox', { name: '要生成的文字' })).toBeVisible()
await dialog.locator('footer').scrollIntoViewIfNeeded()
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot(
`personal-wechat-voice-controls-${appearanceTheme}.png`,
{
animations: 'disabled',
caret: 'hide'
}
)
} finally {
await fixture.close()
}
})
}
test('CHAT-03 image viewer visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow })
const pageErrors: Error[] = []
@@ -175,6 +400,288 @@ test('CHAT-03 image viewer visual @visual', async () => {
}
})
test('SETTINGS-02 basic settings controls visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page
.getByRole('navigation', { name: '一级导航' })
.getByRole('button', { name: '设置' })
.click()
await expect(fixture.page.getByRole('heading', { name: '账号与数据库' })).toBeVisible()
await expect(fixture.page.getByRole('switch', { name: '启动时自动连接数据库' })).toBeVisible()
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('settings-account-database.png', {
animations: 'disabled',
caret: 'hide'
})
await fixture.page.getByRole('button', { name: '缓存与清理' }).click()
await expect(fixture.page.getByRole('heading', { name: '缓存与清理' })).toBeVisible()
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('settings-cache-cleanup.png', {
animations: 'disabled',
caret: 'hide'
})
await fixture.page.getByRole('button', { name: '高级' }).click()
await expect(fixture.page.getByRole('switch', { name: '显示诊断日志' })).toBeVisible()
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('settings-advanced.png', {
animations: 'disabled',
caret: 'hide'
})
await fixture.page.getByRole('button', { name: '关于' }).click()
await expect(fixture.page.getByRole('button', { name: '检查更新' })).toBeVisible()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('settings-about.png', {
animations: 'disabled',
caret: 'hide'
})
} finally {
await fixture.close()
}
})
test('SETTINGS-03 database key controls visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page
.getByRole('navigation', { name: '一级导航' })
.getByRole('button', { name: '设置' })
.click()
await fixture.page.getByRole('button', { name: '数据库密钥' }).click()
await expect(fixture.page.getByRole('heading', { name: '数据库密钥' })).toBeVisible()
await expect(fixture.page.getByRole('button', { name: '重新验证' })).toBeVisible()
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('settings-database-key.png', {
animations: 'disabled',
caret: 'hide'
})
const clearKey = fixture.page.getByRole('button', { name: '清除密钥' })
await clearKey.scrollIntoViewIfNeeded()
await expect(clearKey).toBeVisible()
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('settings-database-key-danger.png', {
animations: 'disabled',
caret: 'hide'
})
} finally {
await fixture.close()
}
})
for (const appearanceTheme of ['light', 'dark'] as const) {
test(`SETTINGS-08 recall protection ${appearanceTheme} visual @visual`, async () => {
const fixture = await launchTestApp({ now: visualNow, appearanceTheme })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page
.getByRole('navigation', { name: '一级导航' })
.getByRole('button', { name: '设置' })
.click()
await fixture.page.getByRole('button', { name: '防撤回' }).click()
await expect(fixture.page.getByRole('switch', { name: '开启防撤回' })).toBeVisible()
await expect(fixture.page.locator('html')).toHaveAttribute('data-theme', appearanceTheme)
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot(
`settings-recall-protection-${appearanceTheme}.png`,
{
animations: 'disabled',
caret: 'hide'
}
)
} finally {
await fixture.close()
}
})
}
for (const appearanceTheme of ['light', 'dark'] as const) {
test(`SETTINGS-04 image decryption controls ${appearanceTheme} visual @visual`, async () => {
const fixture = await launchTestApp({ now: visualNow, appearanceTheme })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page
.getByRole('navigation', { name: '一级导航' })
.getByRole('button', { name: '设置' })
.click()
await fixture.page.getByRole('button', { name: '图片解密' }).click()
await expect(
fixture.page.getByRole('heading', { name: '图片解密', exact: true })
).toBeVisible()
const filterGroup = fixture.page.getByRole('radiogroup', { name: '会话类型筛选' })
await filterGroup.scrollIntoViewIfNeeded()
await expect(filterGroup.getByRole('radio', { name: '全部 3' })).toBeChecked()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot(
`settings-image-decryption-${appearanceTheme}.png`,
{
animations: 'disabled',
caret: 'hide'
}
)
} finally {
await fixture.close()
}
})
}
for (const appearanceTheme of ['light', 'dark'] as const) {
test(`SETTINGS-05 AI model editor ${appearanceTheme} visual @visual`, async () => {
const fixture = await launchTestApp({ now: visualNow, appearanceTheme })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page
.getByRole('navigation', { name: '一级导航' })
.getByRole('button', { name: '设置' })
.click()
await fixture.page.getByRole('button', { name: 'AI 模型' }).click()
await fixture.page.getByRole('button', { name: '添加供应商' }).click()
await expect(fixture.page.getByRole('heading', { name: '新增供应商' })).toBeVisible()
await fixture.page.getByLabel('供应商 ID').fill('fixture-new-provider')
await expect(fixture.page.getByRole('combobox', { name: '快速模板' })).toBeVisible()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot(
`settings-ai-model-editor-${appearanceTheme}.png`,
{
animations: 'disabled',
caret: 'hide'
}
)
} finally {
await fixture.close()
}
})
}
for (const appearanceTheme of ['light', 'dark'] as const) {
test(`SETTINGS-06 text-to-speech controls ${appearanceTheme} visual @visual`, async () => {
const fixture = await launchTestApp({ now: visualNow, appearanceTheme })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page
.getByRole('navigation', { name: '一级导航' })
.getByRole('button', { name: '设置' })
.click()
await fixture.page.getByRole('button', { name: '文字转语音' }).click()
const modelSelect = fixture.page.getByRole('combobox', { name: '合成模型' })
await modelSelect.scrollIntoViewIfNeeded()
await expect(modelSelect).toBeVisible()
await expect(fixture.page.getByRole('searchbox', { name: '按音色名称搜索' })).toBeDisabled()
await expect(fixture.page.locator('html')).toHaveAttribute('data-theme', appearanceTheme)
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot(
`settings-text-to-speech-controls-${appearanceTheme}.png`,
{
animations: 'disabled',
caret: 'hide'
}
)
} finally {
await fixture.close()
}
})
}
for (const appearanceTheme of ['light', 'dark'] as const) {
test(`SETTINGS-07 voice recognition controls ${appearanceTheme} visual @visual`, async () => {
const fixture = await launchTestApp({ now: visualNow, appearanceTheme })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page
.getByRole('navigation', { name: '一级导航' })
.getByRole('button', { name: '设置' })
.click()
await fixture.page.getByRole('button', { name: '语音转文字' }).click()
const categoryTabs = fixture.page.getByRole('tablist', { name: '会话类别' })
await categoryTabs.scrollIntoViewIfNeeded()
await expect(categoryTabs).toBeVisible()
await expect(fixture.page.getByRole('combobox', { name: '语音转写时间范围' })).toBeVisible()
await expect(fixture.page.locator('html')).toHaveAttribute('data-theme', appearanceTheme)
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot(
`settings-voice-recognition-controls-${appearanceTheme}.png`,
{
animations: 'disabled',
caret: 'hide'
}
)
} finally {
await fixture.close()
}
})
}
for (const appearanceTheme of ['light', 'dark'] as const) {
test(`AGENT-01 Agent Hub ${appearanceTheme} visual @visual`, async () => {
const fixture = await launchTestApp({ now: visualNow, appearanceTheme })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page.getByRole('button', { name: 'Agent' }).click()
await expect(fixture.page.getByRole('heading', { name: 'Agent Hub' })).toBeVisible()
await expect(fixture.page.getByText('Agent Hub 未运行')).toBeVisible()
await expect(fixture.page.getByRole('combobox', { name: '筛选日志来源' })).toBeVisible()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot(`agent-hub-${appearanceTheme}.png`, {
animations: 'disabled',
caret: 'hide'
})
} finally {
await fixture.close()
}
})
}
test('EXPORT-01 export workspace idle visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow })
const pageErrors: Error[] = []
+15
View File
@@ -2,6 +2,21 @@ import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import { afterEach, vi } from 'vitest'
if (!HTMLElement.prototype.hasPointerCapture) {
Object.defineProperties(HTMLElement.prototype, {
hasPointerCapture: { configurable: true, value: () => false },
releasePointerCapture: { configurable: true, value: () => undefined },
setPointerCapture: { configurable: true, value: () => undefined }
})
}
if (!HTMLElement.prototype.scrollIntoView) {
Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
configurable: true,
value: () => undefined
})
}
afterEach(() => cleanup())
function createMemoryStorage(): Storage {