import React from 'react' import { render, waitFor } from '@testing-library/react-native' import HomeScreen from '../index' // 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-linear-gradient jest.mock('expo-linear-gradient', () => ({ LinearGradient: 'LinearGradient', })) // Mock expo-image jest.mock('expo-image', () => ({ Image: 'Image', })) // Mock expo-status-bar jest.mock('expo-status-bar', () => ({ StatusBar: 'StatusBar', })) // Mock icons jest.mock('@/components/icon', () => ({ DownArrowIcon: () => 'DownArrowIcon', PointsIcon: () => 'PointsIcon', SearchIcon: () => 'SearchIcon', })) // Mock components jest.mock('@/components/ErrorState', () => 'ErrorState') jest.mock('@/components/LoadingState', () => 'LoadingState') // Mock home blocks jest.mock('@/components/blocks/home', () => ({ TitleBar: 'TitleBar', HeroSlider: 'HeroSlider', TabNavigation: 'TabNavigation', TemplateCard: 'TemplateCard', })) // Mock react-native RefreshControl (directly from react-native) jest.mock('react-native', () => Object.assign({}, jest.requireActual('react-native'), { RefreshControl: 'RefreshControl', }) ) // Mock dependencies jest.mock('expo-router', () => ({ useLocalSearchParams: jest.fn(() => ({})), useRouter: jest.fn(() => ({ push: jest.fn(), })), })) jest.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, }), })) jest.mock('@/hooks/use-activates', () => ({ useActivates: jest.fn(() => ({ load: jest.fn(), data: { activities: [ { id: '1', title: 'Test Activity', desc: 'Test Description', coverUrl: 'https://example.com/image.jpg', link: '/test', }, ], }, error: null, })), })) jest.mock('@/hooks/use-categories', () => ({ useCategories: jest.fn(() => ({ load: jest.fn(), data: { categories: [ { id: 'cat1', name: 'Test Category', templates: [ { id: 'template1', title: 'Test Template', webpPreviewUrl: 'https://example.com/preview.webp', previewUrl: 'https://example.com/preview.jpg', coverImageUrl: 'https://example.com/cover.png', aspectRatio: '1:1', }, ], }, ], }, loading: false, error: null, })), })) jest.mock('@/hooks/use-user-balance', () => ({ useUserBalance: jest.fn(() => ({ balance: 1234, loading: false, error: null, })), })) jest.mock('@/hooks/use-categories-with-tags', () => ({ useCategoriesWithTags: jest.fn(() => ({ load: jest.fn(), data: { categories: [ { id: 'cat1', name: 'Test Category', }, ], }, loading: false, error: null, })), })) jest.mock('@/hooks/use-category-templates', () => ({ useCategoryTemplates: jest.fn(() => ({ templates: [ { id: 'template1', title: 'Test Template', webpPreviewUrl: 'https://example.com/preview.webp', previewUrl: 'https://example.com/preview.jpg', coverImageUrl: 'https://example.com/cover.png', aspectRatio: '1:1', }, ], loading: false, loadingMore: false, execute: jest.fn(), loadMore: jest.fn(), hasMore: false, })), })) jest.mock('@/hooks/use-sticky-tabs', () => ({ useStickyTabs: jest.fn(() => ({ isSticky: false, tabsHeight: 0, titleBarHeightRef: { current: 0 }, handleScroll: jest.fn(), handleTabsLayout: jest.fn(), handleTitleBarLayout: jest.fn(), })), })) jest.mock('@/hooks/use-tab-navigation', () => ({ useTabNavigation: jest.fn(() => ({ activeIndex: 0, selectedCategoryId: 'cat1', tabs: [{ id: 'cat1', name: 'Test Category' }], selectTab: jest.fn(), selectCategoryById: jest.fn(), })), })) jest.mock('@/hooks/use-template-filter', () => ({ useTemplateFilter: jest.fn(({ templates }) => ({ filteredTemplates: templates, })), })) const renderWithProviders = (component: React.ReactElement) => { return render(component) } describe('HomeScreen', () => { it('should render title bar with app name', async () => { const { getByText } = renderWithProviders() await waitFor(() => { expect(getByText('Popcore')).toBeTruthy() }) }) it('should render category tabs when data is loaded', async () => { const { getByText } = renderWithProviders() await waitFor(() => { expect(getByText('Test Category')).toBeTruthy() }) }) it('should render template cards when category has templates', async () => { const { getByText } = renderWithProviders() await waitFor(() => { expect(getByText('Test Template')).toBeTruthy() }) }) it('should not show loading state when data is loaded', async () => { const { queryByText } = renderWithProviders() await waitFor(() => { expect(queryByText('加载中')).toBeNull() }) }) it('should display user balance from useUserBalance hook', async () => { const { getByText } = renderWithProviders() await waitFor(() => { expect(getByText('1234')).toBeTruthy() }) }) describe('Loading States', () => { it('should show only one loading indicator when categories are loading', async () => { const { useCategoriesWithTags } = require('@/hooks/use-categories-with-tags') const { useCategoryTemplates } = require('@/hooks/use-category-templates') useCategoriesWithTags.mockReturnValue({ load: jest.fn(), data: null, loading: true, error: null, }) useCategoryTemplates.mockReturnValue({ templates: [], loading: false, loadingMore: false, execute: jest.fn(), loadMore: jest.fn(), hasMore: false, }) const { UNSAFE_getAllByType } = renderWithProviders() await waitFor(() => { // 应该只有一个 LoadingState 组件 const loadingStates = UNSAFE_getAllByType('LoadingState' as any) expect(loadingStates.length).toBe(1) }) }) it('should show only one loading indicator when templates are loading', async () => { const { useCategoriesWithTags } = require('@/hooks/use-categories-with-tags') const { useCategoryTemplates } = require('@/hooks/use-category-templates') useCategoriesWithTags.mockReturnValue({ load: jest.fn(), data: { categories: [{ id: 'cat1', name: 'Test Category' }], }, loading: false, error: null, }) useCategoryTemplates.mockReturnValue({ templates: [], loading: true, loadingMore: false, execute: jest.fn(), loadMore: jest.fn(), hasMore: false, }) const { UNSAFE_queryAllByType } = renderWithProviders() await waitFor(() => { // 应该只有一个 ActivityIndicator const activityIndicators = UNSAFE_queryAllByType('ActivityIndicator' as any) expect(activityIndicators.length).toBeLessThanOrEqual(1) }) }) it('should NOT show multiple loading indicators simultaneously', async () => { const { useCategoriesWithTags } = require('@/hooks/use-categories-with-tags') const { useCategoryTemplates } = require('@/hooks/use-category-templates') // 模拟两个都在加载的情况 useCategoriesWithTags.mockReturnValue({ load: jest.fn(), data: null, loading: true, error: null, }) useCategoryTemplates.mockReturnValue({ templates: [], loading: true, loadingMore: false, execute: jest.fn(), loadMore: jest.fn(), hasMore: false, }) const { UNSAFE_queryAllByType } = renderWithProviders() await waitFor(() => { // 即使两个都在加载,也应该只显示一个 loading const loadingStates = UNSAFE_queryAllByType('LoadingState' as any) const activityIndicators = UNSAFE_queryAllByType('ActivityIndicator' as any) // 总共的 loading 指示器不应该超过 1 个 const totalLoadingIndicators = loadingStates.length + activityIndicators.length expect(totalLoadingIndicators).toBeLessThanOrEqual(1) }) }) it('should show unified loading state during initial load', async () => { const { useCategoriesWithTags } = require('@/hooks/use-categories-with-tags') const { useCategoryTemplates } = require('@/hooks/use-category-templates') useCategoriesWithTags.mockReturnValue({ load: jest.fn(), data: null, loading: true, error: null, }) useCategoryTemplates.mockReturnValue({ templates: [], loading: true, loadingMore: false, execute: jest.fn(), loadMore: jest.fn(), hasMore: false, }) const { UNSAFE_queryAllByType } = renderWithProviders() await waitFor(() => { // 应该只显示一个统一的 loading 状态 const allLoadingComponents = [ ...UNSAFE_queryAllByType('LoadingState' as any), ...UNSAFE_queryAllByType('ActivityIndicator' as any), ] expect(allLoadingComponents.length).toBe(1) }) }) }) })