358 lines
8.9 KiB
TypeScript
358 lines
8.9 KiB
TypeScript
import React from 'react'
|
|
import { render, waitFor, fireEvent } from '@testing-library/react-native'
|
|
import My from '../my'
|
|
|
|
// Mock react-native-safe-area-context FIRST
|
|
jest.mock('react-native-safe-area-context', () => ({
|
|
SafeAreaProvider: ({ children }: any) => children,
|
|
SafeAreaView: ({ children }: any) => children,
|
|
useSafeAreaInsets: () => ({ top: 0, right: 0, bottom: 0, left: 0 }),
|
|
}))
|
|
|
|
// Mock expo-status-bar
|
|
jest.mock('expo-status-bar', () => ({
|
|
StatusBar: 'StatusBar',
|
|
}))
|
|
|
|
// Mock expo-image
|
|
jest.mock('expo-image', () => ({
|
|
Image: 'Image',
|
|
}))
|
|
|
|
jest.mock('expo-router', () => ({
|
|
useRouter: () => ({
|
|
push: jest.fn(),
|
|
replace: jest.fn(),
|
|
}),
|
|
}))
|
|
|
|
jest.mock('react-i18next', () => ({
|
|
useTranslation: () => ({
|
|
t: (key: string) => key,
|
|
i18n: { language: 'zh-CN', changeLanguage: jest.fn() },
|
|
}),
|
|
}))
|
|
|
|
jest.mock('@/lib/auth', () => ({
|
|
signOut: jest.fn(),
|
|
useSession: () => ({
|
|
data: {
|
|
user: {
|
|
id: 'test-user-id',
|
|
name: 'Test User',
|
|
username: 'testuser',
|
|
image: null,
|
|
},
|
|
},
|
|
}),
|
|
}))
|
|
|
|
jest.mock('@/hooks/use-user-balance', () => ({
|
|
useUserBalance: () => ({
|
|
balance: 100,
|
|
}),
|
|
}))
|
|
|
|
jest.mock('@/components/skeleton/MySkeleton', () => ({
|
|
MySkeleton: () => 'MySkeleton',
|
|
}))
|
|
|
|
jest.mock('@/components/icon', () => ({
|
|
PointsIcon: () => 'PointsIcon',
|
|
SearchIcon: () => 'SearchIcon',
|
|
SettingsIcon: () => 'SettingsIcon',
|
|
}))
|
|
|
|
jest.mock('@/components/drawer/EditProfileDrawer', () => ({
|
|
__esModule: true,
|
|
default: () => 'EditProfileDrawer',
|
|
}))
|
|
|
|
jest.mock('@/components/ui/dropdown', () => ({
|
|
__esModule: true,
|
|
default: () => 'Dropdown',
|
|
}))
|
|
|
|
jest.mock('@/components/ui/Toast', () => ({
|
|
__esModule: true,
|
|
default: {
|
|
show: jest.fn(),
|
|
showLoading: jest.fn(),
|
|
hideLoading: jest.fn(),
|
|
showActionSheet: jest.fn(),
|
|
},
|
|
}))
|
|
|
|
jest.mock('expo-image', () => ({
|
|
Image: 'Image',
|
|
}))
|
|
|
|
const mockRefetch = jest.fn()
|
|
const mockLoadMore = jest.fn()
|
|
const mockFavoritesRefetch = jest.fn()
|
|
const mockFavoritesLoadMore = jest.fn()
|
|
const mockLikesRefetch = jest.fn()
|
|
const mockLikesLoadMore = jest.fn()
|
|
|
|
jest.mock('@/hooks', () => ({
|
|
useTemplateGenerations: jest.fn(() => ({
|
|
generations: [],
|
|
loading: false,
|
|
loadingMore: false,
|
|
refetch: mockRefetch,
|
|
loadMore: mockLoadMore,
|
|
hasMore: true,
|
|
})),
|
|
useUserFavorites: jest.fn(() => ({
|
|
favorites: [],
|
|
loading: false,
|
|
loadingMore: false,
|
|
refetch: mockFavoritesRefetch,
|
|
loadMore: mockFavoritesLoadMore,
|
|
hasMore: false,
|
|
})),
|
|
useUserLikes: jest.fn(() => ({
|
|
likes: [],
|
|
loading: false,
|
|
loadingMore: false,
|
|
refetch: mockLikesRefetch,
|
|
loadMore: mockLikesLoadMore,
|
|
hasMore: false,
|
|
})),
|
|
}))
|
|
|
|
describe('My Page - Pagination', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
})
|
|
|
|
it('should load first page with limit 20 on mount', async () => {
|
|
render(<My />)
|
|
|
|
await waitFor(() => {
|
|
expect(mockRefetch).toHaveBeenCalledWith({ page: 1, limit: 20 })
|
|
})
|
|
})
|
|
|
|
it('should refresh with limit 20 on pull to refresh', async () => {
|
|
const { useTemplateGenerations } = require('@/hooks')
|
|
const mockRefetchAsync = jest.fn().mockResolvedValue({ data: [], error: null })
|
|
|
|
useTemplateGenerations.mockReturnValue({
|
|
generations: [{ id: '1', status: 'completed' }],
|
|
loading: false,
|
|
loadingMore: false,
|
|
refetch: mockRefetchAsync,
|
|
loadMore: mockLoadMore,
|
|
hasMore: true,
|
|
})
|
|
|
|
const { getByTestId } = render(<My />)
|
|
|
|
// 模拟下拉刷新
|
|
const scrollView = getByTestId('my-scroll-view')
|
|
const refreshControl = scrollView.props.refreshControl
|
|
|
|
// 触发刷新
|
|
await refreshControl.props.onRefresh()
|
|
|
|
await waitFor(() => {
|
|
expect(mockRefetchAsync).toHaveBeenCalledWith({ page: 1, limit: 20 })
|
|
})
|
|
})
|
|
|
|
it('should stop showing refreshing indicator after refresh completes', async () => {
|
|
const { useTemplateGenerations } = require('@/hooks')
|
|
let resolveRefetch: any
|
|
const mockRefetchAsync = jest.fn(() => new Promise((resolve) => {
|
|
resolveRefetch = resolve
|
|
}))
|
|
|
|
useTemplateGenerations.mockReturnValue({
|
|
generations: [{ id: '1', status: 'completed' }],
|
|
loading: false,
|
|
loadingMore: false,
|
|
refetch: mockRefetchAsync,
|
|
loadMore: mockLoadMore,
|
|
hasMore: true,
|
|
})
|
|
|
|
const { getByTestId } = render(<My />)
|
|
const scrollView = getByTestId('my-scroll-view')
|
|
const refreshControl = scrollView.props.refreshControl
|
|
|
|
// 开始刷新
|
|
const refreshPromise = refreshControl.props.onRefresh()
|
|
|
|
// 刷新中应该显示 refreshing
|
|
await waitFor(() => {
|
|
expect(refreshControl.props.refreshing).toBe(true)
|
|
})
|
|
|
|
// 完成刷新
|
|
resolveRefetch({ data: [], error: null })
|
|
await refreshPromise
|
|
|
|
// 刷新完成后应该隐藏 refreshing
|
|
await waitFor(() => {
|
|
expect(refreshControl.props.refreshing).toBe(false)
|
|
})
|
|
})
|
|
|
|
it('should call loadMore when scrolling near bottom', async () => {
|
|
const { useTemplateGenerations } = require('@/hooks')
|
|
|
|
useTemplateGenerations.mockReturnValue({
|
|
generations: Array(20).fill(null).map((_, i) => ({
|
|
id: `${i}`,
|
|
status: 'completed',
|
|
resultUrl: ['url'],
|
|
})),
|
|
loading: false,
|
|
loadingMore: false,
|
|
refetch: mockRefetch,
|
|
loadMore: mockLoadMore,
|
|
hasMore: true,
|
|
})
|
|
|
|
const { getByTestId } = render(<My />)
|
|
const scrollView = getByTestId('my-scroll-view')
|
|
|
|
// 模拟滚动到底部
|
|
const scrollEvent = {
|
|
nativeEvent: {
|
|
layoutMeasurement: { height: 800 },
|
|
contentOffset: { y: 1000 },
|
|
contentSize: { height: 1800 }, // 距离底部 < 100px
|
|
},
|
|
}
|
|
|
|
fireEvent.scroll(scrollView, scrollEvent)
|
|
|
|
await waitFor(() => {
|
|
expect(mockLoadMore).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should not load more when loadingMore is true', async () => {
|
|
const { useTemplateGenerations } = require('@/hooks')
|
|
|
|
useTemplateGenerations.mockReturnValue({
|
|
generations: Array(20).fill(null).map((_, i) => ({
|
|
id: `${i}`,
|
|
status: 'completed'
|
|
})),
|
|
loading: false,
|
|
loadingMore: true,
|
|
refetch: mockRefetch,
|
|
loadMore: mockLoadMore,
|
|
hasMore: true,
|
|
})
|
|
|
|
render(<My />)
|
|
|
|
await waitFor(() => {
|
|
expect(mockRefetch).toHaveBeenCalledWith({ page: 1, limit: 20 })
|
|
})
|
|
})
|
|
|
|
it('should not load more when hasMore is false', async () => {
|
|
const { useTemplateGenerations } = require('@/hooks')
|
|
|
|
useTemplateGenerations.mockReturnValue({
|
|
generations: Array(10).fill(null).map((_, i) => ({
|
|
id: `${i}`,
|
|
status: 'completed'
|
|
})),
|
|
loading: false,
|
|
loadingMore: false,
|
|
refetch: mockRefetch,
|
|
loadMore: mockLoadMore,
|
|
hasMore: false,
|
|
})
|
|
|
|
render(<My />)
|
|
|
|
await waitFor(() => {
|
|
expect(mockRefetch).toHaveBeenCalledWith({ page: 1, limit: 20 })
|
|
})
|
|
})
|
|
|
|
describe('Tab Navigation', () => {
|
|
it('should render tab navigation with 3 tabs', async () => {
|
|
const { getByTestId } = render(<My />)
|
|
|
|
await waitFor(() => {
|
|
const tabNavigation = getByTestId('tab-navigation')
|
|
expect(tabNavigation).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
it('should display works tab as active by default', async () => {
|
|
const { getByTestId, getByText } = render(<My />)
|
|
|
|
await waitFor(() => {
|
|
expect(getByTestId('tab-0')).toBeTruthy()
|
|
})
|
|
|
|
const activeTab = getByTestId('tab-0')
|
|
expect(activeTab).toBeTruthy()
|
|
})
|
|
|
|
it('should switch to favorites tab when pressed', async () => {
|
|
const { getByTestId, getByText } = render(<My />)
|
|
|
|
await waitFor(() => {
|
|
expect(getByTestId('tab-1')).toBeTruthy()
|
|
})
|
|
|
|
const favoritesTab = getByTestId('tab-1')
|
|
fireEvent.press(favoritesTab)
|
|
|
|
// After pressing, the favorites tab should become active
|
|
await waitFor(() => {
|
|
expect(favoritesTab).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
it('should switch to likes tab when pressed', async () => {
|
|
const { getByTestId } = render(<My />)
|
|
|
|
await waitFor(() => {
|
|
expect(getByTestId('tab-2')).toBeTruthy()
|
|
})
|
|
|
|
const likesTab = getByTestId('tab-2')
|
|
fireEvent.press(likesTab)
|
|
|
|
await waitFor(() => {
|
|
expect(likesTab).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
it('should show empty state for favorites when no favorites', async () => {
|
|
const { getByTestId, getByText } = render(<My />)
|
|
|
|
// Switch to favorites tab
|
|
const favoritesTab = getByTestId('tab-1')
|
|
fireEvent.press(favoritesTab)
|
|
|
|
await waitFor(() => {
|
|
expect(getByText('my.noFavorites')).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
it('should show empty state for likes when no likes', async () => {
|
|
const { getByTestId, getByText } = render(<My />)
|
|
|
|
// Switch to likes tab
|
|
const likesTab = getByTestId('tab-2')
|
|
fireEvent.press(likesTab)
|
|
|
|
await waitFor(() => {
|
|
expect(getByText('my.noLikes')).toBeTruthy()
|
|
})
|
|
})
|
|
})
|
|
})
|