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