Files
WechatExplorer/tests/component/report-features.test.tsx
T
2026-08-12 16:10:22 +08:00

390 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ReportGroupMemberSelector } from '../../src/renderer/src/components/reports/ReportGroupMemberSelector'
import { ReportTaskStatusPanel } from '../../src/renderer/src/components/reports/ReportTaskStatusPanel'
import { ReportTemplateSelector } from '../../src/renderer/src/components/reports/ReportTemplateSelector'
import { ReportViewer } from '../../src/renderer/src/components/reports/ReportViewer'
import type { Contact } from '../../src/shared/types'
import type { GeneratedReportRecord } from '../../src/shared/report-history'
const noImageInsights = {
total: 0,
succeeded: 0,
failed: 0,
items: [],
failures: []
}
const currentModel = {
providerId: 'provider-1',
providerName: '默认服务',
model: 'model-1',
modelName: '默认模型',
configured: true,
status: 'connected' as const
}
const groupContact: Contact = {
md5: 'group-md5',
m_nsUsrName: 'group@chatroom',
m_nsNickName: '测试群',
type: 'group'
}
describe('daily report controls', () => {
beforeEach(() => {
Object.defineProperty(window, 'api', {
configurable: true,
value: {
getAppLogPath: vi.fn(async () => ''),
revealAppLog: vi.fn(async () => undefined),
listAIProviders: vi.fn(async () => ({ success: true, providers: [] })),
getGroupSnapshot: vi.fn(async () => ({
members: [
{
wxid: 'wxid-one',
nickname: '兼容名称一',
groupNickname: '群内昵称一',
wechatNickname: '微信昵称一',
remark: '通讯录备注一',
avatar: ''
},
{
wxid: 'wxid-two',
nickname: '兼容名称二',
groupNickname: '群内昵称二',
wechatNickname: '微信昵称二',
remark: '通讯录备注二',
avatar: ''
}
]
}))
}
})
})
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(
<ReportTaskStatusPanel
phase="transcribingVoice"
error=""
voiceTranscriptionProgress={progress}
voiceTranscriptionEnabled
preparationProgress={null}
imageInsightSummary={noImageInsights}
canRetryModelStep={false}
currentModel={currentModel}
onRetry={vi.fn()}
onContinueAfterImageFailures={vi.fn()}
onCancelAfterImageFailures={vi.fn()}
/>
)
expect(screen.getByText('2/5')).toBeVisible()
expect(screen.getByRole('progressbar', { name: '语音转写进度' })).toHaveAttribute('value', '2')
rerender(
<ReportTaskStatusPanel
phase="preparingInput"
error=""
voiceTranscriptionProgress={null}
voiceTranscriptionEnabled={false}
preparationProgress={null}
imageInsightSummary={noImageInsights}
canRetryModelStep={false}
currentModel={currentModel}
onRetry={vi.fn()}
onContinueAfterImageFailures={vi.fn()}
onCancelAfterImageFailures={vi.fn()}
/>
)
expect(screen.queryByText('转写语音消息')).not.toBeInTheDocument()
expect(screen.getByText('2/4')).toBeVisible()
})
it('shows image insight results and pauses for confirmation when some images fail', () => {
const onContinue = vi.fn()
const onCancel = vi.fn()
render(
<ReportTaskStatusPanel
phase="awaitingImageDecision"
error=""
voiceTranscriptionProgress={null}
voiceTranscriptionEnabled={false}
preparationProgress={{
stage: 'summarizingInput',
label: '等待确认是否继续文字总结',
completed: 2,
total: 3
}}
imageInsightSummary={{
total: 3,
succeeded: 2,
failed: 1,
items: [
{
messageId: 'image-1',
sender: '成员一',
time: '10:20',
description: '一张表格型网页截图。',
ocrText: '列 A 列 B',
tags: ['表格', '网页']
}
],
failures: [
{
messageId: 'image-2',
sender: '成员二',
time: '10:21',
error: 'fetch failed'
}
]
}}
canRetryModelStep={false}
currentModel={currentModel}
onRetry={vi.fn()}
onContinueAfterImageFailures={onContinue}
onCancelAfterImageFailures={onCancel}
/>
)
expect(screen.getByText('等待确认')).toBeVisible()
expect(screen.getByText('有 1 张图片识别失败')).toBeVisible()
expect(screen.getByText('一张表格型网页截图。')).toBeVisible()
expect(screen.getByText('OCR:列 A 列 B')).toBeVisible()
fireEvent.click(screen.getByRole('button', { name: '继续文字总结' }))
fireEvent.click(screen.getByRole('button', { name: '停止生成' }))
expect(onContinue).toHaveBeenCalledTimes(1)
expect(onCancel).toHaveBeenCalledTimes(1)
})
it('allows switching models and retrying only the model step', async () => {
const onRetry = vi.fn()
window.api.listAIProviders = vi.fn(async () => ({
success: true,
providers: [
{
id: 'provider-2',
name: '备用服务',
type: 'openai-compatible',
baseUrl: 'https://example.test',
auth: { type: 'bearer' },
models: [
{
id: 'model-2',
name: '备用模型',
capabilities: { chat: true, vision: false, ocr: false, longContext: true }
}
],
defaultModel: 'model-2',
advanced: { timeoutMs: 120000, extraHeaders: {} },
hasApiKey: true,
isDefault: false,
status: 'connected'
}
]
}))
render(
<ReportTaskStatusPanel
phase="error"
error="fetch failed"
voiceTranscriptionProgress={null}
voiceTranscriptionEnabled={false}
preparationProgress={null}
imageInsightSummary={noImageInsights}
canRetryModelStep
currentModel={currentModel}
onRetry={onRetry}
onContinueAfterImageFailures={vi.fn()}
onCancelAfterImageFailures={vi.fn()}
/>
)
const retryButton = await screen.findByRole('button', { name: '使用所选模型重新生成' })
expect(screen.getByRole('option', { name: '备用服务 · 备用模型' })).toBeVisible()
fireEvent.click(retryButton)
expect(onRetry).toHaveBeenCalledWith(
expect.objectContaining({ providerId: 'provider-2', model: 'model-2' })
)
expect(screen.getByText(/从第三步继续/)).toBeVisible()
})
it('zooms relative to a full-image fit constrained by viewport width and height', () => {
const originalResizeObserver = globalThis.ResizeObserver
globalThis.ResizeObserver = class {
observe(): void {
return undefined
}
disconnect(): void {
return undefined
}
unobserve(): void {
return undefined
}
}
const report: GeneratedReportRecord = {
id: 'report-1',
contactId: 'group-md5',
contactName: '测试群',
dateRange: '今天',
messageCount: 10,
generatedAt: '2026-08-12T10:00:00.000Z',
reportDate: '2026-08-12',
htmlStatus: 'ready',
pngStatus: 'ready',
generatedImage: 'data:image/png;base64,fixture'
}
render(
<ReportViewer
report={report}
hasReports
onBackToConfigure={vi.fn()}
onRegenerate={vi.fn()}
onCopyImage={vi.fn(async () => ({ success: true }))}
onReveal={vi.fn(async () => ({ success: true }))}
onSwitchTemplate={vi.fn(async () => ({ success: true }))}
/>
)
const image = screen.getByAltText('测试群 群聊日报') as HTMLImageElement
Object.defineProperty(image, 'naturalWidth', { configurable: true, value: 1440 })
Object.defineProperty(image, 'naturalHeight', { configurable: true, value: 4000 })
Object.defineProperty(image.parentElement?.parentElement, 'clientWidth', {
configurable: true,
value: 760
})
Object.defineProperty(image.parentElement?.parentElement, 'clientHeight', {
configurable: true,
value: 600
})
fireEvent.load(image)
expect(image.style.width).toBe('200px')
fireEvent.click(screen.getByRole('button', { name: '缩小' }))
expect(image.style.width).toBe('160px')
fireEvent.click(screen.getByRole('button', { name: '放大' }))
expect(image.style.width).toBe('200px')
expect(screen.getByRole('button', { name: '完整显示' })).toBeVisible()
fireEvent.click(screen.getByRole('button', { name: '原始大小' }))
expect(image.style.width).toBe('1440px')
fireEvent.click(screen.getByRole('button', { name: '完整显示' }))
expect(image.style.width).toBe('200px')
globalThis.ResizeObserver = originalResizeObserver
})
it('switches templates from the top toolbar using the saved report snapshot', async () => {
const onSwitchTemplate = vi.fn(async () => ({ success: true }))
const report: GeneratedReportRecord = {
id: 'report-switch',
contactId: 'group-md5',
contactName: '测试群',
dateRange: '今天',
messageCount: 10,
generatedAt: '2026-08-12T10:00:00.000Z',
reportDate: '2026-08-12',
htmlStatus: 'ready',
pngStatus: 'ready',
generatedImage: 'data:image/png;base64,fixture',
templateId: 'mobile-feed',
reportSnapshot: {} as GeneratedReportRecord['reportSnapshot'],
reportMetadata: {} as GeneratedReportRecord['reportMetadata']
}
const { rerender } = render(
<ReportViewer
report={report}
hasReports
onBackToConfigure={vi.fn()}
onRegenerate={vi.fn()}
onCopyImage={vi.fn(async () => ({ success: true }))}
onReveal={vi.fn(async () => ({ success: true }))}
onSwitchTemplate={onSwitchTemplate}
/>
)
fireEvent.click(screen.getByRole('button', { name: '切换模板' }))
expect(screen.getByText('仅重新排版,不调用 AI')).toBeVisible()
expect(screen.getByRole('menuitem', { name: /默认模板经典日报/ })).toBeVisible()
fireEvent.click(screen.getByRole('menuitem', { name: /Mobile 03AI Command Center/ }))
await waitFor(() => expect(onSwitchTemplate).toHaveBeenCalledWith(report, 'mobile-dashboard'))
rerender(
<ReportViewer
report={{
...report,
reportSnapshot: undefined,
reportMetadata: undefined,
htmlPath: '/tmp/legacy-report.html'
}}
hasReports
onBackToConfigure={vi.fn()}
onRegenerate={vi.fn()}
onCopyImage={vi.fn(async () => ({ success: true }))}
onReveal={vi.fn(async () => ({ success: true }))}
onSwitchTemplate={onSwitchTemplate}
/>
)
expect(screen.getByRole('button', { name: '切换模板' })).toBeEnabled()
expect(screen.getByRole('button', { name: '切换模板' })).toHaveAttribute(
'title',
'使用已生成的数据或本地 HTML 更换展示模板,不会重新调用 AI'
)
rerender(
<ReportViewer
report={{
...report,
reportSnapshot: undefined,
reportMetadata: undefined,
reportRenderSnapshot: undefined,
htmlPath: undefined
}}
hasReports
onBackToConfigure={vi.fn()}
onRegenerate={vi.fn()}
onCopyImage={vi.fn(async () => ({ success: true }))}
onReveal={vi.fn(async () => ({ success: true }))}
onSwitchTemplate={onSwitchTemplate}
/>
)
expect(screen.getByRole('button', { name: '切换模板' })).toBeDisabled()
expect(screen.getByRole('button', { name: '切换模板' })).toHaveAttribute(
'title',
'当前报告缺少可复用数据和 HTML,无法切换模板'
)
})
it('loads and displays group nickname, WeChat nickname, and remark separately', async () => {
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()
})
it('offers the classic default plus three mobile and two desktop report templates', () => {
const onChange = vi.fn()
render(<ReportTemplateSelector value="v1" onChange={onChange} />)
expect(screen.getAllByRole('radio')).toHaveLength(6)
expect(screen.getByText('默认模板', { selector: '.report-template-group-title' })).toBeVisible()
expect(screen.getByText('手机端 · 375414 px')).toBeVisible()
expect(screen.getByText('电脑端 · 12801920 px')).toBeVisible()
expect(screen.getByText('经典日报')).toBeVisible()
expect(screen.getByRole('radio', { name: /经典日报/ })).toBeChecked()
expect(screen.getByText('微信信息流')).toBeVisible()
expect(screen.getByText('AI Magazine')).toBeVisible()
expect(screen.getByText('AI Command Center')).toBeVisible()
expect(screen.getByText('三栏 AI 工作台')).toBeVisible()
expect(screen.getByText('Editorial 科技日报')).toBeVisible()
const previewButtons = screen.getAllByRole('button', { name: '查看版式' })
expect(previewButtons).toHaveLength(6)
fireEvent.click(previewButtons[2])
fireEvent.click(screen.getByRole('button', { name: '选择此模板' }))
expect(onChange).toHaveBeenCalledWith('mobile-magazine')
})
})