@linktr.ee/messaging-react 4.1.7-rc-1787179679 → 4.1.7-rc-1787181506

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "4.1.7-rc-1787179679",
3
+ "version": "4.1.7-rc-1787181506",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -50,19 +50,17 @@
50
50
  "chromatic:local": "yarn storybook:build && chromatic"
51
51
  },
52
52
  "dependencies": {
53
- "@linktr.ee/messaging-core": "2.5.0-rc-1787179679",
53
+ "@linktr.ee/messaging-core": "2.5.0-rc-1787181506",
54
54
  "@linktr.ee/messaging-taxonomy": "^0.10.0",
55
55
  "@phosphor-icons/react": "^2.1.10"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@linktr.ee/arbor": "25.0.0",
59
59
  "@linktr.ee/design-tokens": "14.0.0",
60
- "@storybook/addon-essentials": "^8.5.0",
61
- "@storybook/addon-interactions": "^8.5.0",
62
- "@storybook/addon-links": "^8.5.0",
63
- "@storybook/react": "^8.5.0",
64
- "@storybook/react-vite": "^8.5.0",
65
- "@storybook/test": "^8.5.0",
60
+ "@storybook/addon-docs": "10.5.7",
61
+ "@storybook/addon-links": "10.5.7",
62
+ "@storybook/react": "10.5.7",
63
+ "@storybook/react-vite": "10.5.7",
66
64
  "@tailwindcss/container-queries": "^0.1.1",
67
65
  "@testing-library/dom": "^10.0.0",
68
66
  "@testing-library/jest-dom": "^6.1.5",
@@ -79,7 +77,7 @@
79
77
  "postcss": "^8.4.49",
80
78
  "react": "^18.3.1",
81
79
  "react-dom": "^18.3.1",
82
- "storybook": "^8.5.0",
80
+ "storybook": "10.5.7",
83
81
  "stream-chat": "^9.41.1",
84
82
  "stream-chat-react": "^13.14.5",
85
83
  "tailwindcss": "^3.4.17",
@@ -27,4 +27,130 @@ describe('Avatar', () => {
27
27
  it('keeps fixed emoji sizes for larger avatars', () => {
28
28
  expect(emojiFontSize(40)).toHaveStyle({ fontSize: '14px' })
29
29
  })
30
+
31
+ it.each([
32
+ [32, '14px'],
33
+ [55, '14px'],
34
+ [56, '18px'],
35
+ [119, '18px'],
36
+ [120, '36px'],
37
+ ])(
38
+ 'uses the exact fixed font size at the %spx boundary',
39
+ (size, fontSize) => {
40
+ expect(emojiFontSize(size)).toHaveStyle({ fontSize })
41
+ }
42
+ )
43
+
44
+ it('does not show a star by default but shows one when starred', () => {
45
+ const defaultAvatar = renderWithProviders(
46
+ <Avatar id="user-1" name="Alice" />
47
+ )
48
+ expect(defaultAvatar.container.querySelector('svg')).not.toBeInTheDocument()
49
+
50
+ const starredAvatar = renderWithProviders(
51
+ <Avatar id="user-1" name="Alice" starred />
52
+ )
53
+ expect(starredAvatar.container.querySelector('svg')).toBeInTheDocument()
54
+ })
55
+
56
+ it('uses a squircle by default and a circle when explicitly requested', () => {
57
+ const squircleAvatar = renderWithProviders(
58
+ <Avatar id="user-1" name="Alice" />
59
+ )
60
+ const squircleElements = squircleAvatar.container.querySelectorAll(
61
+ '[style*="border-radius"]'
62
+ )
63
+ expect(squircleElements).toHaveLength(3)
64
+ squircleElements.forEach((element) =>
65
+ expect(element).toHaveStyle({ borderRadius: '1rem' })
66
+ )
67
+
68
+ const circleAvatar = renderWithProviders(
69
+ <Avatar id="user-1" name="Alice" shape="circle" />
70
+ )
71
+ const circleElements = circleAvatar.container.querySelectorAll(
72
+ '[style*="border-radius"]'
73
+ )
74
+ expect(circleElements).toHaveLength(3)
75
+ circleElements.forEach((element) =>
76
+ expect(element).toHaveStyle({ borderRadius: '50%' })
77
+ )
78
+ })
79
+
80
+ it('only applies the AI ring when dmAgentEnabled is true', () => {
81
+ const defaultAvatar = renderWithProviders(
82
+ <Avatar id="user-1" name="Alice" size={40} />
83
+ )
84
+ expect(
85
+ defaultAvatar.container.querySelector('[data-testid="avatar-ring"]')
86
+ ).not.toHaveStyle({
87
+ borderWidth: '2px',
88
+ borderStyle: 'solid',
89
+ })
90
+
91
+ const enabledAvatar = renderWithProviders(
92
+ <Avatar id="user-1" name="Alice" size={40} dmAgentEnabled />
93
+ )
94
+ expect(
95
+ enabledAvatar.container.querySelector('[data-testid="avatar-ring"]')
96
+ ).toHaveStyle({
97
+ borderWidth: '2px',
98
+ borderStyle: 'solid',
99
+ borderColor: 'var(--AI-Gradient, #7F22FE)',
100
+ boxShadow: 'inset 0 1px 2px 0 rgba(255, 255, 255, 0.5)',
101
+ })
102
+
103
+ const smallEnabledAvatar = renderWithProviders(
104
+ <Avatar id="user-1" name="Alice" size={20} dmAgentEnabled />
105
+ )
106
+ expect(
107
+ smallEnabledAvatar.container.querySelector('[data-testid="avatar-ring"]')
108
+ ).toHaveStyle({ borderWidth: '1px' })
109
+ })
110
+
111
+ it('renders an image instead of the emoji fallback when image is provided', () => {
112
+ const fallbackAvatar = renderWithProviders(<Avatar id="" name="Alice" />)
113
+ expect(
114
+ fallbackAvatar.container.querySelector('img')
115
+ ).not.toBeInTheDocument()
116
+ expect(fallbackAvatar.container).toHaveTextContent('🍎')
117
+ expect(
118
+ fallbackAvatar.container.querySelector('.avatar-fallback')
119
+ ).toHaveClass(
120
+ 'avatar-fallback',
121
+ 'flex',
122
+ 'h-full',
123
+ 'w-full',
124
+ 'items-center',
125
+ 'justify-center',
126
+ 'font-semibold',
127
+ 'select-none',
128
+ 'transition-colors'
129
+ )
130
+
131
+ const imageAvatar = renderWithProviders(
132
+ <Avatar id="" name="Alice" image="https://example.com/avatar.png" />
133
+ )
134
+ expect(imageAvatar.container.querySelector('img')).toHaveAttribute(
135
+ 'src',
136
+ 'https://example.com/avatar.png'
137
+ )
138
+ expect(imageAvatar.container).not.toHaveTextContent('🍎')
139
+ })
140
+
141
+ it('applies the fallback background color and forwards custom classes', () => {
142
+ const { container } = renderWithProviders(
143
+ <Avatar id="" name="Alice" className="custom-avatar" />
144
+ )
145
+ const root = container.firstElementChild
146
+ const ring = container.querySelector('[data-testid="avatar-ring"]')
147
+
148
+ expect(root).toHaveClass('relative', 'flex-shrink-0', 'custom-avatar')
149
+ expect(ring).toHaveClass('h-full', 'w-full', 'bg-transparent')
150
+ expect(root).toHaveStyle({
151
+ backgroundColor: '#FFD1DD',
152
+ width: '40px',
153
+ height: '40px',
154
+ })
155
+ })
30
156
  })
@@ -29,4 +29,43 @@ describe('getAvatarBgColor', () => {
29
29
  expect(color).toBe(emojiBgColors[emoji])
30
30
  expect(getAvatarBgColor(id)).toBe(color)
31
31
  })
32
+
33
+ it.each([
34
+ ['', '🍎', '#FFD1DD'],
35
+ ['id-2', '🍌', '#FFF1C9'],
36
+ ['id-3', '🍇', '#E6D9FF'],
37
+ ['id-4', '🍊', '#D6F4FF'],
38
+ ['id-5', '🍓', '#FFE0AE'],
39
+ ['id-6', '🥥', '#F7E7D8'],
40
+ ['id-7', '🍒', '#DFF4D8'],
41
+ ['id-8', '🥭', '#FFD8C2'],
42
+ ['id-9', '🍉', '#D6EBFF'],
43
+ ['id-10', '🍋', '#CAEEB5'],
44
+ ['id-11', '🥝', '#FFE7C7'],
45
+ ['id-12', '🫒', '#F6E3B4'],
46
+ ['id-13', '🍈', '#D9F7E1'],
47
+ ])('returns the exact fruit pair for %j', (id, emoji, bgColor) => {
48
+ expect(getAvatarEmoji(id)).toBe(emoji)
49
+ expect(getAvatarBgColor(id)).toBe(bgColor)
50
+ })
51
+
52
+ it('uses the absolute hash value for ids with a negative raw hash', () => {
53
+ const id = 'id-100'
54
+
55
+ expect(getAvatarEmoji(id)).toBe('🥭')
56
+ expect(getAvatarBgColor(id)).toBe('#FFD8C2')
57
+ })
58
+
59
+ it('is deterministic and keeps emoji and background color paired', () => {
60
+ const ids = ['', 'id-2', 'id-7', 'id-13', 'id-100']
61
+
62
+ ids.forEach((id) => {
63
+ const emoji = getAvatarEmoji(id)
64
+ const color = getAvatarBgColor(id)
65
+
66
+ expect(getAvatarEmoji(id)).toBe(emoji)
67
+ expect(getAvatarBgColor(id)).toBe(color)
68
+ expect(emojiBgColors[emoji]).toBe(color)
69
+ })
70
+ })
32
71
  })
