@ledgerhq/lumen-ui-rnative 0.0.68 → 0.0.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/.storybook/mocks/blur.tsx +1 -2
  2. package/dist/package.json +2 -2
  3. package/dist/src/i18n/locales/de.json +4 -0
  4. package/dist/src/i18n/locales/en.json +4 -0
  5. package/dist/src/i18n/locales/es.json +4 -0
  6. package/dist/src/i18n/locales/fr.json +4 -0
  7. package/dist/src/i18n/locales/ja.json +4 -0
  8. package/dist/src/i18n/locales/ko.json +4 -0
  9. package/dist/src/i18n/locales/pt.json +4 -0
  10. package/dist/src/i18n/locales/ru.json +4 -0
  11. package/dist/src/i18n/locales/th.json +4 -0
  12. package/dist/src/i18n/locales/tr.json +4 -0
  13. package/dist/src/i18n/locales/zh.json +4 -0
  14. package/dist/src/lib/Components/Avatar/Avatar.d.ts +19 -0
  15. package/dist/src/lib/Components/Avatar/Avatar.d.ts.map +1 -0
  16. package/dist/src/lib/Components/Avatar/Avatar.js +81 -0
  17. package/dist/src/lib/Components/Avatar/Avatar.stories.d.ts +22 -0
  18. package/dist/src/lib/Components/Avatar/Avatar.stories.d.ts.map +1 -0
  19. package/dist/src/lib/Components/Avatar/Avatar.stories.js +72 -0
  20. package/dist/src/lib/Components/Avatar/index.d.ts +3 -0
  21. package/dist/src/lib/Components/Avatar/index.d.ts.map +1 -0
  22. package/dist/src/lib/Components/Avatar/index.js +2 -0
  23. package/dist/src/lib/Components/Avatar/types.d.ts +26 -0
  24. package/dist/src/lib/Components/Avatar/types.d.ts.map +1 -0
  25. package/dist/src/lib/Components/Avatar/types.js +1 -0
  26. package/dist/src/lib/Components/CardButton/CardButton.js +3 -3
  27. package/dist/src/lib/Components/PageIndicator/PageIndicator.d.ts.map +1 -1
  28. package/dist/src/lib/Components/PageIndicator/PageIndicator.js +3 -2
  29. package/dist/src/lib/Components/PageIndicator/PageIndicator.stories.js +4 -4
  30. package/dist/src/lib/Components/PageIndicator/types.d.ts +1 -1
  31. package/dist/src/lib/Components/index.d.ts +1 -0
  32. package/dist/src/lib/Components/index.d.ts.map +1 -1
  33. package/dist/src/lib/Components/index.js +1 -0
  34. package/dist/src/lib/Symbols/Icons/Chart5.d.ts +35 -0
  35. package/dist/src/lib/Symbols/Icons/Chart5.d.ts.map +1 -0
  36. package/dist/src/lib/Symbols/Icons/Chart5.js +34 -0
  37. package/dist/src/lib/Symbols/Icons/Chart5Fill.d.ts +35 -0
  38. package/dist/src/lib/Symbols/Icons/Chart5Fill.d.ts.map +1 -0
  39. package/dist/src/lib/Symbols/Icons/Chart5Fill.js +34 -0
  40. package/dist/src/lib/Symbols/Icons/CurveDown.d.ts +35 -0
  41. package/dist/src/lib/Symbols/Icons/CurveDown.d.ts.map +1 -0
  42. package/dist/src/lib/Symbols/Icons/CurveDown.js +34 -0
  43. package/dist/src/lib/Symbols/Icons/CurveUp.d.ts +35 -0
  44. package/dist/src/lib/Symbols/Icons/CurveUp.d.ts.map +1 -0
  45. package/dist/src/lib/Symbols/Icons/CurveUp.js +34 -0
  46. package/dist/src/lib/Symbols/Icons/Target.d.ts +35 -0
  47. package/dist/src/lib/Symbols/Icons/Target.d.ts.map +1 -0
  48. package/dist/src/lib/Symbols/Icons/Target.js +34 -0
  49. package/dist/src/lib/Symbols/index.d.ts +5 -0
  50. package/dist/src/lib/Symbols/index.d.ts.map +1 -1
  51. package/dist/src/lib/Symbols/index.js +5 -0
  52. package/package.json +3 -3
  53. package/src/i18n/locales/de.json +4 -0
  54. package/src/i18n/locales/en.json +4 -0
  55. package/src/i18n/locales/es.json +4 -0
  56. package/src/i18n/locales/fr.json +4 -0
  57. package/src/i18n/locales/ja.json +4 -0
  58. package/src/i18n/locales/ko.json +4 -0
  59. package/src/i18n/locales/pt.json +4 -0
  60. package/src/i18n/locales/ru.json +4 -0
  61. package/src/i18n/locales/th.json +4 -0
  62. package/src/i18n/locales/tr.json +4 -0
  63. package/src/i18n/locales/zh.json +4 -0
  64. package/src/lib/Components/Avatar/Avatar.mdx +323 -0
  65. package/src/lib/Components/Avatar/Avatar.stories.tsx +127 -0
  66. package/src/lib/Components/Avatar/Avatar.test.tsx +215 -0
  67. package/src/lib/Components/Avatar/Avatar.tsx +132 -0
  68. package/src/lib/Components/Avatar/index.ts +2 -0
  69. package/src/lib/Components/Avatar/types.ts +26 -0
  70. package/src/lib/Components/CardButton/CardButton.tsx +3 -3
  71. package/src/lib/Components/PageIndicator/PageIndicator.mdx +7 -4
  72. package/src/lib/Components/PageIndicator/PageIndicator.stories.tsx +5 -5
  73. package/src/lib/Components/PageIndicator/PageIndicator.test.tsx +14 -14
  74. package/src/lib/Components/PageIndicator/PageIndicator.tsx +6 -2
  75. package/src/lib/Components/PageIndicator/types.ts +1 -1
  76. package/src/lib/Components/index.ts +1 -0
  77. package/src/lib/Symbols/Icons/Chart5.tsx +53 -0
  78. package/src/lib/Symbols/Icons/Chart5Fill.tsx +42 -0
  79. package/src/lib/Symbols/Icons/CurveDown.tsx +69 -0
  80. package/src/lib/Symbols/Icons/CurveUp.tsx +68 -0
  81. package/src/lib/Symbols/Icons/Target.tsx +45 -0
  82. package/src/lib/Symbols/index.ts +5 -0
