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() }) }) }) })