This commit is contained in:
imeepos
2026-01-29 16:00:21 +08:00
parent 7e6de68891
commit 7c3b3c066c
5 changed files with 183 additions and 24 deletions

View File

@@ -23,6 +23,10 @@ jest.mock('react-i18next', () => ({
// Mock expo-router
const mockPush = jest.fn()
const mockUseFocusEffect = jest.fn((callback: () => void) => {
// 立即执行 callback 来模拟页面获得焦点
callback()
})
jest.mock('expo-router', () => ({
useRouter: () => ({
push: mockPush,
@@ -30,6 +34,7 @@ jest.mock('expo-router', () => ({
back: jest.fn(),
}),
useLocalSearchParams: () => ({}),
useFocusEffect: (callback: () => void) => mockUseFocusEffect(callback),
}))
// Mock hooks
@@ -87,6 +92,7 @@ jest.mock('@/hooks/use-messages', () => ({
messages: mockMessages,
loading: false,
loadingMore: false,
refreshing: false,
error: null,
execute: mockExecute,
refetch: mockRefetch,
@@ -524,9 +530,10 @@ describe('MessageScreen', () => {
})
describe('Initial Data Fetch', () => {
it('should call execute on mount', () => {
it('should call refetch on mount via useFocusEffect', () => {
render(<MessageScreen />)
expect(mockExecute).toHaveBeenCalled()
// 现在使用 useFocusEffect + refetch 替代 useEffect + execute
expect(mockRefetch).toHaveBeenCalled()
})
it('should call announcement execute on mount', () => {
@@ -534,4 +541,144 @@ describe('MessageScreen', () => {
expect(mockAnnouncementExecute).toHaveBeenCalled()
})
})
describe('Pull-to-Refresh', () => {
beforeEach(() => {
jest.clearAllMocks()
const { useMessages } = require('@/hooks/use-messages')
useMessages.mockReturnValue({
messages: mockMessages,
loading: false,
loadingMore: false,
refreshing: false,
error: null,
execute: mockExecute,
refetch: mockRefetch,
loadMore: mockLoadMore,
hasMore: true,
})
const { useAnnouncements } = require('@/hooks/use-announcements')
useAnnouncements.mockReturnValue({
announcements: mockAnnouncements,
loading: false,
error: null,
execute: mockAnnouncementExecute,
refetch: jest.fn(),
loadMore: jest.fn(),
hasMore: false,
})
})
it('should render FlatList with RefreshControl', () => {
const { getByTestId } = render(<MessageScreen />)
const flatList = getByTestId('message-list')
expect(flatList.props.refreshControl).toBeTruthy()
})
it('should call refetch when pull-to-refresh is triggered', async () => {
const { getByTestId } = render(<MessageScreen />)
const flatList = getByTestId('message-list')
// 触发 onRefresh
const refreshControl = flatList.props.refreshControl
refreshControl.props.onRefresh()
await waitFor(() => {
expect(mockRefetch).toHaveBeenCalled()
})
})
it('should call executeAnnouncements when pull-to-refresh is triggered', async () => {
const { getByTestId } = render(<MessageScreen />)
const flatList = getByTestId('message-list')
// 触发 onRefresh
const refreshControl = flatList.props.refreshControl
refreshControl.props.onRefresh()
await waitFor(() => {
expect(mockAnnouncementExecute).toHaveBeenCalled()
})
})
it('should show refreshing indicator when refreshing is true', () => {
const { useMessages } = require('@/hooks/use-messages')
useMessages.mockReturnValue({
messages: mockMessages,
loading: false,
loadingMore: false,
refreshing: true,
error: null,
execute: mockExecute,
refetch: mockRefetch,
loadMore: mockLoadMore,
hasMore: true,
})
const { getByTestId } = render(<MessageScreen />)
const flatList = getByTestId('message-list')
const refreshControl = flatList.props.refreshControl
expect(refreshControl.props.refreshing).toBe(true)
})
it('should not show refreshing indicator when refreshing is false', () => {
const { getByTestId } = render(<MessageScreen />)
const flatList = getByTestId('message-list')
const refreshControl = flatList.props.refreshControl
expect(refreshControl.props.refreshing).toBe(false)
})
})
describe('Tab Focus Reload', () => {
beforeEach(() => {
jest.clearAllMocks()
const { useMessages } = require('@/hooks/use-messages')
useMessages.mockReturnValue({
messages: mockMessages,
loading: false,
loadingMore: false,
refreshing: false,
error: null,
execute: mockExecute,
refetch: mockRefetch,
loadMore: mockLoadMore,
hasMore: true,
})
const { useAnnouncements } = require('@/hooks/use-announcements')
useAnnouncements.mockReturnValue({
announcements: mockAnnouncements,
loading: false,
error: null,
execute: mockAnnouncementExecute,
refetch: jest.fn(),
loadMore: jest.fn(),
hasMore: false,
})
})
it('should call refetch when tab gains focus', () => {
render(<MessageScreen />)
// useFocusEffect 在 mock 中会立即执行 callback
// 所以 refetch 应该被调用
expect(mockRefetch).toHaveBeenCalled()
})
it('should call executeAnnouncements when tab gains focus', () => {
render(<MessageScreen />)
expect(mockAnnouncementExecute).toHaveBeenCalled()
})
it('should use useFocusEffect hook', () => {
render(<MessageScreen />)
// 验证 useFocusEffect 被调用
expect(mockUseFocusEffect).toHaveBeenCalled()
})
})
})

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react'
import React, { useState, useCallback } from 'react'
import {
View,
Text,
@@ -6,11 +6,12 @@ import {
FlatList,
StatusBar as RNStatusBar,
Pressable,
RefreshControl,
} from 'react-native'
import { StatusBar } from 'expo-status-bar'
import { SafeAreaView } from 'react-native-safe-area-context'
import { useTranslation } from 'react-i18next'
import { useRouter } from 'expo-router'
import { useRouter, useFocusEffect } from 'expo-router'
import { useMessages, type Message } from '@/hooks/use-messages'
import { useMessageActions } from '@/hooks/use-message-actions'
@@ -54,11 +55,11 @@ export default function MessageScreen() {
messages: allMessages,
loading,
loadingMore,
refreshing,
error,
refetch,
loadMore,
hasMore,
execute,
} = useMessages({ limit: 20 })
const {
@@ -75,11 +76,19 @@ export default function MessageScreen() {
// Filter messages based on active tab
const filteredMessages = filterMessagesByTab(allMessages, activeTab)
// Initial data fetch
useEffect(() => {
execute()
// Reload data when tab gains focus
useFocusEffect(
useCallback(() => {
refetch()
executeAnnouncements()
}, [refetch, executeAnnouncements])
)
// Handle pull-to-refresh
const handleRefresh = useCallback(() => {
refetch()
executeAnnouncements()
}, [])
}, [refetch, executeAnnouncements])
// Handle tab change
const handleTabChange = useCallback((type: MessageType) => {
@@ -242,6 +251,14 @@ export default function MessageScreen() {
onEndReachedThreshold={0.5}
ListFooterComponent={renderFooter}
ListEmptyComponent={renderEmptyComponent}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={handleRefresh}
tintColor="#FFFFFF"
colors={['#FFFFFF']}
/>
}
/>
</SafeAreaView>
)

View File

@@ -141,19 +141,6 @@ export const MessageCard: React.FC<MessageCardProps> = ({
)}
<View style={styles.contentWrapper}>
{/* Left: Icon or Avatar */}
<View style={styles.leftSection}>
{showAvatar ? (
<Image
testID="user-avatar"
source={{ uri: message.data?.likerAvatar }}
style={styles.avatar}
/>
) : (
<Text style={styles.icon}>{icon}</Text>
)}
</View>
{/* Middle: Content */}
<View style={styles.middleSection}>
<Text style={styles.title}>{message.title}</Text>

View File

@@ -66,6 +66,9 @@ const styles = StyleSheet.create({
width: 80,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 12,
borderTopRightRadius: 16,
borderBottomRightRadius: 16,
},
deleteText: {
color: '#FFFFFF',

View File

@@ -22,6 +22,7 @@ const DEFAULT_PARAMS = {
export const useMessages = (initialParams?: ListMessagesParams) => {
const [loading, setLoading] = useState(false)
const [loadingMore, setLoadingMore] = useState(false)
const [refreshing, setRefreshing] = useState(false)
const [error, setError] = useState<ApiError | null>(null)
const [data, setData] = useState<ListMessagesResult>()
const currentPageRef = useRef(1)
@@ -93,9 +94,12 @@ export const useMessages = (initialParams?: ListMessagesParams) => {
return { data: newData, error: null }
}, [loading, loadingMore, initialParams])
const refetch = useCallback(() => {
const refetch = useCallback(async () => {
hasMoreRef.current = true
return execute()
setRefreshing(true)
const result = await execute()
setRefreshing(false)
return result
}, [execute])
return {
@@ -103,6 +107,7 @@ export const useMessages = (initialParams?: ListMessagesParams) => {
messages: data?.messages || [],
loading,
loadingMore,
refreshing,
error,
execute,
refetch,