expo-popcore-app/components/ui/Text.test.tsx

299 lines
9.3 KiB
TypeScript

import React from 'react'
import { render, fireEvent } from '@testing-library/react-native'
import Text from './Text'
describe('Text Component', () => {
describe('Basic Rendering', () => {
it('should render basic text with default dark theme color', () => {
const { getByText } = render(<Text>Hello World</Text>)
const textElement = getByText('Hello World')
expect(textElement).toBeTruthy()
expect(textElement.props.style).toEqual(
expect.objectContaining({
color: '#F5F5F5',
})
)
})
it('should render children correctly', () => {
const { getByText } = render(<Text>Test Content</Text>)
expect(getByText('Test Content')).toBeTruthy()
})
it('should render nested text elements', () => {
const { getByText } = render(
<Text>
Parent <Text>Child</Text>
</Text>
)
// In React Native, nested Text components are flattened into a single text node
expect(getByText('Parent Child')).toBeTruthy()
expect(getByText('Child')).toBeTruthy()
})
it('should have correct displayName', () => {
expect(Text.displayName).toBe('Text')
})
})
describe('Style Merging', () => {
it('should merge custom object style with default style', () => {
const customStyle = { fontSize: 16, fontWeight: '600' as const }
const { getByText } = render(<Text style={customStyle}>Styled Text</Text>)
const textElement = getByText('Styled Text')
expect(textElement.props.style).toEqual(
expect.objectContaining({
color: '#F5F5F5',
fontSize: 16,
fontWeight: '600',
})
)
})
it('should merge custom array style with default style', () => {
const styleArray = [{ fontSize: 14 }, { fontWeight: '500' as const }]
const { getByText } = render(<Text style={styleArray}>Array Style</Text>)
const textElement = getByText('Array Style')
// Array style should be merged as [defaultStyle, ...styleArray]
expect(textElement.props.style).toContainEqual({ color: '#F5F5F5' })
expect(textElement.props.style).toContainEqual({ fontSize: 14 })
expect(textElement.props.style).toContainEqual({ fontWeight: '500' })
})
it('should allow overriding default color', () => {
const { getByText } = render(
<Text style={{ color: '#FF0000' }}>Red Text</Text>
)
const textElement = getByText('Red Text')
expect(textElement.props.style.color).toBe('#FF0000')
})
it('should pass className through correctly', () => {
const { getByText } = render(<Text className="text-primary">Class Text</Text>)
const textElement = getByText('Class Text')
expect(textElement.props.className).toBe('text-primary')
})
})
describe('onClick Handler', () => {
it('should create TouchableOpacity wrapper when onClick is provided', () => {
const mockOnClick = jest.fn()
const { getByText } = render(<Text onClick={mockOnClick}>Clickable Text</Text>)
const textElement = getByText('Clickable Text')
// Parent should be TouchableOpacity (or TouchableWithoutFeedback depending on implementation)
expect(textElement.parent).toBeTruthy()
})
it('should call onClick handler when pressed', () => {
const mockOnClick = jest.fn()
const { getByText } = render(<Text onClick={mockOnClick}>Click Me</Text>)
const textElement = getByText('Click Me')
fireEvent.press(textElement)
expect(mockOnClick).toHaveBeenCalledTimes(1)
})
it('should pass touchProps to TouchableOpacity', () => {
const mockOnClick = jest.fn()
const touchProps = {
testID: 'touchable-test',
activeOpacity: 0.5,
}
const { getByTestId } = render(
<Text onClick={mockOnClick} touchProps={touchProps}>
Touchable Text
</Text>
)
expect(getByTestId('touchable-test')).toBeTruthy()
})
it('should not interfere with normal text rendering when onClick is not provided', () => {
const { getByText } = render(<Text>Normal Text</Text>)
const textElement = getByText('Normal Text')
expect(textElement).toBeTruthy()
// Should be plain Text element, not wrapped
expect(textElement.parent.type).not.toBe('TouchableOpacity')
})
})
describe('Animated Text', () => {
it('should render Animated.Text when animated prop is true', () => {
const { getByText } = render(<Text animated={true}>Animated Text</Text>)
const textElement = getByText('Animated Text')
expect(textElement).toBeTruthy()
// Animated.Text should still render the content
})
it('should pass style to Animated.Text component', () => {
const customStyle = { fontSize: 20 }
const { getByText } = render(
<Text animated={true} style={customStyle}>
Animated Styled
</Text>
)
const textElement = getByText('Animated Styled')
expect(textElement.props.style).toEqual(
expect.objectContaining({
color: '#F5F5F5',
fontSize: 20,
})
)
})
})
describe('Props Forwarding', () => {
it('should forward standard RN Text props', () => {
const { getByText } = render(
<Text numberOfLines={2} ellipsizeMode="tail">
Long text that should be truncated
</Text>
)
const textElement = getByText(/Long text/)
expect(textElement.props.numberOfLines).toBe(2)
expect(textElement.props.ellipsizeMode).toBe('tail')
})
it('should forward accessibility props', () => {
const { getByText } = render(
<Text accessibilityLabel="Label text" accessible={true}>
Content
</Text>
)
const textElement = getByText('Content')
expect(textElement.props.accessibilityLabel).toBe('Label text')
expect(textElement.props.accessible).toBe(true)
})
it('should forward testID prop', () => {
const { getByTestId } = render(<Text testID="text-test-id">Test Content</Text>)
expect(getByTestId('text-test-id')).toBeTruthy()
})
it('should forward onPress through onClick', () => {
const mockOnPress = jest.fn()
const { getByText } = render(<Text onClick={mockOnPress}>Press Me</Text>)
const textElement = getByText('Press Me')
fireEvent.press(textElement)
expect(mockOnPress).toHaveBeenCalled()
})
})
describe('Edge Cases', () => {
it('should handle empty string children', () => {
const { getByText } = render(<Text></Text>)
// Empty text should still render
const textElement = getByText('')
expect(textElement).toBeTruthy()
})
it('should handle null children gracefully', () => {
// null should not cause crash, component renders but with no text content
const result = render(<Text>{null}</Text>)
expect(result).toBeTruthy()
})
it('should handle style as undefined', () => {
const { getByText } = render(<Text style={undefined}>No Style</Text>)
const textElement = getByText('No Style')
expect(textElement.props.style).toEqual({ color: '#F5F5F5' })
})
it('should handle className as undefined', () => {
const { getByText } = render(<Text className={undefined}>No Class</Text>)
const textElement = getByText('No Class')
expect(textElement.props.className).toBe('')
})
it('should handle complex style objects', () => {
const complexStyle = {
fontSize: 16,
fontWeight: 'bold' as const,
lineHeight: 24,
letterSpacing: 0.5,
textTransform: 'uppercase' as const,
}
const { getByText } = render(<Text style={complexStyle}>Complex Style</Text>)
const textElement = getByText('Complex Style')
expect(textElement.props.style).toEqual(
expect.objectContaining({
color: '#F5F5F5',
fontSize: 16,
fontWeight: 'bold',
lineHeight: 24,
letterSpacing: 0.5,
textTransform: 'uppercase',
})
)
})
it('should prioritize onClick over animated when both are provided', () => {
const mockOnClick = jest.fn()
const { getByText } = render(
<Text onClick={mockOnClick} animated={true}>
Both Props
</Text>
)
const textElement = getByText('Both Props')
// animated prop takes precedence in the conditional rendering
// onClick is ignored when animated is true based on the implementation
})
})
describe('Integration with Other Components', () => {
it('should work within a View component', () => {
const { getByText } = render(
<Text>
<Text>Nested</Text> Text
</Text>
)
// In React Native, nested Text components are flattened into a single text node
expect(getByText('Nested Text')).toBeTruthy()
expect(getByText('Nested')).toBeTruthy()
})
it('should handle multiple onClick handlers correctly', () => {
const onClick1 = jest.fn()
const onClick2 = jest.fn()
const { getByText } = render(
<>
<Text onClick={onClick1}>First</Text>
<Text onClick={onClick2}>Second</Text>
</>
)
fireEvent.press(getByText('First'))
fireEvent.press(getByText('Second'))
expect(onClick1).toHaveBeenCalledTimes(1)
expect(onClick2).toHaveBeenCalledTimes(1)
})
})
})