@@ -0,0 +1,204 @@
1
+ import React from 'react'
2
+ import { describe, expect, it, vi } from 'vitest'
3
+
4
+ import { fireEvent, renderWithProviders, screen } from '../../test/utils'
5
+ import { getAvatarEmoji } from '../Avatar/getAvatarEmoji'
6
+
7
+ import { FaqListItem } from './FaqListItem'
8
+
9
+ import { FaqList } from './index'
10
+
11
+ const faqs = [
12
+ {
13
+ id: 'second',
14
+ question: 'What payment methods do you accept?',
15
+ answer: 'Cards and bank transfers.',
16
+ enabled: true,
17
+ order: 2,
18
+ },
19
+ {
20
+ id: 'disabled',
21
+ question: 'What is your phone number?',
22
+ answer: 'This FAQ is disabled.',
23
+ enabled: false,
24
+ order: 0,
25
+ },
26
+ {
27
+ id: 'first',
28
+ question: 'How do I get started?',
29
+ answer: 'Create an account.',
30
+ enabled: true,
31
+ order: 1,
32
+ },
33
+ ]
34
+
35
+ const baseItemClasses = [
36
+ 'w-full',
37
+ 'text-center',
38
+ 'p-4',
39
+ 'rounded-xl',
40
+ 'text-charcoal',
41
+ 'font-medium',
42
+ 'transition-colors',
43
+ 'focus-ring',
44
+ ]
45
+
46
+ describe('FaqListItem', () => {
47
+ it('renders its question and defaults to an enabled button', () => {
48
+ renderWithProviders(
49
+ <FaqListItem question="How do I get started?" onClick={vi.fn()} />
50
+ )
51
+
52
+ const button = screen.getByRole('button', {
53
+ name: 'How do I get started?',
54
+ })
55
+ expect(button).toBeEnabled()
56
+ expect(button.className.split(/\s+/)).toEqual([
57
+ ...baseItemClasses,
58
+ 'hover:brightness-95',
59
+ 'active:brightness-90',
60
+ ])
61
+ })
62
+
63
+ it('renders the FAQ surface color', () => {
64
+ renderWithProviders(
65
+ <FaqListItem question="How do I get started?" onClick={vi.fn()} />
66
+ )
67
+
68
+ expect(
69
+ screen.getByRole('button', { name: 'How do I get started?' })
70
+ ).toHaveStyle({ backgroundColor: '#E6E5E3' })
71
+ })
72
+
73
+ it('uses loading-only classes and blocks clicks while loading', () => {
74
+ const onClick = vi.fn()
75
+ renderWithProviders(
76
+ <FaqListItem question="How do I get started?" onClick={onClick} loading />
77
+ )
78
+
79
+ const button = screen.getByRole('button', {
80
+ name: 'How do I get started?',
81
+ })
82
+ expect(button).toBeDisabled()
83
+ expect(button.className.split(/\s+/)).toEqual([
84
+ ...baseItemClasses,
85
+ 'opacity-50',
86
+ 'cursor-not-allowed',
87
+ ])
88
+
89
+ fireEvent.click(button)
90
+ expect(onClick).not.toHaveBeenCalled()
91
+ })
92
+
93
+ it('calls the click handler once when enabled', () => {
94
+ const onClick = vi.fn()
95
+ renderWithProviders(
96
+ <FaqListItem question="How do I get started?" onClick={onClick} />
97
+ )
98
+
99
+ fireEvent.click(screen.getByRole('button'))
100
+ expect(onClick).toHaveBeenCalledTimes(1)
101
+ })
102
+ })
103
+
104
+ describe('FaqList', () => {
105
+ it('returns nothing when no FAQ is enabled', () => {
106
+ const { container } = renderWithProviders(
107
+ <FaqList
108
+ faqs={faqs.map((faq) => ({ ...faq, enabled: false }))}
109
+ onFaqClick={vi.fn()}
110
+ />
111
+ )
112
+
113
+ expect(container.firstChild).toBeNull()
114
+ })
115
+
116
+ it('renders enabled FAQs in ascending order and filters disabled FAQs', () => {
117
+ renderWithProviders(<FaqList faqs={faqs} onFaqClick={vi.fn()} />)
118
+
119
+ expect(
120
+ screen.getByRole('button', { name: 'How do I get started?' })
121
+ .parentElement
122
+ ).toHaveStyle({ backgroundColor: '#F1F0EE' })
123
+ expect(
124
+ screen.getAllByRole('button').map((button) => button.textContent)
125
+ ).toEqual(['How do I get started?', 'What payment methods do you accept?'])
126
+ expect(
127
+ screen.queryByRole('button', { name: 'What is your phone number?' })
128
+ ).toBeNull()
129
+ })
130
+
131
+ it('renders the header only when headerText is provided', () => {
132
+ const { container, rerender } = renderWithProviders(
133
+ <FaqList faqs={faqs} onFaqClick={vi.fn()} />
134
+ )
135
+ expect(screen.queryByText('Frequently asked questions')).toBeNull()
136
+ expect(container.querySelector('p')).toBeNull()
137
+
138
+ rerender(
139
+ <FaqList
140
+ faqs={faqs}
141
+ onFaqClick={vi.fn()}
142
+ headerText="Frequently asked questions"
143
+ />
144
+ )
145
+ expect(screen.getByText('Frequently asked questions')).toBeInTheDocument()
146
+ })
147
+
148
+ it('passes className to the list root', () => {
149
+ const { container } = renderWithProviders(
150
+ <FaqList
151
+ faqs={faqs}
152
+ onFaqClick={vi.fn()}
153
+ className="faq-list-custom-class"
154
+ />
155
+ )
156
+
157
+ expect(container.firstElementChild).toHaveClass('faq-list-custom-class')
158
+ })
159
+
160
+ it('renders an avatar only when avatar data is provided', () => {
161
+ const { rerender } = renderWithProviders(
162
+ <FaqList faqs={faqs} onFaqClick={vi.fn()} />
163
+ )
164
+ expect(screen.queryAllByTestId('avatar-ring')).toHaveLength(0)
165
+
166
+ rerender(
167
+ <FaqList faqs={faqs} onFaqClick={vi.fn()} avatarName="Brie Nala" />
168
+ )
169
+ expect(screen.queryAllByTestId('avatar-ring')).toHaveLength(1)
170
+ expect(screen.getByText(getAvatarEmoji('Brie Nala'))).toBeInTheDocument()
171
+
172
+ rerender(
173
+ <FaqList
174
+ faqs={faqs}
175
+ onFaqClick={vi.fn()}
176
+ avatarImage="https://cdn.example.com/avatar.jpg"
177
+ />
178
+ )
179
+ expect(screen.queryAllByTestId('avatar-ring')).toHaveLength(1)
180
+ })
181
+
182
+ it('passes the clicked FAQ id and marks only its matching row as loading', () => {
183
+ const onFaqClick = vi.fn()
184
+ renderWithProviders(
185
+ <FaqList faqs={faqs} onFaqClick={onFaqClick} loadingFaqId="second" />
186
+ )
187
+
188
+ const firstButton = screen.getByRole('button', {
189
+ name: 'How do I get started?',
190
+ })
191
+ const secondButton = screen.getByRole('button', {
192
+ name: 'What payment methods do you accept?',
193
+ })
194
+ expect(firstButton).toBeEnabled()
195
+ expect(secondButton).toBeDisabled()
196
+ expect(firstButton).not.toHaveClass('opacity-50', 'cursor-not-allowed')
197
+ expect(secondButton).toHaveClass('opacity-50', 'cursor-not-allowed')
198
+
199
+ fireEvent.click(firstButton)
200
+ fireEvent.click(secondButton)
201
+ expect(onFaqClick).toHaveBeenCalledTimes(1)
202
+ expect(onFaqClick).toHaveBeenCalledWith('first')
203
+ })
204
+ })