stream-chat-react-native-core 9.3.1-beta.3 → 9.3.1-beta.5

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 (177) hide show
  1. package/lib/commonjs/components/Message/MessageItemView/MessageBubble.js +2 -3
  2. package/lib/commonjs/components/Message/MessageItemView/MessageBubble.js.map +1 -1
  3. package/lib/commonjs/components/Message/MessageItemView/MessageContent.js +22 -29
  4. package/lib/commonjs/components/Message/MessageItemView/MessageContent.js.map +1 -1
  5. package/lib/commonjs/components/Message/MessageItemView/MessageWrapper.js +2 -3
  6. package/lib/commonjs/components/Message/MessageItemView/MessageWrapper.js.map +1 -1
  7. package/lib/commonjs/components/Message/MessageItemView/utils/renderText.js +24 -0
  8. package/lib/commonjs/components/Message/MessageItemView/utils/renderText.js.map +1 -1
  9. package/lib/commonjs/components/Message/hooks/useMessageDeliveryData.js +10 -5
  10. package/lib/commonjs/components/Message/hooks/useMessageDeliveryData.js.map +1 -1
  11. package/lib/commonjs/components/Message/hooks/useMessageReadData.js +10 -5
  12. package/lib/commonjs/components/Message/hooks/useMessageReadData.js.map +1 -1
  13. package/lib/commonjs/components/Poll/Poll.js +21 -1
  14. package/lib/commonjs/components/Poll/Poll.js.map +1 -1
  15. package/lib/commonjs/components/Poll/components/PollButtons.js +39 -55
  16. package/lib/commonjs/components/Poll/components/PollButtons.js.map +1 -1
  17. package/lib/commonjs/components/Poll/components/PollOption.js +6 -19
  18. package/lib/commonjs/components/Poll/components/PollOption.js.map +1 -1
  19. package/lib/commonjs/components/Poll/contexts/PollUIStateContext.js +147 -0
  20. package/lib/commonjs/components/Poll/contexts/PollUIStateContext.js.map +1 -0
  21. package/lib/commonjs/components/Poll/contexts/index.js +15 -0
  22. package/lib/commonjs/components/Poll/contexts/index.js.map +1 -0
  23. package/lib/commonjs/components/Poll/hooks/useEndVote.js +48 -0
  24. package/lib/commonjs/components/Poll/hooks/useEndVote.js.map +1 -0
  25. package/lib/commonjs/components/Poll/hooks/usePollAccessibilityActions.js +153 -0
  26. package/lib/commonjs/components/Poll/hooks/usePollAccessibilityActions.js.map +1 -0
  27. package/lib/commonjs/components/Poll/hooks/usePollAccessibilityLabel.js +64 -0
  28. package/lib/commonjs/components/Poll/hooks/usePollAccessibilityLabel.js.map +1 -0
  29. package/lib/commonjs/components/Poll/hooks/usePollState.js +2 -35
  30. package/lib/commonjs/components/Poll/hooks/usePollState.js.map +1 -1
  31. package/lib/commonjs/components/Poll/hooks/usePollVoteToggle.js +41 -0
  32. package/lib/commonjs/components/Poll/hooks/usePollVoteToggle.js.map +1 -0
  33. package/lib/commonjs/components/UIComponents/BottomSheetModal.js +40 -0
  34. package/lib/commonjs/components/UIComponents/BottomSheetModal.js.map +1 -1
  35. package/lib/commonjs/contexts/overlayContext/MessageOverlayHostLayer.js +15 -0
  36. package/lib/commonjs/contexts/overlayContext/MessageOverlayHostLayer.js.map +1 -1
  37. package/lib/commonjs/i18n/ar.json +9 -1
  38. package/lib/commonjs/i18n/en.json +8 -0
  39. package/lib/commonjs/i18n/es.json +9 -1
  40. package/lib/commonjs/i18n/fr.json +9 -1
  41. package/lib/commonjs/i18n/he.json +9 -1
  42. package/lib/commonjs/i18n/hi.json +9 -1
  43. package/lib/commonjs/i18n/it.json +9 -1
  44. package/lib/commonjs/i18n/ja.json +9 -1
  45. package/lib/commonjs/i18n/ko.json +9 -1
  46. package/lib/commonjs/i18n/nl.json +9 -1
  47. package/lib/commonjs/i18n/pt-br.json +9 -1
  48. package/lib/commonjs/i18n/ru.json +9 -1
  49. package/lib/commonjs/i18n/tr.json +9 -1
  50. package/lib/commonjs/version.json +1 -1
  51. package/lib/module/components/Message/MessageItemView/MessageBubble.js +2 -3
  52. package/lib/module/components/Message/MessageItemView/MessageBubble.js.map +1 -1
  53. package/lib/module/components/Message/MessageItemView/MessageContent.js +22 -29
  54. package/lib/module/components/Message/MessageItemView/MessageContent.js.map +1 -1
  55. package/lib/module/components/Message/MessageItemView/MessageWrapper.js +2 -3
  56. package/lib/module/components/Message/MessageItemView/MessageWrapper.js.map +1 -1
  57. package/lib/module/components/Message/MessageItemView/utils/renderText.js +24 -0
  58. package/lib/module/components/Message/MessageItemView/utils/renderText.js.map +1 -1
  59. package/lib/module/components/Message/hooks/useMessageDeliveryData.js +10 -5
  60. package/lib/module/components/Message/hooks/useMessageDeliveryData.js.map +1 -1
  61. package/lib/module/components/Message/hooks/useMessageReadData.js +10 -5
  62. package/lib/module/components/Message/hooks/useMessageReadData.js.map +1 -1
  63. package/lib/module/components/Poll/Poll.js +21 -1
  64. package/lib/module/components/Poll/Poll.js.map +1 -1
  65. package/lib/module/components/Poll/components/PollButtons.js +39 -55
  66. package/lib/module/components/Poll/components/PollButtons.js.map +1 -1
  67. package/lib/module/components/Poll/components/PollOption.js +6 -19
  68. package/lib/module/components/Poll/components/PollOption.js.map +1 -1
  69. package/lib/module/components/Poll/contexts/PollUIStateContext.js +147 -0
  70. package/lib/module/components/Poll/contexts/PollUIStateContext.js.map +1 -0
  71. package/lib/module/components/Poll/contexts/index.js +15 -0
  72. package/lib/module/components/Poll/contexts/index.js.map +1 -0
  73. package/lib/module/components/Poll/hooks/useEndVote.js +48 -0
  74. package/lib/module/components/Poll/hooks/useEndVote.js.map +1 -0
  75. package/lib/module/components/Poll/hooks/usePollAccessibilityActions.js +153 -0
  76. package/lib/module/components/Poll/hooks/usePollAccessibilityActions.js.map +1 -0
  77. package/lib/module/components/Poll/hooks/usePollAccessibilityLabel.js +64 -0
  78. package/lib/module/components/Poll/hooks/usePollAccessibilityLabel.js.map +1 -0
  79. package/lib/module/components/Poll/hooks/usePollState.js +2 -35
  80. package/lib/module/components/Poll/hooks/usePollState.js.map +1 -1
  81. package/lib/module/components/Poll/hooks/usePollVoteToggle.js +41 -0
  82. package/lib/module/components/Poll/hooks/usePollVoteToggle.js.map +1 -0
  83. package/lib/module/components/UIComponents/BottomSheetModal.js +40 -0
  84. package/lib/module/components/UIComponents/BottomSheetModal.js.map +1 -1
  85. package/lib/module/contexts/overlayContext/MessageOverlayHostLayer.js +15 -0
  86. package/lib/module/contexts/overlayContext/MessageOverlayHostLayer.js.map +1 -1
  87. package/lib/module/i18n/ar.json +9 -1
  88. package/lib/module/i18n/en.json +8 -0
  89. package/lib/module/i18n/es.json +9 -1
  90. package/lib/module/i18n/fr.json +9 -1
  91. package/lib/module/i18n/he.json +9 -1
  92. package/lib/module/i18n/hi.json +9 -1
  93. package/lib/module/i18n/it.json +9 -1
  94. package/lib/module/i18n/ja.json +9 -1
  95. package/lib/module/i18n/ko.json +9 -1
  96. package/lib/module/i18n/nl.json +9 -1
  97. package/lib/module/i18n/pt-br.json +9 -1
  98. package/lib/module/i18n/ru.json +9 -1
  99. package/lib/module/i18n/tr.json +9 -1
  100. package/lib/module/version.json +1 -1
  101. package/lib/typescript/components/Message/MessageItemView/MessageBubble.d.ts +1 -1
  102. package/lib/typescript/components/Message/MessageItemView/MessageBubble.d.ts.map +1 -1
  103. package/lib/typescript/components/Message/MessageItemView/MessageContent.d.ts.map +1 -1
  104. package/lib/typescript/components/Message/MessageItemView/MessageWrapper.d.ts +1 -1
  105. package/lib/typescript/components/Message/MessageItemView/MessageWrapper.d.ts.map +1 -1
  106. package/lib/typescript/components/Message/MessageItemView/utils/renderText.d.ts.map +1 -1
  107. package/lib/typescript/components/Message/hooks/useMessageDeliveryData.d.ts.map +1 -1
  108. package/lib/typescript/components/Message/hooks/useMessageReadData.d.ts.map +1 -1
  109. package/lib/typescript/components/Poll/Poll.d.ts.map +1 -1
  110. package/lib/typescript/components/Poll/components/PollButtons.d.ts.map +1 -1
  111. package/lib/typescript/components/Poll/components/PollOption.d.ts.map +1 -1
  112. package/lib/typescript/components/Poll/contexts/PollUIStateContext.d.ts +31 -0
  113. package/lib/typescript/components/Poll/contexts/PollUIStateContext.d.ts.map +1 -0
  114. package/lib/typescript/components/Poll/contexts/index.d.ts +2 -0
  115. package/lib/typescript/components/Poll/contexts/index.d.ts.map +1 -0
  116. package/lib/typescript/components/Poll/hooks/useEndVote.d.ts +7 -0
  117. package/lib/typescript/components/Poll/hooks/useEndVote.d.ts.map +1 -0
  118. package/lib/typescript/components/Poll/hooks/usePollAccessibilityActions.d.ts +25 -0
  119. package/lib/typescript/components/Poll/hooks/usePollAccessibilityActions.d.ts.map +1 -0
  120. package/lib/typescript/components/Poll/hooks/usePollAccessibilityLabel.d.ts +8 -0
  121. package/lib/typescript/components/Poll/hooks/usePollAccessibilityLabel.d.ts.map +1 -0
  122. package/lib/typescript/components/Poll/hooks/usePollState.d.ts.map +1 -1
  123. package/lib/typescript/components/Poll/hooks/usePollVoteToggle.d.ts +8 -0
  124. package/lib/typescript/components/Poll/hooks/usePollVoteToggle.d.ts.map +1 -0
  125. package/lib/typescript/components/UIComponents/BottomSheetModal.d.ts.map +1 -1
  126. package/lib/typescript/contexts/overlayContext/MessageOverlayHostLayer.d.ts.map +1 -1
  127. package/lib/typescript/i18n/ar.json +9 -1
  128. package/lib/typescript/i18n/en.json +8 -0
  129. package/lib/typescript/i18n/es.json +9 -1
  130. package/lib/typescript/i18n/fr.json +9 -1
  131. package/lib/typescript/i18n/he.json +9 -1
  132. package/lib/typescript/i18n/hi.json +9 -1
  133. package/lib/typescript/i18n/it.json +9 -1
  134. package/lib/typescript/i18n/ja.json +9 -1
  135. package/lib/typescript/i18n/ko.json +9 -1
  136. package/lib/typescript/i18n/nl.json +9 -1
  137. package/lib/typescript/i18n/pt-br.json +9 -1
  138. package/lib/typescript/i18n/ru.json +9 -1
  139. package/lib/typescript/i18n/tr.json +9 -1
  140. package/lib/typescript/utils/i18n/Streami18n.d.ts +8 -0
  141. package/lib/typescript/utils/i18n/Streami18n.d.ts.map +1 -1
  142. package/package.json +1 -1
  143. package/src/components/Message/MessageItemView/MessageBubble.tsx +3 -1
  144. package/src/components/Message/MessageItemView/MessageContent.tsx +36 -36
  145. package/src/components/Message/MessageItemView/MessageWrapper.tsx +1 -1
  146. package/src/components/Message/MessageItemView/utils/renderText.tsx +31 -0
  147. package/src/components/Message/hooks/useMessageDeliveryData.ts +11 -6
  148. package/src/components/Message/hooks/useMessageReadData.ts +11 -6
  149. package/src/components/Poll/Poll.tsx +29 -2
  150. package/src/components/Poll/components/PollButtons.tsx +37 -44
  151. package/src/components/Poll/components/PollOption.tsx +4 -13
  152. package/src/components/Poll/contexts/PollUIStateContext.tsx +105 -0
  153. package/src/components/Poll/contexts/index.ts +1 -0
  154. package/src/components/Poll/hooks/__tests__/usePollAccessibilityActions.test.tsx +358 -0
  155. package/src/components/Poll/hooks/__tests__/usePollAccessibilityLabel.test.tsx +142 -0
  156. package/src/components/Poll/hooks/useEndVote.ts +37 -0
  157. package/src/components/Poll/hooks/usePollAccessibilityActions.ts +191 -0
  158. package/src/components/Poll/hooks/usePollAccessibilityLabel.ts +75 -0
  159. package/src/components/Poll/hooks/usePollState.ts +3 -26
  160. package/src/components/Poll/hooks/usePollVoteToggle.ts +34 -0
  161. package/src/components/Thread/__tests__/__snapshots__/Thread.test.tsx.snap +4 -4
  162. package/src/components/UIComponents/BottomSheetModal.tsx +44 -1
  163. package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx +17 -1
  164. package/src/i18n/ar.json +9 -1
  165. package/src/i18n/en.json +8 -0
  166. package/src/i18n/es.json +9 -1
  167. package/src/i18n/fr.json +9 -1
  168. package/src/i18n/he.json +9 -1
  169. package/src/i18n/hi.json +9 -1
  170. package/src/i18n/it.json +9 -1
  171. package/src/i18n/ja.json +9 -1
  172. package/src/i18n/ko.json +9 -1
  173. package/src/i18n/nl.json +9 -1
  174. package/src/i18n/pt-br.json +9 -1
  175. package/src/i18n/ru.json +9 -1
  176. package/src/i18n/tr.json +9 -1
  177. package/src/version.json +1 -1
