fix: 完善聊天解析与导出体验

- 修复引用消息名称和图片布局
- 明确单会话图片测试日志范围
- 支持导出文件附件
- 完善图片批测、会话刷新和安全退出
This commit is contained in:
Wxw-Gu
2026-08-04 12:03:07 +08:00
parent d76727875d
commit c70e49bf16
41 changed files with 2320 additions and 221 deletions
@@ -0,0 +1,38 @@
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'
describe('ConversationSidebarHeader', () => {
it('refreshes the conversation list from the archive header', async () => {
const onRefresh = vi.fn()
render(
<ConversationSidebarHeader
totalCount={1306}
searchValue=""
onSearchChange={vi.fn()}
refreshing={false}
onRefresh={onRefresh}
/>
)
expect(screen.getByText('1306 个会话')).toBeInTheDocument()
await userEvent.click(screen.getByRole('button', { name: '刷新会话列表' }))
expect(onRefresh).toHaveBeenCalledOnce()
})
it('disables the refresh button while contacts are loading', () => {
render(
<ConversationSidebarHeader
totalCount={12}
searchValue="测试"
onSearchChange={vi.fn()}
refreshing
onRefresh={vi.fn()}
/>
)
expect(screen.getByRole('button', { name: '正在刷新会话列表' })).toBeDisabled()
expect(screen.getByDisplayValue('测试')).toBeInTheDocument()
})
})
+21
View File
@@ -9,6 +9,7 @@ vi.mock('../../src/renderer/src/components/image-loader', () => ({
}))
import { ImageBubble } from '../../src/renderer/src/components/ImageBubble'
import { RichMessageBubble } from '../../src/renderer/src/components/RichMessageBubble'
const thumbnail =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZQmcAAAAASUVORK5CYII='
@@ -50,4 +51,24 @@ describe('ImageBubble', () => {
await userEvent.click(screen.getByText('加载失败'))
expect(await screen.findByAltText('图片')).toBeVisible()
})
it('places a quoted image on the line below the quoted sender', async () => {
requestImage.mockResolvedValueOnce({ data: thumbnail, isThumbnail: true })
const { container } = render(
<RichMessageBubble
contentData={{
type: 'quote',
content: '回复内容',
quotedContent: '[图片]',
quotedSender: '测试群成员',
quotedImageMd5: 'fixture-image'
}}
sessionId="fixture-session"
/>
)
expect(screen.getByText('测试群成员')).toBeInTheDocument()
expect(container.querySelector('.quoted-message')).toHaveClass('quoted-message-image')
expect(await screen.findByAltText('图片')).toBeVisible()
})
})
@@ -0,0 +1,183 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { afterEach, describe, expect, it, vi } from 'vitest'
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'
const emptyBatchTest: ImageBatchTestState = {
running: false,
stopRequested: false,
elapsedMs: 0,
items: []
}
afterEach(() => {
vi.restoreAllMocks()
})
describe('ImageTestSection', () => {
it('keeps the file step successful when decryption fails and copies diagnostics', async () => {
const onCopyLog = vi.fn()
render(
<ImageTestSection
state={{
...initialImageDecryptionState,
phase: 'test-failed',
selectedUserMd5: 'conversation-md5',
contacts: [
{
md5: 'conversation-md5',
m_nsUsrName: 'fixture-user',
m_nsNickName: '测试会话',
type: 'user'
}
],
testResult: {
success: false,
code: 'DECRYPT_FAILED',
error: '图片密钥与当前账号不匹配',
fileFound: true,
decrypted: false,
readable: false,
diagnosticLog: 'WechatExplorer 图片解析测试日志(已脱敏)'
}
}}
batchTest={emptyBatchTest}
disabled={false}
canSave={false}
onSelect={vi.fn()}
onTest={vi.fn()}
onBatchTest={vi.fn()}
onStopBatchTest={vi.fn()}
onCopyLog={onCopyLog}
onSave={vi.fn()}
/>
)
expect(screen.getByText('找到图片文件').closest('li')).toHaveClass('image-step-ok')
expect(screen.getByText('解密成功').closest('li')).toHaveClass('image-step-fail')
expect(screen.getByText('图片可以读取').closest('li')).toHaveClass('image-step-skipped')
expect(screen.getByText('仅当前会话的单次测试日志')).toHaveAttribute(
'title',
expect.stringContaining('不包含批量测试结果')
)
await userEvent.click(screen.getByRole('button', { name: '复制日志' }))
expect(onCopyLog).toHaveBeenCalledOnce()
})
it('disables log copying before a test result exists', () => {
render(
<ImageTestSection
state={initialImageDecryptionState}
batchTest={emptyBatchTest}
disabled={false}
canSave={false}
onSelect={vi.fn()}
onTest={vi.fn()}
onBatchTest={vi.fn()}
onStopBatchTest={vi.fn()}
onCopyLog={vi.fn()}
onSave={vi.fn()}
/>
)
expect(screen.getByRole('button', { name: '复制日志' })).toBeDisabled()
})
it('filters groups and contacts before starting a batch test', async () => {
const onBatchTest = vi.fn()
vi.spyOn(window, 'confirm').mockReturnValue(true)
render(
<ImageTestSection
state={{
...initialImageDecryptionState,
contacts: [
{
md5: 'group-md5',
m_nsUsrName: 'group@chatroom',
m_nsNickName: '测试群聊',
type: 'group'
},
{
md5: 'user-md5',
m_nsUsrName: 'wxid_friend',
m_nsNickName: '测试联系人',
type: 'user'
}
]
}}
batchTest={emptyBatchTest}
disabled={false}
canSave={false}
onSelect={vi.fn()}
onTest={vi.fn()}
onBatchTest={onBatchTest}
onStopBatchTest={vi.fn()}
onCopyLog={vi.fn()}
onSave={vi.fn()}
/>
)
await userEvent.click(screen.getByRole('button', { name: '群聊 1' }))
expect(screen.getByRole('option', { name: '测试群聊' })).toBeInTheDocument()
expect(screen.queryByRole('option', { name: '测试联系人' })).not.toBeInTheDocument()
await userEvent.click(screen.getByRole('button', { name: '测试筛选结果(1' }))
expect(window.confirm).toHaveBeenCalledWith(expect.stringContaining('测试 1 个会话'))
expect(onBatchTest).toHaveBeenCalledWith([
expect.objectContaining({ md5: 'group-md5', type: 'group' })
])
})
it('shows per-conversation batch result and elapsed time', () => {
render(
<ImageTestSection
state={initialImageDecryptionState}
batchTest={{
running: false,
stopRequested: false,
elapsedMs: 1320,
items: [
{
contact: {
md5: 'success-md5',
m_nsUsrName: 'success@chatroom',
m_nsNickName: '成功群聊',
type: 'group'
},
status: 'success',
elapsedMs: 120
},
{
contact: {
md5: 'no-image-md5',
m_nsUsrName: 'wxid_no_image',
m_nsNickName: '没有图片的联系人',
type: 'user'
},
status: 'no-image',
elapsedMs: 88,
error: '最近 300 条消息中没有图片'
}
]
}}
disabled={false}
canSave={false}
onSelect={vi.fn()}
onTest={vi.fn()}
onBatchTest={vi.fn()}
onStopBatchTest={vi.fn()}
onCopyLog={vi.fn()}
onSave={vi.fn()}
/>
)
expect(screen.getByText('成功群聊')).toBeInTheDocument()
expect(screen.getByText('没有图片的联系人')).toBeInTheDocument()
expect(screen.getByText('成功 1')).toBeInTheDocument()
expect(screen.getByText('无图片 1')).toBeInTheDocument()
expect(screen.getByText(/2\/2 · 已耗时 1\.3 秒/)).toBeInTheDocument()
})
})