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()
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()
// 模拟下拉刷新
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()
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()
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()
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()
await waitFor(() => {
expect(mockRefetch).toHaveBeenCalledWith({ page: 1, limit: 20 })
})
})
describe('Tab Navigation', () => {
it('should render tab navigation with 3 tabs', async () => {
const { getByTestId } = render()
await waitFor(() => {
const tabNavigation = getByTestId('tab-navigation')
expect(tabNavigation).toBeTruthy()
})
})
it('should display works tab as active by default', async () => {
const { getByTestId, getByText } = render()
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()
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()
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()
// 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()
// Switch to likes tab
const likesTab = getByTestId('tab-2')
fireEvent.press(likesTab)
await waitFor(() => {
expect(getByText('my.noLikes')).toBeTruthy()
})
})
})
})