mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 19:47:08 +08:00
test: 建立桌面端自动化回归测试体系并完善跨平台 CI
(cherry picked from commit 74267ae2f63f8256c3da84b04f5b330e6e9d4c67)
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { render, screen, type RenderResult } from '@testing-library/react'
|
||||
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'
|
||||
|
||||
function renderPage(
|
||||
overrides: Partial<ComponentProps<typeof DatabaseConnectionPage>> = {}
|
||||
): RenderResult & { props: ComponentProps<typeof DatabaseConnectionPage> } {
|
||||
const props = {
|
||||
platform: 'win32',
|
||||
mode: 'manual' as const,
|
||||
dbKey: '',
|
||||
dbRoot: '',
|
||||
showDbKey: false,
|
||||
isFetching: false,
|
||||
status: '',
|
||||
statusKind: 'normal' as const,
|
||||
showMacKeyFaq: false,
|
||||
macKeyFaqUrl: 'https://fixture.invalid/mac',
|
||||
onModeChange: vi.fn(),
|
||||
onDbKeyChange: vi.fn(),
|
||||
onDbRootChange: vi.fn(),
|
||||
onToggleDbKey: vi.fn(),
|
||||
onAutoGetKey: vi.fn(),
|
||||
onManualConnect: vi.fn(),
|
||||
onPasteKey: vi.fn(),
|
||||
onClearKey: vi.fn(),
|
||||
...overrides
|
||||
}
|
||||
return { props, ...render(<DatabaseConnectionPage {...props} />) }
|
||||
}
|
||||
|
||||
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)} />)
|
||||
expect(screen.getByRole('button', { name: '连接数据库' })).toBeEnabled()
|
||||
})
|
||||
|
||||
it('shows a recoverable error and keeps form actions available', async () => {
|
||||
const onManualConnect = vi.fn()
|
||||
renderPage({
|
||||
dbKey: 'b'.repeat(64),
|
||||
status: '数据库密钥无效,请重新输入',
|
||||
statusKind: 'error',
|
||||
onManualConnect
|
||||
})
|
||||
expect(screen.getByText('数据库密钥无效,请重新输入')).toBeVisible()
|
||||
await userEvent.click(screen.getByRole('button', { name: '连接数据库' }))
|
||||
expect(onManualConnect).toHaveBeenCalledOnce()
|
||||
expect(screen.getByRole('button', { name: '从剪贴板粘贴并安全保存' })).toBeEnabled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { EmptyConversationState } from '../../src/renderer/src/components/chat/EmptyConversationState'
|
||||
import { SettingsEmptyState } from '../../src/renderer/src/features/settings/components/SettingsEmptyState'
|
||||
|
||||
describe('empty states', () => {
|
||||
it('explains how to leave an empty archive without pretending data failed', () => {
|
||||
render(<EmptyConversationState />)
|
||||
expect(screen.getByRole('heading', { name: '选择一条消息' })).toBeVisible()
|
||||
expect(screen.getByText('从左侧选择群聊或联系人以浏览历史记录')).toBeVisible()
|
||||
})
|
||||
|
||||
it('labels an unavailable settings section explicitly', () => {
|
||||
render(<SettingsEmptyState label="测试设置" />)
|
||||
expect(screen.getByRole('heading', { name: '测试设置' })).toBeVisible()
|
||||
expect(screen.getByText('该设置将在后续阶段接入。')).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const requestImage = vi.fn()
|
||||
vi.mock('../../src/renderer/src/components/image-loader', () => ({
|
||||
getCachedLoadedImage: vi.fn(() => undefined),
|
||||
requestImage: (...args: unknown[]) => requestImage(...args)
|
||||
}))
|
||||
|
||||
import { ImageBubble } from '../../src/renderer/src/components/ImageBubble'
|
||||
|
||||
const thumbnail =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZQmcAAAAASUVORK5CYII='
|
||||
const original = `${thumbnail}original`
|
||||
|
||||
describe('ImageBubble', () => {
|
||||
beforeEach(() => {
|
||||
requestImage.mockReset()
|
||||
window.api = { copyImage: vi.fn().mockResolvedValue({ success: true }) } as typeof window.api
|
||||
})
|
||||
|
||||
it('loads a thumbnail lazily, then requests the original when opened', async () => {
|
||||
requestImage
|
||||
.mockResolvedValueOnce({ data: thumbnail, isThumbnail: true })
|
||||
.mockResolvedValueOnce({ data: original, isThumbnail: false })
|
||||
const onImageClick = vi.fn()
|
||||
render(
|
||||
<ImageBubble
|
||||
imageMd5="fixture-image"
|
||||
imageDatName="fixture.dat"
|
||||
sessionId="fixture-session"
|
||||
onImageClick={onImageClick}
|
||||
/>
|
||||
)
|
||||
|
||||
const image = await screen.findByAltText('图片')
|
||||
expect(image).toHaveAttribute('src', thumbnail)
|
||||
await userEvent.click(image)
|
||||
await waitFor(() => expect(onImageClick).toHaveBeenCalledWith(original))
|
||||
expect(requestImage.mock.calls[1][3]).toMatchObject({ force: true })
|
||||
})
|
||||
|
||||
it('shows an explicit error and allows retry', async () => {
|
||||
requestImage.mockRejectedValueOnce(new Error('不支持的 DAT 版本'))
|
||||
render(<ImageBubble imageMd5="unsupported" />)
|
||||
expect(await screen.findByText('不支持的 DAT 版本')).toBeVisible()
|
||||
|
||||
requestImage.mockResolvedValueOnce({ data: thumbnail, isThumbnail: true })
|
||||
await userEvent.click(screen.getByText('加载失败'))
|
||||
expect(await screen.findByAltText('图片')).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { PrimaryNavigation } from '../../src/renderer/src/components/layout/PrimaryNavigation'
|
||||
import { PRIMARY_NAV_ITEMS } from '../../src/renderer/src/components/layout/navigation'
|
||||
|
||||
describe('PrimaryNavigation', () => {
|
||||
it('shows every real top-level page exactly once and emits the selected page', async () => {
|
||||
const onPageChange = vi.fn()
|
||||
render(<PrimaryNavigation activePage="archive" onPageChange={onPageChange} />)
|
||||
|
||||
const navigation = screen.getByRole('navigation', { name: '一级导航' })
|
||||
expect(navigation).toBeInTheDocument()
|
||||
for (const item of PRIMARY_NAV_ITEMS) {
|
||||
expect(screen.getAllByRole('button', { name: item.label })).toHaveLength(1)
|
||||
}
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: '设置' }))
|
||||
expect(onPageChange).toHaveBeenCalledWith('settings')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { VoicePlayer } from '../../src/renderer/src/components/VoicePlayer'
|
||||
|
||||
const play = vi.fn(() => Promise.resolve())
|
||||
const pause = vi.fn()
|
||||
|
||||
class FakeAudio {
|
||||
preload = ''
|
||||
src = ''
|
||||
duration = 1
|
||||
currentTime = 0
|
||||
onloadedmetadata: (() => void) | null = null
|
||||
ontimeupdate: (() => void) | null = null
|
||||
onended: (() => void) | null = null
|
||||
play = play
|
||||
pause = pause
|
||||
load = vi.fn()
|
||||
removeAttribute = vi.fn()
|
||||
}
|
||||
|
||||
describe('VoicePlayer', () => {
|
||||
beforeEach(() => {
|
||||
play.mockClear()
|
||||
pause.mockClear()
|
||||
vi.stubGlobal('Audio', FakeAudio)
|
||||
window.api = {
|
||||
getVoiceData: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
data: 'UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA='
|
||||
})
|
||||
} as typeof window.api
|
||||
})
|
||||
|
||||
it('waits for decrypted bytes and calls play on the first click', async () => {
|
||||
const { container } = render(
|
||||
<VoicePlayer sessionId="filehelper" localId={11} createTime={1785553200} duration={1} />
|
||||
)
|
||||
await userEvent.click(container.querySelector('.voice-message') as HTMLElement)
|
||||
|
||||
await waitFor(() => expect(window.api.getVoiceData).toHaveBeenCalledOnce())
|
||||
await waitFor(() => expect(play).toHaveBeenCalledOnce())
|
||||
expect(container.querySelector('.voice-icon')).toHaveClass('playing')
|
||||
expect(screen.queryByText('当前版本暂不支持播放')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user