fix: bug
This commit is contained in:
537
app/(tabs)/__tests__/message.test.tsx
Normal file
537
app/(tabs)/__tests__/message.test.tsx
Normal file
@@ -0,0 +1,537 @@
|
||||
import React from 'react'
|
||||
import { render, fireEvent, waitFor } from '@testing-library/react-native'
|
||||
import MessageScreen from '../message'
|
||||
|
||||
// Mock expo-status-bar
|
||||
jest.mock('expo-status-bar', () => ({
|
||||
StatusBar: 'StatusBar',
|
||||
}))
|
||||
|
||||
// Mock react-i18next
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'message.title': '消息',
|
||||
'message.markAllRead': '全部已读',
|
||||
'message.noMessages': '暂无消息',
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock expo-router
|
||||
const mockPush = jest.fn()
|
||||
jest.mock('expo-router', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
replace: jest.fn(),
|
||||
back: jest.fn(),
|
||||
}),
|
||||
useLocalSearchParams: () => ({}),
|
||||
}))
|
||||
|
||||
// Mock hooks
|
||||
const mockExecute = jest.fn()
|
||||
const mockLoadMore = jest.fn()
|
||||
const mockRefetch = jest.fn()
|
||||
const mockMarkRead = jest.fn()
|
||||
const mockBatchMarkRead = jest.fn()
|
||||
const mockDeleteMessage = jest.fn()
|
||||
const mockAnnouncementExecute = jest.fn()
|
||||
|
||||
// Default mock data
|
||||
const mockMessages = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'ACTIVITY' as const,
|
||||
title: '有人点赞了你的作品',
|
||||
content: '用户A点赞了你的模板',
|
||||
isRead: false,
|
||||
createdAt: new Date('2026-01-29T10:00:00'),
|
||||
data: { subType: 'TEMPLATE_LIKED', templateId: 'template-1' },
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'SYSTEM' as const,
|
||||
title: '生成成功',
|
||||
content: '你的模板已生成完成',
|
||||
isRead: true,
|
||||
createdAt: new Date('2026-01-28T10:00:00'),
|
||||
data: { subType: 'TEMPLATE_GENERATION_SUCCESS', generationId: 'gen-1' },
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'BILLING' as const,
|
||||
title: '积分扣除',
|
||||
content: '消耗10积分',
|
||||
isRead: false,
|
||||
createdAt: new Date('2026-01-27T10:00:00'),
|
||||
data: { subType: 'CREDITS_DEDUCTED', credits: 10 },
|
||||
},
|
||||
]
|
||||
|
||||
const mockAnnouncements = [
|
||||
{
|
||||
id: 'ann-1',
|
||||
title: '系统公告:新功能上线',
|
||||
content: '我们上线了新功能',
|
||||
link: 'https://example.com/announcement',
|
||||
createdAt: new Date('2026-01-29T08:00:00'),
|
||||
},
|
||||
]
|
||||
|
||||
jest.mock('@/hooks/use-messages', () => ({
|
||||
useMessages: jest.fn(() => ({
|
||||
messages: mockMessages,
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
error: null,
|
||||
execute: mockExecute,
|
||||
refetch: mockRefetch,
|
||||
loadMore: mockLoadMore,
|
||||
hasMore: true,
|
||||
})),
|
||||
}))
|
||||
|
||||
jest.mock('@/hooks/use-message-actions', () => ({
|
||||
useMessageActions: jest.fn(() => ({
|
||||
markRead: mockMarkRead,
|
||||
markReadLoading: false,
|
||||
markReadError: null,
|
||||
batchMarkRead: mockBatchMarkRead,
|
||||
batchMarkReadLoading: false,
|
||||
batchMarkReadError: null,
|
||||
deleteMessage: mockDeleteMessage,
|
||||
deleteLoading: false,
|
||||
deleteError: null,
|
||||
})),
|
||||
}))
|
||||
|
||||
jest.mock('@/hooks/use-announcements', () => ({
|
||||
useAnnouncements: jest.fn(() => ({
|
||||
announcements: mockAnnouncements,
|
||||
loading: false,
|
||||
error: null,
|
||||
execute: mockAnnouncementExecute,
|
||||
refetch: jest.fn(),
|
||||
loadMore: jest.fn(),
|
||||
hasMore: false,
|
||||
})),
|
||||
}))
|
||||
|
||||
jest.mock('@/hooks/use-message-unread-count', () => ({
|
||||
useMessageUnreadCount: jest.fn(() => ({
|
||||
count: 2,
|
||||
loading: false,
|
||||
error: null,
|
||||
execute: jest.fn(),
|
||||
})),
|
||||
}))
|
||||
|
||||
// Mock components
|
||||
jest.mock('@/components/LoadingState', () => 'LoadingState')
|
||||
jest.mock('@/components/ErrorState', () => 'ErrorState')
|
||||
jest.mock('@/components/PaginationLoader', () => 'PaginationLoader')
|
||||
|
||||
describe('MessageScreen', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Page Structure', () => {
|
||||
it('should render page header with title "消息"', () => {
|
||||
const { getByText } = render(<MessageScreen />)
|
||||
expect(getByText('消息')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should render "全部已读" button in header', () => {
|
||||
const { getByText } = render(<MessageScreen />)
|
||||
expect(getByText('全部已读')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should render MessageTabBar component', () => {
|
||||
const { getByTestId } = render(<MessageScreen />)
|
||||
expect(getByTestId('tab-all')).toBeTruthy()
|
||||
expect(getByTestId('tab-activity')).toBeTruthy()
|
||||
expect(getByTestId('tab-system')).toBeTruthy()
|
||||
expect(getByTestId('tab-billing')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Announcement Banner', () => {
|
||||
it('should render AnnouncementBanner when announcements exist', () => {
|
||||
const { getByTestId } = render(<MessageScreen />)
|
||||
expect(getByTestId('announcement-banner')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not render AnnouncementBanner when no announcements', () => {
|
||||
const { useAnnouncements } = require('@/hooks/use-announcements')
|
||||
useAnnouncements.mockReturnValue({
|
||||
announcements: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
execute: mockAnnouncementExecute,
|
||||
})
|
||||
|
||||
const { queryByTestId } = render(<MessageScreen />)
|
||||
expect(queryByTestId('announcement-banner')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Message List', () => {
|
||||
it('should render message list with MessageCard components', () => {
|
||||
const { getAllByTestId } = render(<MessageScreen />)
|
||||
const messageCards = getAllByTestId('message-card')
|
||||
expect(messageCards.length).toBe(3)
|
||||
})
|
||||
|
||||
it('should render MessageEmptyState when no messages', () => {
|
||||
const { useMessages } = require('@/hooks/use-messages')
|
||||
useMessages.mockReturnValue({
|
||||
messages: [],
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
error: null,
|
||||
execute: mockExecute,
|
||||
refetch: mockRefetch,
|
||||
loadMore: mockLoadMore,
|
||||
hasMore: false,
|
||||
})
|
||||
|
||||
const { getByTestId } = render(<MessageScreen />)
|
||||
expect(getByTestId('message-empty-state')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should wrap MessageCard with SwipeToDelete', () => {
|
||||
// Reset mock to default with messages
|
||||
const { useMessages } = require('@/hooks/use-messages')
|
||||
useMessages.mockReturnValue({
|
||||
messages: mockMessages,
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
error: null,
|
||||
execute: mockExecute,
|
||||
refetch: mockRefetch,
|
||||
loadMore: mockLoadMore,
|
||||
hasMore: true,
|
||||
})
|
||||
|
||||
const { getAllByTestId } = render(<MessageScreen />)
|
||||
const swipeContainers = getAllByTestId('swipe-container')
|
||||
expect(swipeContainers.length).toBe(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Tab Switching', () => {
|
||||
beforeEach(() => {
|
||||
// Reset to default mock
|
||||
const { useMessages } = require('@/hooks/use-messages')
|
||||
useMessages.mockReturnValue({
|
||||
messages: mockMessages,
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
error: null,
|
||||
execute: mockExecute,
|
||||
refetch: mockRefetch,
|
||||
loadMore: mockLoadMore,
|
||||
hasMore: true,
|
||||
})
|
||||
|
||||
const { useAnnouncements } = require('@/hooks/use-announcements')
|
||||
useAnnouncements.mockReturnValue({
|
||||
announcements: mockAnnouncements,
|
||||
loading: false,
|
||||
error: null,
|
||||
execute: mockAnnouncementExecute,
|
||||
})
|
||||
})
|
||||
|
||||
it('should show all messages when "全部" tab is active', () => {
|
||||
const { getAllByTestId } = render(<MessageScreen />)
|
||||
const messageCards = getAllByTestId('message-card')
|
||||
expect(messageCards.length).toBe(3)
|
||||
})
|
||||
|
||||
it('should filter ACTIVITY messages when "互动" tab is clicked', async () => {
|
||||
const { getByTestId, getAllByTestId } = render(<MessageScreen />)
|
||||
|
||||
fireEvent.press(getByTestId('tab-activity'))
|
||||
|
||||
await waitFor(() => {
|
||||
const messageCards = getAllByTestId('message-card')
|
||||
expect(messageCards.length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('should filter SYSTEM messages when "系统" tab is clicked', async () => {
|
||||
const { getByTestId, getAllByTestId } = render(<MessageScreen />)
|
||||
|
||||
fireEvent.press(getByTestId('tab-system'))
|
||||
|
||||
await waitFor(() => {
|
||||
const messageCards = getAllByTestId('message-card')
|
||||
expect(messageCards.length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('should filter BILLING messages when "账单" tab is clicked', async () => {
|
||||
const { getByTestId, getAllByTestId } = render(<MessageScreen />)
|
||||
|
||||
fireEvent.press(getByTestId('tab-billing'))
|
||||
|
||||
await waitFor(() => {
|
||||
const messageCards = getAllByTestId('message-card')
|
||||
expect(messageCards.length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('should show empty state when filtered tab has no messages', async () => {
|
||||
const { useMessages } = require('@/hooks/use-messages')
|
||||
useMessages.mockReturnValue({
|
||||
messages: [mockMessages[0]], // Only ACTIVITY message
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
error: null,
|
||||
execute: mockExecute,
|
||||
refetch: mockRefetch,
|
||||
loadMore: mockLoadMore,
|
||||
hasMore: false,
|
||||
})
|
||||
|
||||
const { getByTestId, queryByTestId } = render(<MessageScreen />)
|
||||
|
||||
fireEvent.press(getByTestId('tab-billing'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('message-empty-state')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Mark All Read', () => {
|
||||
beforeEach(() => {
|
||||
const { useMessages } = require('@/hooks/use-messages')
|
||||
useMessages.mockReturnValue({
|
||||
messages: mockMessages,
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
error: null,
|
||||
execute: mockExecute,
|
||||
refetch: mockRefetch,
|
||||
loadMore: mockLoadMore,
|
||||
hasMore: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should call batchMarkRead with unread message ids when "全部已读" is clicked', async () => {
|
||||
const { getByText } = render(<MessageScreen />)
|
||||
|
||||
fireEvent.press(getByText('全部已读'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockBatchMarkRead).toHaveBeenCalledWith(['1', '3'])
|
||||
})
|
||||
})
|
||||
|
||||
it('should only mark unread messages in current tab as read', async () => {
|
||||
const { getByText, getByTestId } = render(<MessageScreen />)
|
||||
|
||||
// Switch to ACTIVITY tab
|
||||
fireEvent.press(getByTestId('tab-activity'))
|
||||
|
||||
// Click mark all read
|
||||
fireEvent.press(getByText('全部已读'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockBatchMarkRead).toHaveBeenCalledWith(['1'])
|
||||
})
|
||||
})
|
||||
|
||||
it('should refetch messages after marking all as read', async () => {
|
||||
const { getByText } = render(<MessageScreen />)
|
||||
|
||||
fireEvent.press(getByText('全部已读'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRefetch).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Swipe to Delete', () => {
|
||||
beforeEach(() => {
|
||||
const { useMessages } = require('@/hooks/use-messages')
|
||||
useMessages.mockReturnValue({
|
||||
messages: mockMessages,
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
error: null,
|
||||
execute: mockExecute,
|
||||
refetch: mockRefetch,
|
||||
loadMore: mockLoadMore,
|
||||
hasMore: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should call deleteMessage when swipe delete button is pressed', async () => {
|
||||
const { getAllByTestId } = render(<MessageScreen />)
|
||||
|
||||
const deleteButtons = getAllByTestId('swipe-delete-button')
|
||||
fireEvent.press(deleteButtons[0])
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDeleteMessage).toHaveBeenCalledWith('1')
|
||||
})
|
||||
})
|
||||
|
||||
it('should refetch messages after deletion', async () => {
|
||||
const { getAllByTestId } = render(<MessageScreen />)
|
||||
|
||||
const deleteButtons = getAllByTestId('swipe-delete-button')
|
||||
fireEvent.press(deleteButtons[0])
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRefetch).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Message Click Navigation', () => {
|
||||
beforeEach(() => {
|
||||
const { useMessages } = require('@/hooks/use-messages')
|
||||
useMessages.mockReturnValue({
|
||||
messages: mockMessages,
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
error: null,
|
||||
execute: mockExecute,
|
||||
refetch: mockRefetch,
|
||||
loadMore: mockLoadMore,
|
||||
hasMore: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should navigate to /templateDetail when TEMPLATE_LIKED message is clicked', async () => {
|
||||
const { getAllByTestId } = render(<MessageScreen />)
|
||||
|
||||
const messageCards = getAllByTestId('message-card')
|
||||
fireEvent.press(messageCards[0])
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPush).toHaveBeenCalledWith(expect.stringContaining('/templateDetail'))
|
||||
})
|
||||
})
|
||||
|
||||
it('should navigate to /generationRecord when TEMPLATE_GENERATION_SUCCESS message is clicked', async () => {
|
||||
const { getAllByTestId } = render(<MessageScreen />)
|
||||
|
||||
const messageCards = getAllByTestId('message-card')
|
||||
fireEvent.press(messageCards[1])
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPush).toHaveBeenCalledWith(expect.stringContaining('/generationRecord'))
|
||||
})
|
||||
})
|
||||
|
||||
it('should navigate to /membership when CREDITS_DEDUCTED message is clicked', async () => {
|
||||
const { getAllByTestId } = render(<MessageScreen />)
|
||||
|
||||
const messageCards = getAllByTestId('message-card')
|
||||
fireEvent.press(messageCards[2])
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPush).toHaveBeenCalledWith(expect.stringContaining('/membership'))
|
||||
})
|
||||
})
|
||||
|
||||
it('should mark message as read when clicked', async () => {
|
||||
const { getAllByTestId } = render(<MessageScreen />)
|
||||
|
||||
const messageCards = getAllByTestId('message-card')
|
||||
fireEvent.press(messageCards[0])
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMarkRead).toHaveBeenCalledWith('1')
|
||||
})
|
||||
})
|
||||
|
||||
it('should not call markRead for already read messages', async () => {
|
||||
const { getAllByTestId } = render(<MessageScreen />)
|
||||
|
||||
const messageCards = getAllByTestId('message-card')
|
||||
fireEvent.press(messageCards[1]) // This is the read message
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMarkRead).not.toHaveBeenCalledWith('2')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Pagination', () => {
|
||||
it('should call loadMore when scrolling to bottom', async () => {
|
||||
const { getByTestId } = render(<MessageScreen />)
|
||||
|
||||
const flatList = getByTestId('message-list')
|
||||
|
||||
fireEvent(flatList, 'onEndReached')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockLoadMore).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Loading State', () => {
|
||||
it('should show loading state when loading is true and no messages', () => {
|
||||
const { useMessages } = require('@/hooks/use-messages')
|
||||
useMessages.mockReturnValue({
|
||||
messages: [],
|
||||
loading: true,
|
||||
loadingMore: false,
|
||||
error: null,
|
||||
execute: mockExecute,
|
||||
refetch: mockRefetch,
|
||||
loadMore: mockLoadMore,
|
||||
hasMore: true,
|
||||
})
|
||||
|
||||
const { UNSAFE_queryAllByType } = render(<MessageScreen />)
|
||||
const loadingStates = UNSAFE_queryAllByType('LoadingState' as any)
|
||||
expect(loadingStates.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error State', () => {
|
||||
it('should show error state when error exists and no messages', () => {
|
||||
const { useMessages } = require('@/hooks/use-messages')
|
||||
useMessages.mockReturnValue({
|
||||
messages: [],
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
error: { message: 'Network error' },
|
||||
execute: mockExecute,
|
||||
refetch: mockRefetch,
|
||||
loadMore: mockLoadMore,
|
||||
hasMore: false,
|
||||
})
|
||||
|
||||
const { UNSAFE_queryAllByType } = render(<MessageScreen />)
|
||||
const errorStates = UNSAFE_queryAllByType('ErrorState' as any)
|
||||
expect(errorStates.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Initial Data Fetch', () => {
|
||||
it('should call execute on mount', () => {
|
||||
render(<MessageScreen />)
|
||||
expect(mockExecute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call announcement execute on mount', () => {
|
||||
render(<MessageScreen />)
|
||||
expect(mockAnnouncementExecute).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user