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