@@ -0,0 +1,358 @@
1
+ import React from 'react';
2
+
3
+ import type { AccessibilityActionEvent } from 'react-native';
4
+
5
+ import { act, renderHook } from '@testing-library/react-native';
6
+
7
+ import { AccessibilityProvider } from '../../../../contexts/accessibilityContext/AccessibilityContext';
8
+ import { TranslationProvider } from '../../../../contexts/translationContext/TranslationContext';
9
+ import { usePollAccessibilityActions } from '../usePollAccessibilityActions';
10
+
11
+ const mockOpenAddComment = jest.fn();
12
+ const mockOpenAllComments = jest.fn();
13
+ const mockOpenAllOptions = jest.fn();
14
+ const mockOpenSuggestOption = jest.fn();
15
+ const mockOpenViewResults = jest.fn();
16
+ const mockEndVote = jest.fn();
17
+ const mockToggleVote = jest.fn();
18
+
19
+ jest.mock('../../contexts/PollUIStateContext', () => ({
20
+ usePollUIStateContext: () => ({
21
+ openAddComment: mockOpenAddComment,
22
+ openAllComments: mockOpenAllComments,
23
+ openAllOptions: mockOpenAllOptions,
24
+ openSuggestOption: mockOpenSuggestOption,
25
+ openViewResults: mockOpenViewResults,
26
+ }),
27
+ }));
28
+
29
+ jest.mock('../usePollStateStore', () => ({
30
+ usePollStateStore: (selector: (state: unknown) => unknown) => selector(mockPollState),
31
+ }));
32
+
33
+ jest.mock('../useEndVote', () => ({
34
+ useEndVote: () => mockEndVote,
35
+ }));
36
+
37
+ jest.mock('../usePollVoteToggle', () => ({
38
+ usePollVoteToggle: () => mockToggleVote,
39
+ }));
40
+
41
+ const mockChatContext = { client: { userID: 'me' } };
42
+ const mockOwnCapabilities = { castPollVote: true };
43
+
44
+ jest.mock('../../../../contexts', () => {
45
+ const actual = jest.requireActual('../../../../contexts');
46
+ return {
47
+ ...actual,
48
+ useChatContext: () => mockChatContext,
49
+ useOwnCapabilitiesContext: () => mockOwnCapabilities,
50
+ };
51
+ });
52
+
53
+ let mockPollState: Record<string, unknown> = {};
54
+
55
+ const setPollState = (state: Record<string, unknown>) => {
56
+ mockPollState = state;
57
+ };
58
+
59
+ const setCastPollVote = (allowed: boolean) => {
60
+ mockOwnCapabilities.castPollVote = allowed;
61
+ };
62
+
63
+ const setUserID = (id: string) => {
64
+ mockChatContext.client.userID = id;
65
+ };
66
+
67
+ const t = (key: string, vars?: Record<string, unknown>) => {
68
+ if (!vars) return key;
69
+ if (key === 'a11y/Vote on {{option}}') return `Vote on ${vars.option}`;
70
+ return key;
71
+ };
72
+
73
+ const wrapper =
74
+ (enabled: boolean) =>
75
+ ({ children }: { children: React.ReactNode }) => (
76
+ <AccessibilityProvider value={{ enabled }}>
77
+ <TranslationProvider
78
+ value={
79
+ {
80
+ t,
81
+ tDateTimeParser: () => null,
82
+ } as never
83
+ }
84
+ >
85
+ {children}
86
+ </TranslationProvider>
87
+ </AccessibilityProvider>
88
+ );
89
+
90
+ const buildOption = (id: string, text: string) => ({ id, text });
91
+
92
+ const fireAction = (
93
+ handler: ((event: AccessibilityActionEvent) => void) | undefined,
94
+ actionName: string,
95
+ ) => {
96
+ handler?.({ nativeEvent: { actionName } } as AccessibilityActionEvent);
97
+ };
98
+
99
+ beforeEach(() => {
100
+ mockOpenAddComment.mockClear();
101
+ mockOpenAllComments.mockClear();
102
+ mockOpenAllOptions.mockClear();
103
+ mockOpenSuggestOption.mockClear();
104
+ mockOpenViewResults.mockClear();
105
+ mockEndVote.mockClear();
106
+ mockToggleVote.mockClear();
107
+ setCastPollVote(true);
108
+ setUserID('me');
109
+ });
110
+
111
+ describe('usePollAccessibilityActions', () => {
112
+ it('returns undefined when accessibility is disabled', () => {
113
+ setPollState({
114
+ allow_answers: true,
115
+ allow_user_suggested_options: true,
116
+ created_by: { id: 'me' },
117
+ is_closed: false,
118
+ options: [buildOption('o1', 'A')],
119
+ });
120
+
121
+ const { result } = renderHook(() => usePollAccessibilityActions(), {
122
+ wrapper: wrapper(false),
123
+ });
124
+
125
+ expect(result.current.accessibilityActions).toBeUndefined();
126
+ expect(result.current.onAccessibilityAction).toBeUndefined();
127
+ });
128
+
129
+ it('every action uses the same human label for name and label', () => {
130
+ setPollState({
131
+ allow_answers: true,
132
+ allow_user_suggested_options: true,
133
+ created_by: { id: 'me' },
134
+ is_closed: false,
135
+ options: [buildOption('o1', 'Pizza'), buildOption('o2', 'Pasta')],
136
+ });
137
+
138
+ const { result } = renderHook(() => usePollAccessibilityActions(), {
139
+ wrapper: wrapper(true),
140
+ });
141
+
142
+ const actions = result.current.accessibilityActions;
143
+ expect(actions).toBeDefined();
144
+ for (const action of actions ?? []) {
145
+ expect(action.name).toBe(action.label);
146
+ }
147
+ });
148
+
149
+ it('exposes only View Results for an ended poll', () => {
150
+ setPollState({
151
+ allow_answers: true,
152
+ allow_user_suggested_options: true,
153
+ created_by: { id: 'me' },
154
+ is_closed: true,
155
+ options: [buildOption('o1', 'A'), buildOption('o2', 'B')],
156
+ });
157
+
158
+ const { result } = renderHook(() => usePollAccessibilityActions(), {
159
+ wrapper: wrapper(true),
160
+ });
161
+
162
+ const labels = result.current.accessibilityActions?.map((a) => a.label);
163
+ expect(labels).toEqual(['View Results']);
164
+ });
165
+
166
+ it('lists vote actions with the option text, plus End vote / Add comment / Suggest option for creator', () => {
167
+ setPollState({
168
+ allow_answers: true,
169
+ allow_user_suggested_options: true,
170
+ created_by: { id: 'me' },
171
+ is_closed: false,
172
+ options: [buildOption('o1', 'Pizza'), buildOption('o2', 'Pasta')],
173
+ });
174
+
175
+ const { result } = renderHook(() => usePollAccessibilityActions(), {
176
+ wrapper: wrapper(true),
177
+ });
178
+
179
+ const labels = result.current.accessibilityActions?.map((a) => a.label);
180
+ expect(labels).toEqual([
181
+ 'View Results',
182
+ 'Vote on Pizza',
183
+ 'Vote on Pasta',
184
+ 'a11y/End vote',
185
+ 'Add a comment',
186
+ 'Suggest an option',
187
+ ]);
188
+ });
189
+
190
+ it('omits End vote when the current user is not the creator', () => {
191
+ setUserID('someone-else');
192
+ setPollState({
193
+ allow_answers: false,
194
+ allow_user_suggested_options: false,
195
+ created_by: { id: 'me' },
196
+ is_closed: false,
197
+ options: [buildOption('o1', 'Pizza')],
198
+ });
199
+
200
+ const { result } = renderHook(() => usePollAccessibilityActions(), {
201
+ wrapper: wrapper(true),
202
+ });
203
+
204
+ const labels = result.current.accessibilityActions?.map((a) => a.label);
205
+ expect(labels).toEqual(['View Results', 'Vote on Pizza']);
206
+ });
207
+
208
+ it('omits vote actions when the user lacks castPollVote capability', () => {
209
+ setCastPollVote(false);
210
+ setPollState({
211
+ allow_answers: true,
212
+ allow_user_suggested_options: false,
213
+ created_by: { id: 'somebody' },
214
+ is_closed: false,
215
+ options: [buildOption('o1', 'Pizza')],
216
+ });
217
+
218
+ const { result } = renderHook(() => usePollAccessibilityActions(), {
219
+ wrapper: wrapper(true),
220
+ });
221
+
222
+ const labels = result.current.accessibilityActions?.map((a) => a.label);
223
+ expect(labels?.some((l) => l?.startsWith('Vote on'))).toBe(false);
224
+ });
225
+
226
+ it('exposes "View N comments" when the poll has answers', () => {
227
+ setPollState({
228
+ allow_answers: false,
229
+ allow_user_suggested_options: false,
230
+ answers_count: 4,
231
+ created_by: { id: 'somebody' },
232
+ is_closed: true,
233
+ options: [buildOption('o1', 'A')],
234
+ });
235
+
236
+ const { result } = renderHook(() => usePollAccessibilityActions(), {
237
+ wrapper: wrapper(true),
238
+ });
239
+
240
+ const labels = result.current.accessibilityActions?.map((a) => a.label);
241
+ expect(labels).toContain('View {{count}} comments');
242
+ });
243
+
244
+ it('omits "View N comments" when there are no answers', () => {
245
+ setPollState({
246
+ allow_answers: false,
247
+ allow_user_suggested_options: false,
248
+ answers_count: 0,
249
+ created_by: { id: 'somebody' },
250
+ is_closed: true,
251
+ options: [buildOption('o1', 'A')],
252
+ });
253
+
254
+ const { result } = renderHook(() => usePollAccessibilityActions(), {
255
+ wrapper: wrapper(true),
256
+ });
257
+
258
+ const labels = result.current.accessibilityActions?.map((a) => a.label);
259
+ expect(labels?.some((l) => l?.includes('comments'))).toBe(false);
260
+ });
261
+
262
+ it('exposes Show all options when options exceed the visible cap', () => {
263
+ const manyOptions = Array.from({ length: 12 }, (_, i) => buildOption(`o${i}`, `Option ${i}`));
264
+ setPollState({
265
+ allow_answers: false,
266
+ allow_user_suggested_options: false,
267
+ created_by: { id: 'somebody' },
268
+ is_closed: true,
269
+ options: manyOptions,
270
+ });
271
+
272
+ const { result } = renderHook(() => usePollAccessibilityActions(), {
273
+ wrapper: wrapper(true),
274
+ });
275
+
276
+ const labels = result.current.accessibilityActions?.map((a) => a.label);
277
+ expect(labels).toContain('a11y/Show all options');
278
+ });
279
+
280
+ it('routes each action to the right side effect', () => {
281
+ setPollState({
282
+ allow_answers: true,
283
+ allow_user_suggested_options: true,
284
+ created_by: { id: 'me' },
285
+ is_closed: false,
286
+ options: [buildOption('o1', 'Pizza'), buildOption('o2', 'Pasta')],
287
+ });
288
+
289
+ const { result } = renderHook(() => usePollAccessibilityActions(), {
290
+ wrapper: wrapper(true),
291
+ });
292
+
293
+ act(() => {
294
+ fireAction(result.current.onAccessibilityAction, 'View Results');
295
+ });
296
+ expect(mockOpenViewResults).toHaveBeenCalledTimes(1);
297
+
298
+ act(() => {
299
+ fireAction(result.current.onAccessibilityAction, 'a11y/End vote');
300
+ });
301
+ expect(mockEndVote).toHaveBeenCalledTimes(1);
302
+
303
+ act(() => {
304
+ fireAction(result.current.onAccessibilityAction, 'Add a comment');
305
+ });
306
+ expect(mockOpenAddComment).toHaveBeenCalledTimes(1);
307
+
308
+ act(() => {
309
+ fireAction(result.current.onAccessibilityAction, 'Suggest an option');
310
+ });
311
+ expect(mockOpenSuggestOption).toHaveBeenCalledTimes(1);
312
+
313
+ act(() => {
314
+ fireAction(result.current.onAccessibilityAction, 'Vote on Pasta');
315
+ });
316
+ expect(mockToggleVote).toHaveBeenCalledWith('o2');
317
+ });
318
+
319
+ it('routes the "View N comments" action to openAllComments', () => {
320
+ setPollState({
321
+ allow_answers: false,
322
+ allow_user_suggested_options: false,
323
+ answers_count: 7,
324
+ created_by: { id: 'somebody' },
325
+ is_closed: true,
326
+ options: [buildOption('o1', 'A')],
327
+ });
328
+
329
+ const { result } = renderHook(() => usePollAccessibilityActions(), {
330
+ wrapper: wrapper(true),
331
+ });
332
+
333
+ act(() => {
334
+ fireAction(result.current.onAccessibilityAction, 'View {{count}} comments');
335
+ });
336
+ expect(mockOpenAllComments).toHaveBeenCalledTimes(1);
337
+ });
338
+
339
+ it('ignores unknown action names', () => {
340
+ setPollState({
341
+ allow_answers: true,
342
+ allow_user_suggested_options: true,
343
+ created_by: { id: 'me' },
344
+ is_closed: false,
345
+ options: [buildOption('o1', 'Pizza')],
346
+ });
347
+
348
+ const { result } = renderHook(() => usePollAccessibilityActions(), {
349
+ wrapper: wrapper(true),
350
+ });
351
+
352
+ act(() => {
353
+ fireAction(result.current.onAccessibilityAction, 'streamPollVoteOption_o1');
354
+ });
355
+ expect(mockToggleVote).not.toHaveBeenCalled();
356
+ expect(mockOpenViewResults).not.toHaveBeenCalled();
357
+ });
358
+ });
@@ -0,0 +1,142 @@
1
+ import React from 'react';
2
+
3
+ import { renderHook } from '@testing-library/react-native';
4
+
5
+ import { AccessibilityProvider } from '../../../../contexts/accessibilityContext/AccessibilityContext';
6
+ import { TranslationProvider } from '../../../../contexts/translationContext/TranslationContext';
7
+ import { usePollAccessibilityLabel } from '../usePollAccessibilityLabel';
8
+
9
+ jest.mock('../usePollStateStore', () => ({
10
+ usePollStateStore: (selector: (state: unknown) => unknown) => selector(mockPollState),
11
+ }));
12
+
13
+ let mockPollState: Record<string, unknown> = {};
14
+
15
+ const setPollState = (state: Record<string, unknown>) => {
16
+ mockPollState = state;
17
+ };
18
+
19
+ const t = (key: string, vars?: Record<string, unknown>) => {
20
+ if (!vars) return key;
21
+ if (key === '{{count}} votes') return `${vars.count} votes`;
22
+ if (key === 'Select up to {{count}}') return `Select up to ${vars.count}`;
23
+ if (key === '+{{count}} More Options') return `+${vars.count} More Options`;
24
+ return key;
25
+ };
26
+
27
+ const wrapper =
28
+ (enabled: boolean) =>
29
+ ({ children }: { children: React.ReactNode }) => (
30
+ <AccessibilityProvider value={{ enabled }}>
31
+ <TranslationProvider
32
+ value={
33
+ {
34
+ t,
35
+ tDateTimeParser: () => null,
36
+ } as never
37
+ }
38
+ >
39
+ {children}
40
+ </TranslationProvider>
41
+ </AccessibilityProvider>
42
+ );
43
+
44
+ const buildOption = (id: string, text: string) => ({ id, text });
45
+
46
+ describe('usePollAccessibilityLabel', () => {
47
+ it('returns undefined when accessibility is disabled', () => {
48
+ setPollState({
49
+ enforce_unique_vote: false,
50
+ is_closed: true,
51
+ max_votes_allowed: 0,
52
+ name: 'Lunch?',
53
+ options: [buildOption('o1', 'Pizza')],
54
+ vote_counts_by_option: { o1: 3 },
55
+ });
56
+
57
+ const { result } = renderHook(() => usePollAccessibilityLabel(), {
58
+ wrapper: wrapper(false),
59
+ });
60
+
61
+ expect(result.current).toBeUndefined();
62
+ });
63
+
64
+ it('builds composite label for an ended poll', () => {
65
+ setPollState({
66
+ enforce_unique_vote: false,
67
+ is_closed: true,
68
+ max_votes_allowed: 0,
69
+ name: 'Test',
70
+ options: [buildOption('o1', 'Option 1'), buildOption('o2', 'Option 2')],
71
+ vote_counts_by_option: { o1: 0, o2: 0 },
72
+ });
73
+
74
+ const { result } = renderHook(() => usePollAccessibilityLabel(), {
75
+ wrapper: wrapper(true),
76
+ });
77
+
78
+ expect(result.current).toBe(
79
+ 'Test, Poll has ended, Option 1: 0 votes, Option 2: 0 votes, a11y/Activate to view results',
80
+ );
81
+ });
82
+
83
+ it('uses "Select one" for an open enforce-unique-vote poll', () => {
84
+ setPollState({
85
+ enforce_unique_vote: true,
86
+ is_closed: false,
87
+ max_votes_allowed: 0,
88
+ name: 'Pick a venue',
89
+ options: [buildOption('o1', 'Cafe')],
90
+ vote_counts_by_option: { o1: 2 },
91
+ });
92
+
93
+ const { result } = renderHook(() => usePollAccessibilityLabel(), {
94
+ wrapper: wrapper(true),
95
+ });
96
+
97
+ expect(result.current).toBe(
98
+ 'Pick a venue, Select one, Cafe: 2 votes, a11y/Activate to view results',
99
+ );
100
+ });
101
+
102
+ it('uses "Select up to N" when maxVotesAllowed is set', () => {
103
+ setPollState({
104
+ enforce_unique_vote: false,
105
+ is_closed: false,
106
+ max_votes_allowed: 3,
107
+ name: 'Top picks',
108
+ options: [buildOption('o1', 'A')],
109
+ vote_counts_by_option: { o1: 1 },
110
+ });
111
+
112
+ const { result } = renderHook(() => usePollAccessibilityLabel(), {
113
+ wrapper: wrapper(true),
114
+ });
115
+
116
+ expect(result.current).toBe(
117
+ 'Top picks, Select up to 3, A: 1 votes, a11y/Activate to view results',
118
+ );
119
+ });
120
+
121
+ it('appends overflow hint when options exceed the visible cap', () => {
122
+ const manyOptions = Array.from({ length: 12 }, (_, i) => buildOption(`o${i}`, `Option ${i}`));
123
+ const counts = Object.fromEntries(manyOptions.map((o) => [o.id, 0]));
124
+
125
+ setPollState({
126
+ enforce_unique_vote: false,
127
+ is_closed: false,
128
+ max_votes_allowed: 0,
129
+ name: 'Big poll',
130
+ options: manyOptions,
131
+ vote_counts_by_option: counts,
132
+ });
133
+
134
+ const { result } = renderHook(() => usePollAccessibilityLabel(), {
135
+ wrapper: wrapper(true),
136
+ });
137
+
138
+ expect(result.current).toContain('+7 More Options');
139
+ expect(result.current).toContain('Option 0: 0 votes');
140
+ expect(result.current).not.toContain('Option 5:');
141
+ });
142
+ });
@@ -0,0 +1,37 @@
1
+ import { usePollContext, useTranslationContext } from '../../../contexts';
2
+ import { useStableCallback } from '../../../hooks';
3
+ import { useNotificationApi } from '../../Notifications';
4
+
5
+ /**
6
+ * Returns a stable callback that closes the current poll and emits a success or
7
+ * failure notification. Shared by `EndVoteButton` and the rotor accessibility
8
+ * action so both paths produce identical side effects.
9
+ */
10
+ export const useEndVote = () => {
11
+ const { poll } = usePollContext();
12
+ const { addNotification } = useNotificationApi();
13
+ const { t } = useTranslationContext();
14
+
15
+ return useStableCallback(async () => {
16
+ try {
17
+ const response = await poll.close();
18
+ addNotification({
19
+ message: t('Poll ended'),
20
+ options: { severity: 'success', type: 'api:poll:end:success' },
21
+ origin: { emitter: 'PollActions' },
22
+ });
23
+ return response;
24
+ } catch (error) {
25
+ addNotification({
26
+ message: t('Failed to end the poll'),
27
+ options: {
28
+ ...(error instanceof Error ? { originalError: error } : {}),
29
+ severity: 'error',
30
+ type: 'api:poll:end:failed',
31
+ },
32
+ origin: { emitter: 'PollActions' },
33
+ });
34
+ throw error;
35
+ }
36
+ });
37
+ };