stream-chat-react-native-core 9.3.1-beta.4 → 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 (144) hide show
  1. package/lib/commonjs/components/Message/MessageItemView/MessageContent.js +4 -0
  2. package/lib/commonjs/components/Message/MessageItemView/MessageContent.js.map +1 -1
  3. package/lib/commonjs/components/Poll/Poll.js +21 -1
  4. package/lib/commonjs/components/Poll/Poll.js.map +1 -1
  5. package/lib/commonjs/components/Poll/components/PollButtons.js +39 -55
  6. package/lib/commonjs/components/Poll/components/PollButtons.js.map +1 -1
  7. package/lib/commonjs/components/Poll/components/PollOption.js +6 -19
  8. package/lib/commonjs/components/Poll/components/PollOption.js.map +1 -1
  9. package/lib/commonjs/components/Poll/contexts/PollUIStateContext.js +147 -0
  10. package/lib/commonjs/components/Poll/contexts/PollUIStateContext.js.map +1 -0
  11. package/lib/commonjs/components/Poll/contexts/index.js +15 -0
  12. package/lib/commonjs/components/Poll/contexts/index.js.map +1 -0
  13. package/lib/commonjs/components/Poll/hooks/useEndVote.js +48 -0
  14. package/lib/commonjs/components/Poll/hooks/useEndVote.js.map +1 -0
  15. package/lib/commonjs/components/Poll/hooks/usePollAccessibilityActions.js +153 -0
  16. package/lib/commonjs/components/Poll/hooks/usePollAccessibilityActions.js.map +1 -0
  17. package/lib/commonjs/components/Poll/hooks/usePollAccessibilityLabel.js +64 -0
  18. package/lib/commonjs/components/Poll/hooks/usePollAccessibilityLabel.js.map +1 -0
  19. package/lib/commonjs/components/Poll/hooks/usePollState.js +2 -35
  20. package/lib/commonjs/components/Poll/hooks/usePollState.js.map +1 -1
  21. package/lib/commonjs/components/Poll/hooks/usePollVoteToggle.js +41 -0
  22. package/lib/commonjs/components/Poll/hooks/usePollVoteToggle.js.map +1 -0
  23. package/lib/commonjs/components/UIComponents/BottomSheetModal.js +40 -0
  24. package/lib/commonjs/components/UIComponents/BottomSheetModal.js.map +1 -1
  25. package/lib/commonjs/contexts/overlayContext/MessageOverlayHostLayer.js +15 -0
  26. package/lib/commonjs/contexts/overlayContext/MessageOverlayHostLayer.js.map +1 -1
  27. package/lib/commonjs/i18n/ar.json +9 -1
  28. package/lib/commonjs/i18n/en.json +8 -0
  29. package/lib/commonjs/i18n/es.json +9 -1
  30. package/lib/commonjs/i18n/fr.json +9 -1
  31. package/lib/commonjs/i18n/he.json +9 -1
  32. package/lib/commonjs/i18n/hi.json +9 -1
  33. package/lib/commonjs/i18n/it.json +9 -1
  34. package/lib/commonjs/i18n/ja.json +9 -1
  35. package/lib/commonjs/i18n/ko.json +9 -1
  36. package/lib/commonjs/i18n/nl.json +9 -1
  37. package/lib/commonjs/i18n/pt-br.json +9 -1
  38. package/lib/commonjs/i18n/ru.json +9 -1
  39. package/lib/commonjs/i18n/tr.json +9 -1
  40. package/lib/commonjs/version.json +1 -1
  41. package/lib/module/components/Message/MessageItemView/MessageContent.js +4 -0
  42. package/lib/module/components/Message/MessageItemView/MessageContent.js.map +1 -1
  43. package/lib/module/components/Poll/Poll.js +21 -1
  44. package/lib/module/components/Poll/Poll.js.map +1 -1
  45. package/lib/module/components/Poll/components/PollButtons.js +39 -55
  46. package/lib/module/components/Poll/components/PollButtons.js.map +1 -1
  47. package/lib/module/components/Poll/components/PollOption.js +6 -19
  48. package/lib/module/components/Poll/components/PollOption.js.map +1 -1
  49. package/lib/module/components/Poll/contexts/PollUIStateContext.js +147 -0
  50. package/lib/module/components/Poll/contexts/PollUIStateContext.js.map +1 -0
  51. package/lib/module/components/Poll/contexts/index.js +15 -0
  52. package/lib/module/components/Poll/contexts/index.js.map +1 -0
  53. package/lib/module/components/Poll/hooks/useEndVote.js +48 -0
  54. package/lib/module/components/Poll/hooks/useEndVote.js.map +1 -0
  55. package/lib/module/components/Poll/hooks/usePollAccessibilityActions.js +153 -0
  56. package/lib/module/components/Poll/hooks/usePollAccessibilityActions.js.map +1 -0
  57. package/lib/module/components/Poll/hooks/usePollAccessibilityLabel.js +64 -0
  58. package/lib/module/components/Poll/hooks/usePollAccessibilityLabel.js.map +1 -0
  59. package/lib/module/components/Poll/hooks/usePollState.js +2 -35
  60. package/lib/module/components/Poll/hooks/usePollState.js.map +1 -1
  61. package/lib/module/components/Poll/hooks/usePollVoteToggle.js +41 -0
  62. package/lib/module/components/Poll/hooks/usePollVoteToggle.js.map +1 -0
  63. package/lib/module/components/UIComponents/BottomSheetModal.js +40 -0
  64. package/lib/module/components/UIComponents/BottomSheetModal.js.map +1 -1
  65. package/lib/module/contexts/overlayContext/MessageOverlayHostLayer.js +15 -0
  66. package/lib/module/contexts/overlayContext/MessageOverlayHostLayer.js.map +1 -1
  67. package/lib/module/i18n/ar.json +9 -1
  68. package/lib/module/i18n/en.json +8 -0
  69. package/lib/module/i18n/es.json +9 -1
  70. package/lib/module/i18n/fr.json +9 -1
  71. package/lib/module/i18n/he.json +9 -1
  72. package/lib/module/i18n/hi.json +9 -1
  73. package/lib/module/i18n/it.json +9 -1
  74. package/lib/module/i18n/ja.json +9 -1
  75. package/lib/module/i18n/ko.json +9 -1
  76. package/lib/module/i18n/nl.json +9 -1
  77. package/lib/module/i18n/pt-br.json +9 -1
  78. package/lib/module/i18n/ru.json +9 -1
  79. package/lib/module/i18n/tr.json +9 -1
  80. package/lib/module/version.json +1 -1
  81. package/lib/typescript/components/Message/MessageItemView/MessageContent.d.ts.map +1 -1
  82. package/lib/typescript/components/Poll/Poll.d.ts.map +1 -1
  83. package/lib/typescript/components/Poll/components/PollButtons.d.ts.map +1 -1
  84. package/lib/typescript/components/Poll/components/PollOption.d.ts.map +1 -1
  85. package/lib/typescript/components/Poll/contexts/PollUIStateContext.d.ts +31 -0
  86. package/lib/typescript/components/Poll/contexts/PollUIStateContext.d.ts.map +1 -0
  87. package/lib/typescript/components/Poll/contexts/index.d.ts +2 -0
  88. package/lib/typescript/components/Poll/contexts/index.d.ts.map +1 -0
  89. package/lib/typescript/components/Poll/hooks/useEndVote.d.ts +7 -0
  90. package/lib/typescript/components/Poll/hooks/useEndVote.d.ts.map +1 -0
  91. package/lib/typescript/components/Poll/hooks/usePollAccessibilityActions.d.ts +25 -0
  92. package/lib/typescript/components/Poll/hooks/usePollAccessibilityActions.d.ts.map +1 -0
  93. package/lib/typescript/components/Poll/hooks/usePollAccessibilityLabel.d.ts +8 -0
  94. package/lib/typescript/components/Poll/hooks/usePollAccessibilityLabel.d.ts.map +1 -0
  95. package/lib/typescript/components/Poll/hooks/usePollState.d.ts.map +1 -1
  96. package/lib/typescript/components/Poll/hooks/usePollVoteToggle.d.ts +8 -0
  97. package/lib/typescript/components/Poll/hooks/usePollVoteToggle.d.ts.map +1 -0
  98. package/lib/typescript/components/UIComponents/BottomSheetModal.d.ts.map +1 -1
  99. package/lib/typescript/contexts/overlayContext/MessageOverlayHostLayer.d.ts.map +1 -1
  100. package/lib/typescript/i18n/ar.json +9 -1
  101. package/lib/typescript/i18n/en.json +8 -0
  102. package/lib/typescript/i18n/es.json +9 -1
  103. package/lib/typescript/i18n/fr.json +9 -1
  104. package/lib/typescript/i18n/he.json +9 -1
  105. package/lib/typescript/i18n/hi.json +9 -1
  106. package/lib/typescript/i18n/it.json +9 -1
  107. package/lib/typescript/i18n/ja.json +9 -1
  108. package/lib/typescript/i18n/ko.json +9 -1
  109. package/lib/typescript/i18n/nl.json +9 -1
  110. package/lib/typescript/i18n/pt-br.json +9 -1
  111. package/lib/typescript/i18n/ru.json +9 -1
  112. package/lib/typescript/i18n/tr.json +9 -1
  113. package/lib/typescript/utils/i18n/Streami18n.d.ts +8 -0
  114. package/lib/typescript/utils/i18n/Streami18n.d.ts.map +1 -1
  115. package/package.json +1 -1
  116. package/src/components/Message/MessageItemView/MessageContent.tsx +4 -0
  117. package/src/components/Poll/Poll.tsx +29 -2
  118. package/src/components/Poll/components/PollButtons.tsx +37 -44
  119. package/src/components/Poll/components/PollOption.tsx +4 -13
  120. package/src/components/Poll/contexts/PollUIStateContext.tsx +105 -0
  121. package/src/components/Poll/contexts/index.ts +1 -0
  122. package/src/components/Poll/hooks/__tests__/usePollAccessibilityActions.test.tsx +358 -0
  123. package/src/components/Poll/hooks/__tests__/usePollAccessibilityLabel.test.tsx +142 -0
  124. package/src/components/Poll/hooks/useEndVote.ts +37 -0
  125. package/src/components/Poll/hooks/usePollAccessibilityActions.ts +191 -0
  126. package/src/components/Poll/hooks/usePollAccessibilityLabel.ts +75 -0
  127. package/src/components/Poll/hooks/usePollState.ts +3 -26
  128. package/src/components/Poll/hooks/usePollVoteToggle.ts +34 -0
  129. package/src/components/UIComponents/BottomSheetModal.tsx +44 -1
  130. package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx +17 -1
  131. package/src/i18n/ar.json +9 -1
  132. package/src/i18n/en.json +8 -0
  133. package/src/i18n/es.json +9 -1
  134. package/src/i18n/fr.json +9 -1
  135. package/src/i18n/he.json +9 -1
  136. package/src/i18n/hi.json +9 -1
  137. package/src/i18n/it.json +9 -1
  138. package/src/i18n/ja.json +9 -1
  139. package/src/i18n/ko.json +9 -1
  140. package/src/i18n/nl.json +9 -1
  141. package/src/i18n/pt-br.json +9 -1
  142. package/src/i18n/ru.json +9 -1
  143. package/src/i18n/tr.json +9 -1
  144. 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
+ };