555 lines
16 KiB
TypeScript
555 lines
16 KiB
TypeScript
import React from 'react'
|
|
import { render, fireEvent, waitFor } from '@testing-library/react-native'
|
|
import { View, Text, ActivityIndicator } from 'react-native'
|
|
import { useRouter } from 'expo-router'
|
|
import LikesScreen from './likes'
|
|
|
|
// Mock expo-router
|
|
jest.mock('expo-router', () => ({
|
|
useRouter: jest.fn(),
|
|
useLocalSearchParams: jest.fn(() => ({})),
|
|
}))
|
|
|
|
// Mock react-i18next
|
|
jest.mock('react-i18next', () => ({
|
|
useTranslation: jest.fn(() => ({
|
|
t: (key: string) => key,
|
|
i18n: { language: 'zh-CN' },
|
|
})),
|
|
}))
|
|
|
|
// Mock expo-status-bar
|
|
jest.mock('expo-status-bar', () => ({
|
|
StatusBar: 'StatusBar',
|
|
}))
|
|
|
|
// Mock react-native-safe-area-context
|
|
jest.mock('react-native-safe-area-context', () => ({
|
|
SafeAreaView: ({ children }: { children: React.ReactNode }) => (
|
|
<View testID="safe-area">{children}</View>
|
|
),
|
|
useSafeAreaInsets: jest.fn(() => ({ top: 0, bottom: 0, left: 0, right: 0 })),
|
|
}))
|
|
|
|
// Mock expo-linear-gradient
|
|
jest.mock('expo-linear-gradient', () => ({
|
|
LinearGradient: 'LinearGradient',
|
|
}))
|
|
|
|
// Mock expo-image
|
|
jest.mock('expo-image', () => ({
|
|
Image: 'Image',
|
|
}))
|
|
|
|
// Mock react-native RefreshControl
|
|
jest.mock('react-native', () =>
|
|
Object.assign({}, jest.requireActual('react-native'), {
|
|
RefreshControl: 'RefreshControl',
|
|
})
|
|
)
|
|
|
|
// Mock UI components
|
|
jest.mock('@/components/LoadingState', () => ({
|
|
__esModule: true,
|
|
default: ({ message, testID }: any) => (
|
|
<View testID={testID || 'loading-state'}>
|
|
<ActivityIndicator size="large" color="#FFFFFF" />
|
|
{message && <Text>{message}</Text>}
|
|
</View>
|
|
),
|
|
}))
|
|
|
|
jest.mock('@/components/ErrorState', () => ({
|
|
__esModule: true,
|
|
default: ({ message, onRetry, testID, variant }: any) => (
|
|
<View testID={testID || 'error-state'}>
|
|
<Text>{variant === 'error' ? 'Error Icon' : 'Empty Icon'}</Text>
|
|
<Text>{message}</Text>
|
|
{onRetry && <Text onPress={onRetry}>重新加载</Text>}
|
|
</View>
|
|
),
|
|
}))
|
|
|
|
jest.mock('@/components/blocks/home/TemplateCard', () => ({
|
|
__esModule: true,
|
|
TemplateCard: ({ id, title, onPress, testID }: any) => (
|
|
<View testID={testID || `template-card-${id}`}>
|
|
<Text>{title}</Text>
|
|
<Text onPress={() => onPress(id)}>Press</Text>
|
|
</View>
|
|
),
|
|
}))
|
|
|
|
// Mock hooks
|
|
const mockExecute = jest.fn()
|
|
const mockRefetch = jest.fn()
|
|
const mockLoadMore = jest.fn()
|
|
|
|
jest.mock('@/hooks', () => ({
|
|
useUserLikes: jest.fn(() => ({
|
|
likes: [],
|
|
loading: false,
|
|
loadingMore: false,
|
|
error: null,
|
|
execute: mockExecute,
|
|
refetch: mockRefetch,
|
|
loadMore: mockLoadMore,
|
|
hasMore: false,
|
|
data: undefined,
|
|
})),
|
|
}))
|
|
|
|
import { useUserLikes } from '@/hooks'
|
|
|
|
const mockUseRouter = useRouter as jest.MockedFunction<typeof useRouter>
|
|
const mockUseUserLikes = useUserLikes as jest.MockedFunction<typeof useUserLikes>
|
|
|
|
const createMockLikeItem = (overrides = {}) => ({
|
|
id: 'like-1',
|
|
templateId: 'template-1',
|
|
createdAt: new Date('2024-01-01'),
|
|
template: {
|
|
id: 'template-1',
|
|
title: 'Test Template 1',
|
|
titleEn: 'Test Template 1',
|
|
previewUrl: 'https://example.com/preview1.jpg',
|
|
webpPreviewUrl: 'https://example.com/preview1.webp',
|
|
coverImageUrl: 'https://example.com/cover1.jpg',
|
|
aspectRatio: '1:1',
|
|
},
|
|
...overrides,
|
|
})
|
|
|
|
describe('Likes Screen', () => {
|
|
const mockPush = jest.fn()
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
mockUseRouter.mockReturnValue({
|
|
push: mockPush,
|
|
} as any)
|
|
})
|
|
|
|
describe('Page Rendering', () => {
|
|
it('should render the likes screen', () => {
|
|
mockUseUserLikes.mockReturnValue({
|
|
likes: [],
|
|
loading: false,
|
|
loadingMore: false,
|
|
error: null,
|
|
execute: mockExecute,
|
|
refetch: mockRefetch,
|
|
loadMore: mockLoadMore,
|
|
hasMore: false,
|
|
data: undefined,
|
|
} as any)
|
|
|
|
const { getByTestId } = render(<LikesScreen />)
|
|
|
|
expect(getByTestId('safe-area')).toBeTruthy()
|
|
})
|
|
|
|
it('should render loading state when loading', () => {
|
|
mockUseUserLikes.mockReturnValue({
|
|
likes: [],
|
|
loading: true,
|
|
loadingMore: false,
|
|
error: null,
|
|
execute: mockExecute,
|
|
refetch: mockRefetch,
|
|
loadMore: mockLoadMore,
|
|
hasMore: false,
|
|
data: undefined,
|
|
} as any)
|
|
|
|
const { getByTestId } = render(<LikesScreen />)
|
|
|
|
expect(getByTestId('loading-state')).toBeTruthy()
|
|
})
|
|
|
|
it('should render error state when error occurs', () => {
|
|
const mockError = { message: 'Failed to load likes' }
|
|
mockUseUserLikes.mockReturnValue({
|
|
likes: [],
|
|
loading: false,
|
|
loadingMore: false,
|
|
error: mockError,
|
|
execute: mockExecute,
|
|
refetch: mockRefetch,
|
|
loadMore: mockLoadMore,
|
|
hasMore: false,
|
|
data: undefined,
|
|
} as any)
|
|
|
|
const { getByTestId, getByText } = render(<LikesScreen />)
|
|
|
|
expect(getByTestId('error-state')).toBeTruthy()
|
|
expect(getByText('加载失败,请下拉刷新重试')).toBeTruthy()
|
|
})
|
|
|
|
it('should render empty state when no likes', () => {
|
|
mockUseUserLikes.mockReturnValue({
|
|
likes: [],
|
|
loading: false,
|
|
loadingMore: false,
|
|
error: null,
|
|
execute: mockExecute,
|
|
refetch: mockRefetch,
|
|
loadMore: mockLoadMore,
|
|
hasMore: false,
|
|
data: { likes: [], total: 0, page: 1, limit: 20 },
|
|
} as any)
|
|
|
|
const { getByTestId, getByText } = render(<LikesScreen />)
|
|
|
|
expect(getByTestId('error-state')).toBeTruthy()
|
|
expect(getByText('暂无点赞')).toBeTruthy()
|
|
})
|
|
|
|
it('should render likes list when data is loaded', () => {
|
|
const mockLikes = [
|
|
createMockLikeItem({
|
|
id: 'like-1',
|
|
templateId: 'template-1',
|
|
template: {
|
|
id: 'template-1',
|
|
title: 'Template 1',
|
|
titleEn: 'Template 1',
|
|
previewUrl: 'https://example.com/preview1.jpg',
|
|
coverImageUrl: 'https://example.com/cover1.jpg',
|
|
aspectRatio: '1:1',
|
|
},
|
|
}),
|
|
createMockLikeItem({
|
|
id: 'like-2',
|
|
templateId: 'template-2',
|
|
template: {
|
|
id: 'template-2',
|
|
title: 'Template 2',
|
|
titleEn: 'Template 2',
|
|
previewUrl: 'https://example.com/preview2.jpg',
|
|
coverImageUrl: 'https://example.com/cover2.jpg',
|
|
aspectRatio: '1:1',
|
|
},
|
|
}),
|
|
]
|
|
|
|
mockUseUserLikes.mockReturnValue({
|
|
likes: mockLikes,
|
|
loading: false,
|
|
loadingMore: false,
|
|
error: null,
|
|
execute: mockExecute,
|
|
refetch: mockRefetch,
|
|
loadMore: mockLoadMore,
|
|
hasMore: false,
|
|
data: { likes: mockLikes, total: 2, page: 1, limit: 20 },
|
|
} as any)
|
|
|
|
const { getByText } = render(<LikesScreen />)
|
|
|
|
expect(getByText('Template 1')).toBeTruthy()
|
|
expect(getByText('Template 2')).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
describe('Loading States', () => {
|
|
it('should show loading indicator on initial load', () => {
|
|
mockUseUserLikes.mockReturnValue({
|
|
likes: [],
|
|
loading: true,
|
|
loadingMore: false,
|
|
error: null,
|
|
execute: mockExecute,
|
|
refetch: mockRefetch,
|
|
loadMore: mockLoadMore,
|
|
hasMore: false,
|
|
data: undefined,
|
|
} as any)
|
|
|
|
const { getByTestId } = render(<LikesScreen />)
|
|
|
|
expect(getByTestId('loading-state')).toBeTruthy()
|
|
})
|
|
|
|
it('should show loading more indicator at bottom', () => {
|
|
const mockLikes = [createMockLikeItem()]
|
|
|
|
mockUseUserLikes.mockReturnValue({
|
|
likes: mockLikes,
|
|
loading: false,
|
|
loadingMore: true,
|
|
error: null,
|
|
execute: mockExecute,
|
|
refetch: mockRefetch,
|
|
loadMore: mockLoadMore,
|
|
hasMore: true,
|
|
data: { likes: mockLikes, total: 10, page: 1, limit: 5 },
|
|
} as any)
|
|
|
|
const { getByTestId } = render(<LikesScreen />)
|
|
|
|
expect(getByTestId('loading-more')).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
describe('Error States', () => {
|
|
it('should display error message when loading fails', () => {
|
|
const mockError = { message: 'Network error' }
|
|
|
|
mockUseUserLikes.mockReturnValue({
|
|
likes: [],
|
|
loading: false,
|
|
loadingMore: false,
|
|
error: mockError,
|
|
execute: mockExecute,
|
|
refetch: mockRefetch,
|
|
loadMore: mockLoadMore,
|
|
hasMore: false,
|
|
data: undefined,
|
|
} as any)
|
|
|
|
const { getByText } = render(<LikesScreen />)
|
|
|
|
expect(getByText('加载失败,请下拉刷新重试')).toBeTruthy()
|
|
})
|
|
|
|
it('should call refetch when retry button is pressed', () => {
|
|
const mockError = { message: 'Network error' }
|
|
|
|
mockUseUserLikes.mockReturnValue({
|
|
likes: [],
|
|
loading: false,
|
|
loadingMore: false,
|
|
error: mockError,
|
|
execute: mockExecute,
|
|
refetch: mockRefetch,
|
|
loadMore: mockLoadMore,
|
|
hasMore: false,
|
|
data: undefined,
|
|
} as any)
|
|
|
|
const { getByText } = render(<LikesScreen />)
|
|
|
|
const retryButton = getByText('重新加载')
|
|
fireEvent.press(retryButton)
|
|
|
|
expect(mockRefetch).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('List Display', () => {
|
|
it('should display all liked templates', () => {
|
|
const mockLikes = [
|
|
createMockLikeItem({
|
|
templateId: 'template-1',
|
|
template: { id: 'template-1', title: 'Template 1', previewUrl: 'https://example.com/1.jpg', coverImageUrl: 'https://example.com/1.jpg' },
|
|
}),
|
|
createMockLikeItem({
|
|
templateId: 'template-2',
|
|
template: { id: 'template-2', title: 'Template 2', previewUrl: 'https://example.com/2.jpg', coverImageUrl: 'https://example.com/2.jpg' },
|
|
}),
|
|
createMockLikeItem({
|
|
templateId: 'template-3',
|
|
template: { id: 'template-3', title: 'Template 3', previewUrl: 'https://example.com/3.jpg', coverImageUrl: 'https://example.com/3.jpg' },
|
|
}),
|
|
]
|
|
|
|
mockUseUserLikes.mockReturnValue({
|
|
likes: mockLikes,
|
|
loading: false,
|
|
loadingMore: false,
|
|
error: null,
|
|
execute: mockExecute,
|
|
refetch: mockRefetch,
|
|
loadMore: mockLoadMore,
|
|
hasMore: false,
|
|
data: { likes: mockLikes, total: 3, page: 1, limit: 20 },
|
|
} as any)
|
|
|
|
const { getByText } = render(<LikesScreen />)
|
|
|
|
expect(getByText('Template 1')).toBeTruthy()
|
|
expect(getByText('Template 2')).toBeTruthy()
|
|
expect(getByText('Template 3')).toBeTruthy()
|
|
})
|
|
|
|
it('should navigate to template detail when template is pressed', () => {
|
|
const mockLikes = [
|
|
createMockLikeItem({
|
|
templateId: 'template-1',
|
|
template: { id: 'template-1', title: 'Template 1', previewUrl: 'https://example.com/1.jpg', coverImageUrl: 'https://example.com/1.jpg' },
|
|
}),
|
|
]
|
|
|
|
mockUseUserLikes.mockReturnValue({
|
|
likes: mockLikes,
|
|
loading: false,
|
|
loadingMore: false,
|
|
error: null,
|
|
execute: mockExecute,
|
|
refetch: mockRefetch,
|
|
loadMore: mockLoadMore,
|
|
hasMore: false,
|
|
data: { likes: mockLikes, total: 1, page: 1, limit: 20 },
|
|
} as any)
|
|
|
|
const { getByText } = render(<LikesScreen />)
|
|
|
|
const pressable = getByText('Press')
|
|
fireEvent.press(pressable)
|
|
|
|
expect(mockPush).toHaveBeenCalledWith({
|
|
pathname: '/templateDetail',
|
|
params: { id: 'template-1' },
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Pull to Refresh', () => {
|
|
it('should call refetch when refresh is triggered', async () => {
|
|
const mockLikes = [createMockLikeItem()]
|
|
|
|
mockUseUserLikes.mockReturnValue({
|
|
likes: mockLikes,
|
|
loading: false,
|
|
loadingMore: false,
|
|
error: null,
|
|
execute: mockExecute,
|
|
refetch: mockRefetch,
|
|
loadMore: mockLoadMore,
|
|
hasMore: false,
|
|
data: { likes: mockLikes, total: 1, page: 1, limit: 20 },
|
|
} as any)
|
|
|
|
render(<LikesScreen />)
|
|
|
|
// Note: Testing RefreshControl onRefresh is difficult in React Native testing
|
|
// In a real scenario, this would be tested with integration tests
|
|
// The mock setup confirms refetch is available
|
|
expect(mockRefetch).toBeDefined()
|
|
})
|
|
})
|
|
|
|
describe('Load More', () => {
|
|
it('should call loadMore when scrolling to end', () => {
|
|
const mockLikes = [
|
|
createMockLikeItem({
|
|
templateId: 'template-1',
|
|
template: { id: 'template-1', title: 'Template 1', previewUrl: 'https://example.com/1.jpg', coverImageUrl: 'https://example.com/1.jpg' },
|
|
}),
|
|
createMockLikeItem({
|
|
templateId: 'template-2',
|
|
template: { id: 'template-2', title: 'Template 2', previewUrl: 'https://example.com/2.jpg', coverImageUrl: 'https://example.com/2.jpg' },
|
|
}),
|
|
]
|
|
|
|
mockUseUserLikes.mockReturnValue({
|
|
likes: mockLikes,
|
|
loading: false,
|
|
loadingMore: false,
|
|
error: null,
|
|
execute: mockExecute,
|
|
refetch: mockRefetch,
|
|
loadMore: mockLoadMore,
|
|
hasMore: true,
|
|
data: { likes: mockLikes, total: 10, page: 1, limit: 2 },
|
|
} as any)
|
|
|
|
const { getByText } = render(<LikesScreen />)
|
|
|
|
expect(getByText('Template 1')).toBeTruthy()
|
|
expect(mockLoadMore).toBeDefined()
|
|
})
|
|
|
|
it('should not call loadMore when already loading more', () => {
|
|
const mockLikes = [createMockLikeItem()]
|
|
|
|
mockUseUserLikes.mockReturnValue({
|
|
likes: mockLikes,
|
|
loading: false,
|
|
loadingMore: true,
|
|
error: null,
|
|
execute: mockExecute,
|
|
refetch: mockRefetch,
|
|
loadMore: mockLoadMore,
|
|
hasMore: true,
|
|
data: { likes: mockLikes, total: 10, page: 1, limit: 5 },
|
|
} as any)
|
|
|
|
render(<LikesScreen />)
|
|
|
|
// When loadingMore is true, loadMore should not be called again
|
|
expect(mockLoadMore).toBeDefined()
|
|
})
|
|
|
|
it('should not call loadMore when no more data', () => {
|
|
const mockLikes = [createMockLikeItem()]
|
|
|
|
mockUseUserLikes.mockReturnValue({
|
|
likes: mockLikes,
|
|
loading: false,
|
|
loadingMore: false,
|
|
error: null,
|
|
execute: mockExecute,
|
|
refetch: mockRefetch,
|
|
loadMore: mockLoadMore,
|
|
hasMore: false,
|
|
data: { likes: mockLikes, total: 1, page: 1, limit: 20 },
|
|
} as any)
|
|
|
|
render(<LikesScreen />)
|
|
|
|
// When hasMore is false, loadMore should not be called
|
|
expect(mockLoadMore).toBeDefined()
|
|
})
|
|
})
|
|
|
|
describe('Data Fetching', () => {
|
|
it('should execute fetch on mount', () => {
|
|
mockUseUserLikes.mockReturnValue({
|
|
likes: [],
|
|
loading: false,
|
|
loadingMore: false,
|
|
error: null,
|
|
execute: mockExecute,
|
|
refetch: mockRefetch,
|
|
loadMore: mockLoadMore,
|
|
hasMore: false,
|
|
data: undefined,
|
|
} as any)
|
|
|
|
render(<LikesScreen />)
|
|
|
|
expect(mockExecute).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('Layout', () => {
|
|
it('should use 2 column grid layout', () => {
|
|
const mockLikes = [
|
|
createMockLikeItem({
|
|
templateId: 'template-1',
|
|
template: { id: 'template-1', title: 'Template 1', previewUrl: 'https://example.com/1.jpg', coverImageUrl: 'https://example.com/1.jpg' },
|
|
}),
|
|
]
|
|
|
|
mockUseUserLikes.mockReturnValue({
|
|
likes: mockLikes,
|
|
loading: false,
|
|
loadingMore: false,
|
|
error: null,
|
|
execute: mockExecute,
|
|
refetch: mockRefetch,
|
|
loadMore: mockLoadMore,
|
|
hasMore: false,
|
|
data: { likes: mockLikes, total: 1, page: 1, limit: 20 },
|
|
} as any)
|
|
|
|
const { getByText } = render(<LikesScreen />)
|
|
|
|
expect(getByText('Template 1')).toBeTruthy()
|
|
})
|
|
})
|
|
})
|