import React from 'react' import { render, fireEvent } from '@testing-library/react-native' import { MessageCard, getMessageIcon, getMessageConfig, formatRelativeTime } from './MessageCard' // Mock react-i18next jest.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, i18n: { language: 'zh-CN' }, }), })) describe('MessageCard Utilities', () => { describe('getMessageIcon', () => { it('should return celebration icon for TEMPLATE_GENERATION_SUCCESS', () => { expect(getMessageIcon('TEMPLATE_GENERATION_SUCCESS')).toBe('πŸŽ‰') }) it('should return error icon for TEMPLATE_GENERATION_FAILED', () => { expect(getMessageIcon('TEMPLATE_GENERATION_FAILED')).toBe('❌') }) it('should return thumbs up icon for TEMPLATE_LIKED', () => { expect(getMessageIcon('TEMPLATE_LIKED')).toBe('πŸ‘') }) it('should return star icon for TEMPLATE_FAVORITED', () => { expect(getMessageIcon('TEMPLATE_FAVORITED')).toBe('⭐') }) it('should return comment icon for TEMPLATE_COMMENTED', () => { expect(getMessageIcon('TEMPLATE_COMMENTED')).toBe('πŸ’¬') }) it('should return comment icon for COMMENT_REPLIED', () => { expect(getMessageIcon('COMMENT_REPLIED')).toBe('πŸ’¬') }) it('should return money icon for CREDITS_DEDUCTED', () => { expect(getMessageIcon('CREDITS_DEDUCTED')).toBe('πŸ’°') }) it('should return money icon for CREDITS_REFUNDED', () => { expect(getMessageIcon('CREDITS_REFUNDED')).toBe('πŸ’°') }) it('should return money icon for CREDITS_RECHARGED', () => { expect(getMessageIcon('CREDITS_RECHARGED')).toBe('πŸ’°') }) it('should return announcement icon for ANNOUNCEMENT', () => { expect(getMessageIcon('ANNOUNCEMENT')).toBe('πŸ“’') }) it('should return default icon for unknown subType', () => { expect(getMessageIcon('UNKNOWN_TYPE')).toBe('πŸ“©') }) it('should return default icon for undefined subType', () => { expect(getMessageIcon(undefined)).toBe('πŸ“©') }) }) describe('getMessageConfig', () => { it('should return showPreview true for TEMPLATE_GENERATION_SUCCESS', () => { const config = getMessageConfig('TEMPLATE_GENERATION_SUCCESS') expect(config.showPreview).toBe(true) expect(config.showAvatar).toBe(false) }) it('should return showAvatar true for TEMPLATE_LIKED', () => { const config = getMessageConfig('TEMPLATE_LIKED') expect(config.showAvatar).toBe(true) expect(config.showPreview).toBe(false) }) it('should return showAvatar true for TEMPLATE_FAVORITED', () => { const config = getMessageConfig('TEMPLATE_FAVORITED') expect(config.showAvatar).toBe(true) }) it('should return showAvatar true for TEMPLATE_COMMENTED', () => { const config = getMessageConfig('TEMPLATE_COMMENTED') expect(config.showAvatar).toBe(true) }) it('should return showQuote true for COMMENT_REPLIED', () => { const config = getMessageConfig('COMMENT_REPLIED') expect(config.showQuote).toBe(true) expect(config.showAvatar).toBe(true) }) it('should return default config for unknown subType', () => { const config = getMessageConfig('UNKNOWN_TYPE') expect(config.showPreview).toBe(false) expect(config.showAvatar).toBe(false) expect(config.showQuote).toBe(false) }) }) describe('formatRelativeTime', () => { beforeEach(() => { jest.useFakeTimers() jest.setSystemTime(new Date('2026-01-29T12:00:00Z')) }) afterEach(() => { jest.useRealTimers() }) it('should return "刚刚" for time within 1 minute', () => { const now = new Date() const thirtySecondsAgo = new Date(now.getTime() - 30 * 1000) expect(formatRelativeTime(thirtySecondsAgo)).toBe('刚刚') }) it('should return "Xεˆ†ι’Ÿε‰" for time within 1 hour', () => { const now = new Date() const tenMinutesAgo = new Date(now.getTime() - 10 * 60 * 1000) expect(formatRelativeTime(tenMinutesAgo)).toBe('10εˆ†ι’Ÿε‰') }) it('should return "X小既前" for time within 24 hours', () => { const now = new Date() const threeHoursAgo = new Date(now.getTime() - 3 * 60 * 60 * 1000) expect(formatRelativeTime(threeHoursAgo)).toBe('3小既前') }) it('should return "昨倩 HH:MM" for time within 48 hours', () => { const now = new Date() const yesterday = new Date(now.getTime() - 25 * 60 * 60 * 1000) const result = formatRelativeTime(yesterday) expect(result).toMatch(/^昨倩 \d{2}:\d{2}$/) }) it('should return formatted date for older times', () => { const now = new Date() const threeDaysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000) const result = formatRelativeTime(threeDaysAgo) expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/) }) it('should handle string date input', () => { const result = formatRelativeTime('2026-01-29T11:50:00Z') expect(result).toBe('10εˆ†ι’Ÿε‰') }) }) }) describe('MessageCard Component', () => { const mockMessage = { id: 'msg-123', type: 'ACTIVITY' as const, title: 'ζ”Άεˆ°ζ–°η‚Ήθ΅ž', content: '小明 θ΅žδΊ†δ½ ηš„δ½œε“γ€Œι£Žζ™―η…§γ€', data: { subType: 'TEMPLATE_LIKED', likerName: '小明', likerAvatar: 'https://example.com/avatar.jpg', templateId: 'tpl-456', templateTitle: 'ι£Žζ™―η…§', }, isRead: false, createdAt: new Date('2026-01-29T11:50:00Z'), } const mockOnPress = jest.fn() const mockOnDelete = jest.fn() beforeEach(() => { jest.clearAllMocks() jest.useFakeTimers() jest.setSystemTime(new Date('2026-01-29T12:00:00Z')) }) afterEach(() => { jest.useRealTimers() }) it('should render message title', () => { const { getByText } = render( ) expect(getByText('ζ”Άεˆ°ζ–°η‚Ήθ΅ž')).toBeTruthy() }) it('should render message content', () => { const { getByText } = render( ) expect(getByText('小明 θ΅žδΊ†δ½ ηš„δ½œε“γ€Œι£Žζ™―η…§γ€')).toBeTruthy() }) it('should render relative time', () => { const { getByText } = render( ) expect(getByText('10εˆ†ι’Ÿε‰')).toBeTruthy() }) it('should show unread indicator when isRead is false', () => { const { getByTestId } = render( ) expect(getByTestId('unread-indicator')).toBeTruthy() }) it('should not show unread indicator when isRead is true', () => { const readMessage = { ...mockMessage, isRead: true } const { queryByTestId } = render( ) expect(queryByTestId('unread-indicator')).toBeNull() }) it('should call onPress when card is pressed', () => { const { getByTestId } = render( ) fireEvent.press(getByTestId('message-card')) expect(mockOnPress).toHaveBeenCalledWith(mockMessage) }) it('should render message icon based on subType', () => { // Use a message without avatar to test icon display const billingMessage = { ...mockMessage, type: 'BILLING' as const, data: { subType: 'CREDITS_RECHARGED', credits: 1000 }, } const { getByText } = render( ) expect(getByText('πŸ’°')).toBeTruthy() }) describe('Avatar Display', () => { it('should show avatar for TEMPLATE_LIKED messages', () => { const { getByTestId } = render( ) expect(getByTestId('user-avatar')).toBeTruthy() }) it('should not show avatar for CREDITS_RECHARGED messages', () => { const billingMessage = { ...mockMessage, type: 'BILLING' as const, data: { subType: 'CREDITS_RECHARGED', credits: 1000 }, } const { queryByTestId } = render( ) expect(queryByTestId('user-avatar')).toBeNull() }) }) describe('Preview Image Display', () => { it('should show preview image for TEMPLATE_GENERATION_SUCCESS', () => { const successMessage = { ...mockMessage, type: 'SYSTEM' as const, data: { subType: 'TEMPLATE_GENERATION_SUCCESS', webpPreviewUrl: 'https://example.com/preview.webp', }, } const { getByTestId } = render( ) expect(getByTestId('preview-image')).toBeTruthy() }) it('should not show preview image when webpPreviewUrl is missing', () => { const successMessage = { ...mockMessage, type: 'SYSTEM' as const, data: { subType: 'TEMPLATE_GENERATION_SUCCESS' }, } const { queryByTestId } = render( ) expect(queryByTestId('preview-image')).toBeNull() }) }) describe('Quote Display', () => { it('should show quote for COMMENT_REPLIED messages', () => { const replyMessage = { ...mockMessage, data: { subType: 'COMMENT_REPLIED', originalComment: 'ε€ͺ棒了!', likerAvatar: 'https://example.com/avatar.jpg', }, } const { getByTestId, getByText } = render( ) expect(getByTestId('quote-container')).toBeTruthy() expect(getByText('ε€ͺ棒了!')).toBeTruthy() }) }) describe('Swipe to Delete', () => { it('should call onDelete when delete action is triggered', () => { const { getByTestId } = render( ) // Simulate swipe delete action const deleteButton = getByTestId('delete-button') fireEvent.press(deleteButton) expect(mockOnDelete).toHaveBeenCalledWith(mockMessage.id) }) it('should not render delete button when onDelete is not provided', () => { const { queryByTestId } = render( ) expect(queryByTestId('delete-button')).toBeNull() }) }) }) describe('MessageCard Types', () => { it('should accept Message type with all required fields', () => { const message = { id: 'msg-123', type: 'SYSTEM' as const, title: 'Test Title', content: 'Test Content', isRead: false, createdAt: new Date(), } expect(message.id).toBeDefined() expect(message.type).toBeDefined() expect(message.title).toBeDefined() expect(message.content).toBeDefined() expect(message.isRead).toBeDefined() expect(message.createdAt).toBeDefined() }) it('should accept Message type with optional data field', () => { const message = { id: 'msg-123', type: 'ACTIVITY' as const, title: 'Test Title', content: 'Test Content', data: { subType: 'TEMPLATE_LIKED', likerName: '小明', }, isRead: false, createdAt: new Date(), } expect(message.data).toBeDefined() expect(message.data?.subType).toBe('TEMPLATE_LIKED') }) it('should handle all message types', () => { const types = ['SYSTEM', 'ACTIVITY', 'BILLING', 'MARKETING'] as const types.forEach((type) => { const message = { id: 'msg-123', type, title: 'Test', content: 'Test', isRead: false, createdAt: new Date(), } expect(message.type).toBe(type) }) }) })