fix: bug
This commit is contained in:
@@ -35,6 +35,14 @@ jest.mock('@/components/icon', () => ({
|
||||
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'), {
|
||||
@@ -108,6 +116,69 @@ jest.mock('@/hooks/use-user-balance', () => ({
|
||||
})),
|
||||
}))
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -152,4 +223,132 @@ describe('HomeScreen', () => {
|
||||
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(<HomeScreen />)
|
||||
|
||||
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(<HomeScreen />)
|
||||
|
||||
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(<HomeScreen />)
|
||||
|
||||
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(<HomeScreen />)
|
||||
|
||||
await waitFor(() => {
|
||||
// 应该只显示一个统一的 loading 状态
|
||||
const allLoadingComponents = [
|
||||
...UNSAFE_queryAllByType('LoadingState' as any),
|
||||
...UNSAFE_queryAllByType('ActivityIndicator' as any),
|
||||
]
|
||||
expect(allLoadingComponents.length).toBe(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
import React from 'react'
|
||||
import { render, waitFor } from '@testing-library/react-native'
|
||||
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(),
|
||||
@@ -36,6 +53,40 @@ jest.mock('@/hooks/use-user-balance', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
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()
|
||||
|
||||
@@ -65,21 +116,66 @@ describe('My Page - Pagination', () => {
|
||||
|
||||
it('should refresh with limit 20 on pull to refresh', async () => {
|
||||
const { useTemplateGenerations } = require('@/hooks')
|
||||
const mockOnRefresh = jest.fn()
|
||||
const mockRefetchAsync = jest.fn().mockResolvedValue({ data: [], error: null })
|
||||
|
||||
useTemplateGenerations.mockReturnValue({
|
||||
generations: [{ id: '1', status: 'completed' }],
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
refetch: mockRefetch,
|
||||
refetch: mockRefetchAsync,
|
||||
loadMore: mockLoadMore,
|
||||
hasMore: true,
|
||||
})
|
||||
|
||||
render(<My />)
|
||||
const { getByTestId } = render(<My />)
|
||||
|
||||
// 模拟下拉刷新
|
||||
const scrollView = getByTestId('my-scroll-view')
|
||||
const refreshControl = scrollView.props.refreshControl
|
||||
|
||||
// 触发刷新
|
||||
await refreshControl.props.onRefresh()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRefetch).toHaveBeenCalledWith({ page: 1, limit: 20 })
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -99,10 +195,22 @@ describe('My Page - Pagination', () => {
|
||||
hasMore: true,
|
||||
})
|
||||
|
||||
render(<My />)
|
||||
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(mockRefetch).toHaveBeenCalledWith({ page: 1, limit: 20 })
|
||||
expect(mockLoadMore).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user