@@ -0,0 +1,323 @@
1
+ import { Meta, Canvas, Controls } from '@storybook/addon-docs/blocks';
2
+ import * as AvatarStories from './Avatar.stories';
3
+ import { Avatar } from './Avatar';
4
+ import { CustomTabs, Tab } from '../../../../.storybook/components';
5
+ import { DoVsDontRow, DoBlockItem, DontBlockItem } from '../../../../.storybook/components/DoVsDont';
6
+ import CommonRulesDoAndDont from '../../../../.storybook/components/DoVsDont/CommonRulesDoAndDont.mdx';
7
+ import { Box } from '../Utility';
8
+
9
+ <Meta title='Communication/Avatar' of={AvatarStories} />
10
+
11
+ # 👤 Avatar
12
+
13
+ <CustomTabs>
14
+ <Tab label="Overview">
15
+
16
+ ## Introduction
17
+
18
+ The Avatar component displays a circular user profile image with automatic fallback to a User icon when the image is unavailable or fails to load. It supports an optional notification indicator and two size variants. This React Native implementation ensures consistent behavior across mobile platforms while maintaining design system coherence.
19
+
20
+ > View in [Figma](https://www.figma.com/design/JxaLVMTWirCpU0rsbZ30k7/2.-Components-Library?node-id=10546-614&p=f&m=dev).
21
+
22
+ ## Anatomy
23
+
24
+ <Canvas of={AvatarStories.Base} />
25
+
26
+ - **Image/Fallback Icon**: The circular user image or User icon fallback displayed when image is unavailable
27
+ - **Notification Indicator (optional)**: A red dot positioned at the top-right corner to indicate notifications
28
+ - **Container**: Rounded full background with muted styling
29
+
30
+ ## Properties
31
+
32
+ ### Overview
33
+
34
+ <Canvas of={AvatarStories.Base} />
35
+ <Controls of={AvatarStories.Base} />
36
+
37
+ ### Size
38
+
39
+ Avatars come in two different sizes:
40
+
41
+ - **sm** (40px)
42
+ - **md** (48px, default)
43
+
44
+ <Canvas of={AvatarStories.SizeShowcase} />
45
+
46
+ ### States
47
+
48
+ The Avatar component handles two primary states automatically:
49
+
50
+ - **With Image**: Displays the provided image when `src` is valid and loads successfully
51
+ - **Fallback**: Shows a User icon when `src` is undefined or the image fails to load
52
+
53
+ <Canvas of={AvatarStories.FallbackShowcase} />
54
+
55
+ ### Notification Indicator
56
+
57
+ An optional notification indicator can be displayed to show status or alerts:
58
+
59
+ - **showNotification**: Boolean prop to toggle the notification dot (default: false)
60
+
61
+ <Canvas of={AvatarStories.NotificationShowcase} />
62
+
63
+ ### As Interactive Trigger
64
+
65
+ Avatars are commonly used as interactive triggers for navigation or actions:
66
+
67
+ <Canvas of={AvatarStories.InteractiveShowcase} />
68
+
69
+ ## Accessibility
70
+
71
+ To be implemented:
72
+
73
+ - **Color contrast**
74
+ - **Text zoom support**
75
+ - **Semantic alt text for images**
76
+ - **Screen reader announcements for notification state**
77
+ - **Touch target sizing** (minimum 44px)
78
+
79
+ </Tab>
80
+ <Tab label="Implementation">
81
+
82
+ ## Setup
83
+
84
+ Install and set up the library with our [Setup Guide →](?path=/docs/getting-started-setup--docs).
85
+
86
+ ### Basic Usage
87
+
88
+ ```tsx
89
+ import { Avatar } from '@ledgerhq/lumen-ui-rnative';
90
+
91
+ function MyComponent() {
92
+ return <Avatar src="https://example.com/photo.jpg" size="md" />;
93
+ }
94
+ ```
95
+
96
+ ### Fallback Handling
97
+
98
+ The Avatar component automatically displays a fallback User icon when the image source is undefined or fails to load:
99
+
100
+ ```tsx
101
+ import { Avatar } from '@ledgerhq/lumen-ui-rnative';
102
+
103
+ function MyComponent() {
104
+ // Fallback will be shown automatically
105
+ return <Avatar size="md" alt="User profile" />;
106
+ }
107
+ ```
108
+
109
+ ### With Notification
110
+
111
+ Display a notification indicator to show status or alerts:
112
+
113
+ ```tsx
114
+ import { Avatar } from '@ledgerhq/lumen-ui-rnative';
115
+
116
+ function MyComponent() {
117
+ const hasNotifications = true;
118
+
119
+ return (
120
+ <Avatar
121
+ src="https://example.com/photo.jpg"
122
+ showNotification={hasNotifications}
123
+ size="md"
124
+ alt="User profile"
125
+ />
126
+ );
127
+ }
128
+ ```
129
+
130
+ ### Different Sizes
131
+
132
+ Choose between small and medium sizes based on your layout needs:
133
+
134
+ ```tsx
135
+ import { Avatar, Box } from '@ledgerhq/lumen-ui-rnative';
136
+
137
+ function MyComponent() {
138
+ return (
139
+ <Box lx={{ flexDirection: 'row', alignItems: 'center', gap: 's8' }}>
140
+ <Avatar src="https://example.com/photo.jpg" size="sm" />
141
+ <Avatar src="https://example.com/photo.jpg" size="md" />
142
+ </Box>
143
+ );
144
+ }
145
+ ```
146
+
147
+ ### As Interactive Trigger
148
+
149
+ Avatars are commonly used as interactive triggers for navigation or actions. Wrap the Avatar in a `Pressable` component:
150
+
151
+ ```tsx
152
+ import { Avatar } from '@ledgerhq/lumen-ui-rnative';
153
+ import { Pressable, Linking } from 'react-native';
154
+
155
+ function UserProfile() {
156
+ const handlePress = () => {
157
+ // Navigate or perform action
158
+ console.log('Avatar pressed');
159
+ };
160
+
161
+ return (
162
+ <Pressable
163
+ onPress={handlePress}
164
+ style={({ pressed }) => ({
165
+ borderRadius: 9999,
166
+ opacity: pressed ? 0.7 : 1,
167
+ backgroundColor: pressed ? 'rgba(0, 0, 0, 0.05)' : 'transparent',
168
+ })}
169
+ >
170
+ <Avatar
171
+ src="https://example.com/photo.jpg"
172
+ size="md"
173
+ alt="User menu"
174
+ showNotification
175
+ />
176
+ </Pressable>
177
+ );
178
+ }
179
+ ```
180
+
181
+ **Key points:**
182
+ - Wrap Avatar in a `Pressable` component for touch interactions
183
+ - Use `borderRadius: 9999` to maintain circular touch area
184
+ - Add opacity changes for visual feedback on press
185
+ - Provide appropriate `alt` text for accessibility
186
+
187
+ ### Custom Styling
188
+
189
+ While the component comes with predefined styles, you can extend them using the `lx` prop for token-based styling:
190
+
191
+ ```tsx
192
+ <Avatar
193
+ src="https://example.com/photo.jpg"
194
+ size="md"
195
+ lx={{ marginRight: 's8' }} // Layout positioning with design tokens
196
+ />
197
+ ```
198
+
199
+ You can also use the `style` prop for escape-hatch styling when needed:
200
+
201
+ ```tsx
202
+ <Avatar
203
+ src="https://example.com/photo.jpg"
204
+ size="md"
205
+ style={{ marginRight: 8 }} // Raw style values
206
+ />
207
+ ```
208
+
209
+ ### Complete Example
210
+
211
+ Here's a comprehensive example showing all Avatar features in a user profile context:
212
+
213
+ ```tsx
214
+ import { Avatar, Box, Text } from '@ledgerhq/lumen-ui-rnative';
215
+ import { Pressable } from 'react-native';
216
+ import { useState } from 'react';
217
+
218
+ function UserProfileHeader() {
219
+ const [user, setUser] = useState({
220
+ name: 'John Doe',
221
+ avatar: 'https://example.com/photo.jpg',
222
+ hasNotifications: true,
223
+ });
224
+
225
+ const handleProfilePress = () => {
226
+ console.log('Opening user menu...');
227
+ // Navigate to profile or show menu
228
+ };
229
+
230
+ return (
231
+ <Box lx={{ flexDirection: 'row', alignItems: 'center', gap: 's12', padding: 's16' }}>
232
+ <Pressable
233
+ onPress={handleProfilePress}
234
+ style={({ pressed }) => ({
235
+ borderRadius: 9999,
236
+ opacity: pressed ? 0.7 : 1,
237
+ backgroundColor: pressed ? 'rgba(0, 0, 0, 0.05)' : 'transparent',
238
+ })}
239
+ >
240
+ <Avatar
241
+ src={user.avatar}
242
+ size="md"
243
+ showNotification={user.hasNotifications}
244
+ alt={`${user.name} profile picture`}
245
+ />
246
+ </Pressable>
247
+ <Box lx={{ flexDirection: 'column' }}>
248
+ <Text>{user.name}</Text>
249
+ {user.hasNotifications && (
250
+ <Text style={{ fontSize: 12, color: '#666' }}>
251
+ You have new notifications
252
+ </Text>
253
+ )}
254
+ </Box>
255
+ </Box>
256
+ );
257
+ }
258
+ ```
259
+
260
+ ## Do's and Don'ts
261
+
262
+ The following guidelines ensure consistent usage of the Avatar component and maintain design system principles.
263
+
264
+ <Box lx={{ flexDirection: 'column', gap: 's24' }}>
265
+ <DoVsDontRow>
266
+ <DoBlockItem
267
+ title='Provide descriptive alt text'
268
+ description='Use meaningful alt text for better accessibility and screen reader support'
269
+ >
270
+
271
+ {/* prettier-ignore */}
272
+ ```tsx
273
+ <Avatar
274
+ src="https://example.com/photo.jpg"
275
+ alt="John Doe profile picture"
276
+ size="md"
277
+ />
278
+ ```
279
+
280
+ </DoBlockItem>
281
+ <DontBlockItem
282
+ title="Don't omit alt text"
283
+ description='Missing alt text reduces accessibility for screen reader users'
284
+ >
285
+
286
+ {/* prettier-ignore */}
287
+ ```tsx
288
+ <Avatar
289
+ src="https://example.com/photo.jpg"
290
+ size="md"
291
+ />
292
+ ```
293
+
294
+ </DontBlockItem>
295
+
296
+ </DoVsDontRow>
297
+
298
+ <CommonRulesDoAndDont />
299
+ </Box>
300
+
301
+ ## Platform Considerations
302
+
303
+ ### iOS Specific
304
+
305
+ - Avatars automatically adapt to iOS design guidelines
306
+ - Touch feedback uses appropriate opacity transitions
307
+ - Respects iOS accessibility settings like reduced motion
308
+
309
+ ### Android Specific
310
+
311
+ - Material Design touch feedback is supported through React Native's Pressable
312
+ - Handles Android back button interactions appropriately
313
+ - Respects Android accessibility services
314
+
315
+ ### Cross-Platform
316
+
317
+ - Consistent visual appearance across platforms
318
+ - Platform-appropriate touch feedback via Pressable
319
+ - Unified API regardless of platform
320
+ - Images load using React Native's Image component with cross-platform optimization
321
+
322
+ </Tab>
323
+ </CustomTabs>
@@ -0,0 +1,127 @@
1
+ import { Meta, StoryObj } from '@storybook/react-native-web-vite';
2
+ import { View, Text, Pressable, Linking } from 'react-native';
3
+
4
+ import { Box } from '../Utility';
5
+ import { Avatar } from './Avatar';
6
+
7
+ const meta = {
8
+ component: Avatar,
9
+ title: 'Communication/Avatar',
10
+ parameters: {
11
+ docs: {
12
+ source: {
13
+ language: 'tsx',
14
+ format: true,
15
+ type: 'code',
16
+ },
17
+ },
18
+ },
19
+ } satisfies Meta<typeof Avatar>;
20
+
21
+ export default meta;
22
+ type Story = StoryObj<typeof meta>;
23
+
24
+ const exampleSrc =
25
+ 'https://plus.unsplash.com/premium_photo-1689551670902-19b441a6afde?q=80&w=774&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D';
26
+
27
+ export const Base: Story = {
28
+ args: {
29
+ src: exampleSrc,
30
+ alt: 'avatar',
31
+ size: 'md',
32
+ showNotification: false,
33
+ },
34
+ render: (args) => <Avatar {...args} />,
35
+ parameters: {
36
+ docs: {
37
+ source: {
38
+ code: `<Avatar src="https://example.com" size="md" alt="avatar" showNotification={false} />`,
39
+ },
40
+ },
41
+ },
42
+ };
43
+
44
+ export const SizeShowcase: Story = {
45
+ render: () => (
46
+ <Box
47
+ lx={{
48
+ alignItems: 'stretch',
49
+ flexDirection: 'row',
50
+ gap: 's16',
51
+ }}
52
+ >
53
+ <View style={{ alignItems: 'center', justifyContent: 'flex-end' }}>
54
+ <Avatar
55
+ src={exampleSrc}
56
+ alt='avatar'
57
+ size='sm'
58
+ showNotification={false}
59
+ />
60
+ <Text style={{ marginTop: 4 }}>sm</Text>
61
+ </View>
62
+ <View style={{ alignItems: 'center', justifyContent: 'flex-end' }}>
63
+ <Avatar
64
+ src={exampleSrc}
65
+ alt='avatar'
66
+ size='md'
67
+ showNotification={false}
68
+ />
69
+ <Text style={{ marginTop: 4 }}>md</Text>
70
+ </View>
71
+ </Box>
72
+ ),
73
+ };
74
+
75
+ export const FallbackShowcase: Story = {
76
+ args: {
77
+ src: 'https://brokenLink.random',
78
+ size: 'md',
79
+ alt: 'Fallback example',
80
+ showNotification: false,
81
+ },
82
+ render: (args) => <Avatar {...args} />,
83
+ parameters: {
84
+ docs: {
85
+ source: {
86
+ code: `<Avatar src="https://brokenLink.random" size="md" alt="Fallback example" showNotification={false} />`,
87
+ },
88
+ },
89
+ },
90
+ };
91
+
92
+ export const NotificationShowcase: Story = {
93
+ render: () => (
94
+ <Box
95
+ lx={{
96
+ flexDirection: 'row',
97
+ gap: 's16',
98
+ }}
99
+ >
100
+ <Avatar
101
+ src={exampleSrc}
102
+ alt='avatar'
103
+ size='md'
104
+ showNotification={false}
105
+ />
106
+ <Avatar src={exampleSrc} alt='avatar' size='md' showNotification={true} />
107
+ </Box>
108
+ ),
109
+ };
110
+
111
+ const onPressRedirect = () =>
112
+ Linking.openURL('https://shop.ledger.com/pages/ledger-nano-gen5');
113
+
114
+ export const InteractiveShowcase: Story = {
115
+ render: () => (
116
+ <Pressable
117
+ onPress={onPressRedirect}
118
+ style={({ pressed }) => ({
119
+ borderRadius: 9999,
120
+ opacity: pressed ? 0.7 : 1,
121
+ backgroundColor: pressed ? 'rgba(0, 0, 0, 0.05)' : 'transparent',
122
+ })}
123
+ >
124
+ <Avatar src={exampleSrc} size='md' showNotification />
125
+ </Pressable>
126
+ ),
127
+ };
@@ -0,0 +1,215 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
3
+ import { render, waitFor } from '@testing-library/react-native';
4
+ import React from 'react';
5
+ import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
6
+ import { Avatar } from './Avatar';
7
+
8
+ const { colors, sizes } = ledgerLiveThemes.dark;
9
+
10
+ const TestWrapper = ({ children }: { children: React.ReactNode }) => (
11
+ <ThemeProvider themes={ledgerLiveThemes} colorScheme='dark' locale='en'>
12
+ {children}
13
+ </ThemeProvider>
14
+ );
15
+
16
+ describe('Avatar Component', () => {
17
+ const testSrc =
18
+ 'https://plus.unsplash.com/premium_photo-1689551670902-19b441a6afde?q=80&w=774&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D';
19
+ it('should render correctly with minimal props', () => {
20
+ const { getByLabelText } = render(
21
+ <TestWrapper>
22
+ <Avatar src={testSrc} />
23
+ </TestWrapper>,
24
+ );
25
+ getByLabelText('avatar');
26
+ });
27
+
28
+ it('should render with custom alt text', () => {
29
+ const { getByLabelText } = render(
30
+ <TestWrapper>
31
+ <Avatar src={testSrc} alt='user profile' />
32
+ </TestWrapper>,
33
+ );
34
+ getByLabelText('user profile');
35
+ });
36
+
37
+ it('should render with different sizes', () => {
38
+ const { getByTestId, rerender } = render(
39
+ <TestWrapper>
40
+ <Avatar testID='avatar-id' size='sm' />
41
+ </TestWrapper>,
42
+ );
43
+ expect(getByTestId('avatar-id').props.style.width).toBe(sizes.s40);
44
+ expect(getByTestId('avatar-id').props.style.height).toBe(sizes.s40);
45
+
46
+ rerender(
47
+ <TestWrapper>
48
+ <Avatar testID='avatar-id' size='md' />
49
+ </TestWrapper>,
50
+ );
51
+ expect(getByTestId('avatar-id').props.style.width).toBe(sizes.s48);
52
+ expect(getByTestId('avatar-id').props.style.height).toBe(sizes.s48);
53
+ });
54
+
55
+ it('should render fallback icon when no src is provided', () => {
56
+ const { getByLabelText, getByTestId } = render(
57
+ <TestWrapper>
58
+ <Avatar />
59
+ </TestWrapper>,
60
+ );
61
+
62
+ const avatarContainer = getByLabelText('avatar');
63
+ expect(avatarContainer).toBeTruthy();
64
+ expect(avatarContainer.props.accessibilityRole).toBe('image');
65
+
66
+ expect(getByTestId('avatar-fallback-icon')).toBeTruthy();
67
+ });
68
+
69
+ it('should render image when src is provided', () => {
70
+ const { getByLabelText, getByTestId } = render(
71
+ <TestWrapper>
72
+ <Avatar src='https://example.com/avatar.jpg' alt='user avatar' />
73
+ </TestWrapper>,
74
+ );
75
+
76
+ const avatarContainer = getByLabelText('user avatar');
77
+ expect(avatarContainer).toBeTruthy();
78
+ expect(avatarContainer.props.accessibilityRole).toBe('image');
79
+
80
+ const image = getByTestId('avatar-image');
81
+ expect(image.props.source).toEqual({
82
+ uri: 'https://example.com/avatar.jpg',
83
+ });
84
+ });
85
+
86
+ it('should render fallback icon on image error', async () => {
87
+ const { getByLabelText, getByTestId, queryByTestId, rerender } = render(
88
+ <TestWrapper>
89
+ <Avatar src='https://example.com/invalid.jpg' alt='broken image' />
90
+ </TestWrapper>,
91
+ );
92
+
93
+ const avatarContainer = getByLabelText('broken image');
94
+ expect(avatarContainer).toBeTruthy();
95
+
96
+ const image = getByTestId('avatar-image');
97
+ expect(image).toBeTruthy();
98
+
99
+ image.props.onError();
100
+
101
+ rerender(
102
+ <TestWrapper>
103
+ <Avatar src='https://example.com/invalid.jpg' alt='broken image' />
104
+ </TestWrapper>,
105
+ );
106
+
107
+ await waitFor(() => {
108
+ expect(queryByTestId('avatar-image')).toBeNull();
109
+ expect(getByTestId('avatar-fallback-icon')).toBeTruthy();
110
+ });
111
+ });
112
+
113
+ it('should reset error state when src changes', async () => {
114
+ const { getByLabelText, getByTestId, rerender } = render(
115
+ <TestWrapper>
116
+ <Avatar src='https://example.com/avatar1.jpg' alt='avatar image' />
117
+ </TestWrapper>,
118
+ );
119
+
120
+ const avatarContainer = getByLabelText('avatar image');
121
+ expect(avatarContainer).toBeTruthy();
122
+
123
+ const image = getByTestId('avatar-image');
124
+ image.props.onError();
125
+
126
+ rerender(
127
+ <TestWrapper>
128
+ <Avatar src='https://example.com/avatar2.jpg' alt='avatar image' />
129
+ </TestWrapper>,
130
+ );
131
+
132
+ await waitFor(() => {
133
+ const newImage = getByTestId('avatar-image');
134
+ expect(newImage.props.source).toEqual({
135
+ uri: 'https://example.com/avatar2.jpg',
136
+ });
137
+ });
138
+ });
139
+
140
+ it('should show notification indicator when showNotification is true', () => {
141
+ const { getByTestId } = render(
142
+ <TestWrapper>
143
+ <Avatar testID='avatar-id' showNotification />
144
+ </TestWrapper>,
145
+ );
146
+
147
+ const avatar = getByTestId('avatar-id');
148
+ const notificationIndicator = avatar.props.children[0];
149
+
150
+ expect(notificationIndicator).toBeTruthy();
151
+ expect(notificationIndicator.props.style.backgroundColor).toBe(
152
+ colors.bg.errorStrong,
153
+ );
154
+ });
155
+
156
+ it('should not show notification indicator by default', () => {
157
+ const { getByTestId } = render(
158
+ <TestWrapper>
159
+ <Avatar testID='avatar-id' />
160
+ </TestWrapper>,
161
+ );
162
+
163
+ const avatar = getByTestId('avatar-id');
164
+ const notificationIndicator = avatar.props.children[0];
165
+
166
+ expect(notificationIndicator).toBe(false);
167
+ });
168
+
169
+ it('should apply correct notification indicator size based on avatar size', () => {
170
+ const { getByTestId, rerender } = render(
171
+ <TestWrapper>
172
+ <Avatar testID='avatar-id' size='sm' showNotification />
173
+ </TestWrapper>,
174
+ );
175
+
176
+ let avatar = getByTestId('avatar-id');
177
+ let notificationIndicator = avatar.props.children[0];
178
+ expect(notificationIndicator.props.style.width).toBe(sizes.s10);
179
+ expect(notificationIndicator.props.style.height).toBe(sizes.s10);
180
+
181
+ rerender(
182
+ <TestWrapper>
183
+ <Avatar testID='avatar-id' size='md' showNotification />
184
+ </TestWrapper>,
185
+ );
186
+
187
+ avatar = getByTestId('avatar-id');
188
+ notificationIndicator = avatar.props.children[0];
189
+ expect(notificationIndicator.props.style.width).toBe(sizes.s12);
190
+ expect(notificationIndicator.props.style.height).toBe(sizes.s12);
191
+ });
192
+
193
+ it('should apply custom styles', () => {
194
+ const customStyle = { borderWidth: 2 };
195
+ const { getByTestId } = render(
196
+ <TestWrapper>
197
+ <Avatar testID='avatar-id' style={customStyle} />
198
+ </TestWrapper>,
199
+ );
200
+
201
+ const avatar = getByTestId('avatar-id');
202
+ expect(avatar.props.style.borderWidth).toBe(2);
203
+ });
204
+
205
+ it('should pass additional props to the Box component', () => {
206
+ const { getByTestId } = render(
207
+ <TestWrapper>
208
+ <Avatar testID='custom-test-id' accessibilityLabel='Profile Avatar' />
209
+ </TestWrapper>,
210
+ );
211
+
212
+ const avatar = getByTestId('custom-test-id');
213
+ expect(avatar.props.accessibilityLabel).toBe('Profile Avatar');
214
+ });
215
+ });