@youversion/platform-react-hooks 1.11.0 → 1.12.1
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +19 -0
- package/dist/__tests__/mocks/bibles.d.ts +22 -0
- package/dist/__tests__/mocks/bibles.d.ts.map +1 -0
- package/dist/__tests__/mocks/bibles.js +52 -0
- package/dist/__tests__/mocks/bibles.js.map +1 -0
- package/dist/useVersions.d.ts +11 -1
- package/dist/useVersions.d.ts.map +1 -1
- package/dist/useVersions.js +25 -1
- package/dist/useVersions.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/mocks/bibles.ts +56 -0
- package/src/context/ReaderProvider.test.tsx +264 -0
- package/src/context/VerseSelectionProvider.test.tsx +362 -0
- package/src/useVerse.test.tsx +228 -0
- package/src/useVerseSelection.test.tsx +33 -0
- package/src/useVerses.test.tsx +202 -0
- package/src/useVersion.test.tsx +242 -0
- package/src/useVersions.test.tsx +641 -0
- package/src/useVersions.ts +42 -4
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { renderHook, act } from '@testing-library/react';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { VerseSelectionProvider } from './VerseSelectionProvider';
|
|
5
|
+
import { useVerseSelection } from '../useVerseSelection';
|
|
6
|
+
|
|
7
|
+
// Wrapper for renderHook
|
|
8
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
9
|
+
<VerseSelectionProvider>{children}</VerseSelectionProvider>
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
describe('VerseSelectionProvider', () => {
|
|
13
|
+
describe('initial state', () => {
|
|
14
|
+
it('should provide an empty Set instance', () => {
|
|
15
|
+
const { result } = renderHook(() => useVerseSelection(), { wrapper });
|
|
16
|
+
|
|
17
|
+
expect(result.current.selectedVerseUsfms).toBeInstanceOf(Set);
|
|
18
|
+
expect(result.current.selectedVerseUsfms.size).toBe(0);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should have selectedCount of 0', () => {
|
|
22
|
+
const { result } = renderHook(() => useVerseSelection(), { wrapper });
|
|
23
|
+
|
|
24
|
+
expect(result.current.selectedCount).toBe(0);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should return false for isSelected on any verse', () => {
|
|
28
|
+
const { result } = renderHook(() => useVerseSelection(), { wrapper });
|
|
29
|
+
|
|
30
|
+
expect(result.current.isSelected('MAT.1.1')).toBe(false);
|
|
31
|
+
expect(result.current.isSelected('GEN.1.1')).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('toggleVerse', () => {
|
|
36
|
+
it('should add verse to selection when not present', () => {
|
|
37
|
+
const { result } = renderHook(() => useVerseSelection(), { wrapper });
|
|
38
|
+
|
|
39
|
+
act(() => {
|
|
40
|
+
result.current.toggleVerse('MAT.1.1');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(result.current.selectedVerseUsfms.has('MAT.1.1')).toBe(true);
|
|
44
|
+
expect(result.current.selectedCount).toBe(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should remove verse from selection when already present', () => {
|
|
48
|
+
const { result } = renderHook(() => useVerseSelection(), { wrapper });
|
|
49
|
+
|
|
50
|
+
act(() => {
|
|
51
|
+
result.current.toggleVerse('MAT.1.1');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(result.current.selectedCount).toBe(1);
|
|
55
|
+
|
|
56
|
+
act(() => {
|
|
57
|
+
result.current.toggleVerse('MAT.1.1');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(result.current.selectedVerseUsfms.has('MAT.1.1')).toBe(false);
|
|
61
|
+
expect(result.current.selectedCount).toBe(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should support multiple verses selected simultaneously', () => {
|
|
65
|
+
const { result } = renderHook(() => useVerseSelection(), { wrapper });
|
|
66
|
+
|
|
67
|
+
act(() => {
|
|
68
|
+
result.current.toggleVerse('MAT.1.1');
|
|
69
|
+
result.current.toggleVerse('GEN.1.1');
|
|
70
|
+
result.current.toggleVerse('JHN.3.16');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(result.current.selectedCount).toBe(3);
|
|
74
|
+
expect(result.current.selectedVerseUsfms.has('MAT.1.1')).toBe(true);
|
|
75
|
+
expect(result.current.selectedVerseUsfms.has('GEN.1.1')).toBe(true);
|
|
76
|
+
expect(result.current.selectedVerseUsfms.has('JHN.3.16')).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should create new Set reference on each update (immutability)', () => {
|
|
80
|
+
const { result } = renderHook(() => useVerseSelection(), { wrapper });
|
|
81
|
+
|
|
82
|
+
const set1 = result.current.selectedVerseUsfms;
|
|
83
|
+
|
|
84
|
+
act(() => {
|
|
85
|
+
result.current.toggleVerse('MAT.1.1');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const set2 = result.current.selectedVerseUsfms;
|
|
89
|
+
|
|
90
|
+
expect(set1).not.toBe(set2);
|
|
91
|
+
expect(set1.size).toBe(0);
|
|
92
|
+
expect(set2.size).toBe(1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should create new Set reference when removing a verse', () => {
|
|
96
|
+
const { result } = renderHook(() => useVerseSelection(), { wrapper });
|
|
97
|
+
|
|
98
|
+
act(() => {
|
|
99
|
+
result.current.toggleVerse('MAT.1.1');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const setWithVerse = result.current.selectedVerseUsfms;
|
|
103
|
+
|
|
104
|
+
act(() => {
|
|
105
|
+
result.current.toggleVerse('MAT.1.1');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const setWithoutVerse = result.current.selectedVerseUsfms;
|
|
109
|
+
|
|
110
|
+
expect(setWithVerse).not.toBe(setWithoutVerse);
|
|
111
|
+
expect(setWithVerse.size).toBe(1);
|
|
112
|
+
expect(setWithoutVerse.size).toBe(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should not add duplicate verses', () => {
|
|
116
|
+
const { result } = renderHook(() => useVerseSelection(), { wrapper });
|
|
117
|
+
|
|
118
|
+
act(() => {
|
|
119
|
+
result.current.toggleVerse('MAT.1.1');
|
|
120
|
+
result.current.toggleVerse('MAT.1.1');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Should toggle off, not add twice
|
|
124
|
+
expect(result.current.selectedCount).toBe(0);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('isSelected', () => {
|
|
129
|
+
it('should return true for selected verses', () => {
|
|
130
|
+
const { result } = renderHook(() => useVerseSelection(), { wrapper });
|
|
131
|
+
|
|
132
|
+
act(() => {
|
|
133
|
+
result.current.toggleVerse('MAT.1.1');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(result.current.isSelected('MAT.1.1')).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should return false for unselected verses', () => {
|
|
140
|
+
const { result } = renderHook(() => useVerseSelection(), { wrapper });
|
|
141
|
+
|
|
142
|
+
act(() => {
|
|
143
|
+
result.current.toggleVerse('MAT.1.1');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(result.current.isSelected('GEN.1.1')).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should work correctly after toggle operations', () => {
|
|
150
|
+
const { result } = renderHook(() => useVerseSelection(), { wrapper });
|
|
151
|
+
|
|
152
|
+
// Initially not selected
|
|
153
|
+
expect(result.current.isSelected('MAT.1.1')).toBe(false);
|
|
154
|
+
|
|
155
|
+
// Add verse - now selected
|
|
156
|
+
act(() => {
|
|
157
|
+
result.current.toggleVerse('MAT.1.1');
|
|
158
|
+
});
|
|
159
|
+
expect(result.current.isSelected('MAT.1.1')).toBe(true);
|
|
160
|
+
|
|
161
|
+
// Remove verse - not selected again
|
|
162
|
+
act(() => {
|
|
163
|
+
result.current.toggleVerse('MAT.1.1');
|
|
164
|
+
});
|
|
165
|
+
expect(result.current.isSelected('MAT.1.1')).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('clearSelection', () => {
|
|
170
|
+
it('should clear all selected verses', () => {
|
|
171
|
+
const { result } = renderHook(() => useVerseSelection(), { wrapper });
|
|
172
|
+
|
|
173
|
+
act(() => {
|
|
174
|
+
result.current.toggleVerse('MAT.1.1');
|
|
175
|
+
result.current.toggleVerse('GEN.1.1');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(result.current.selectedCount).toBe(2);
|
|
179
|
+
|
|
180
|
+
act(() => {
|
|
181
|
+
result.current.clearSelection();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(result.current.selectedCount).toBe(0);
|
|
185
|
+
expect(result.current.selectedVerseUsfms.size).toBe(0);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should create new Set reference', () => {
|
|
189
|
+
const { result } = renderHook(() => useVerseSelection(), { wrapper });
|
|
190
|
+
|
|
191
|
+
act(() => {
|
|
192
|
+
result.current.toggleVerse('MAT.1.1');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const setBeforeClear = result.current.selectedVerseUsfms;
|
|
196
|
+
|
|
197
|
+
act(() => {
|
|
198
|
+
result.current.clearSelection();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const setAfterClear = result.current.selectedVerseUsfms;
|
|
202
|
+
|
|
203
|
+
expect(setBeforeClear).not.toBe(setAfterClear);
|
|
204
|
+
expect(setBeforeClear.size).toBe(1);
|
|
205
|
+
expect(setAfterClear.size).toBe(0);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should work when already empty', () => {
|
|
209
|
+
const { result } = renderHook(() => useVerseSelection(), { wrapper });
|
|
210
|
+
|
|
211
|
+
const setBeforeClear = result.current.selectedVerseUsfms;
|
|
212
|
+
|
|
213
|
+
act(() => {
|
|
214
|
+
result.current.clearSelection();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const setAfterClear = result.current.selectedVerseUsfms;
|
|
218
|
+
|
|
219
|
+
expect(setBeforeClear).not.toBe(setAfterClear);
|
|
220
|
+
expect(setAfterClear.size).toBe(0);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('selectedCount', () => {
|
|
225
|
+
it('should start at 0', () => {
|
|
226
|
+
const { result } = renderHook(() => useVerseSelection(), { wrapper });
|
|
227
|
+
|
|
228
|
+
expect(result.current.selectedCount).toBe(0);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should increment when verse added', () => {
|
|
232
|
+
const { result } = renderHook(() => useVerseSelection(), { wrapper });
|
|
233
|
+
|
|
234
|
+
act(() => {
|
|
235
|
+
result.current.toggleVerse('MAT.1.1');
|
|
236
|
+
});
|
|
237
|
+
expect(result.current.selectedCount).toBe(1);
|
|
238
|
+
|
|
239
|
+
act(() => {
|
|
240
|
+
result.current.toggleVerse('GEN.1.1');
|
|
241
|
+
});
|
|
242
|
+
expect(result.current.selectedCount).toBe(2);
|
|
243
|
+
|
|
244
|
+
act(() => {
|
|
245
|
+
result.current.toggleVerse('JHN.3.16');
|
|
246
|
+
});
|
|
247
|
+
expect(result.current.selectedCount).toBe(3);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should decrement when verse removed', () => {
|
|
251
|
+
const { result } = renderHook(() => useVerseSelection(), { wrapper });
|
|
252
|
+
|
|
253
|
+
act(() => {
|
|
254
|
+
result.current.toggleVerse('MAT.1.1');
|
|
255
|
+
result.current.toggleVerse('GEN.1.1');
|
|
256
|
+
});
|
|
257
|
+
expect(result.current.selectedCount).toBe(2);
|
|
258
|
+
|
|
259
|
+
act(() => {
|
|
260
|
+
result.current.toggleVerse('MAT.1.1');
|
|
261
|
+
});
|
|
262
|
+
expect(result.current.selectedCount).toBe(1);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should return to 0 after clear', () => {
|
|
266
|
+
const { result } = renderHook(() => useVerseSelection(), { wrapper });
|
|
267
|
+
|
|
268
|
+
act(() => {
|
|
269
|
+
result.current.toggleVerse('MAT.1.1');
|
|
270
|
+
result.current.toggleVerse('GEN.1.1');
|
|
271
|
+
result.current.toggleVerse('JHN.3.16');
|
|
272
|
+
});
|
|
273
|
+
expect(result.current.selectedCount).toBe(3);
|
|
274
|
+
|
|
275
|
+
act(() => {
|
|
276
|
+
result.current.clearSelection();
|
|
277
|
+
});
|
|
278
|
+
expect(result.current.selectedCount).toBe(0);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('callback stability', () => {
|
|
283
|
+
it('should have stable toggleVerse reference across renders', () => {
|
|
284
|
+
const { result, rerender } = renderHook(() => useVerseSelection(), { wrapper });
|
|
285
|
+
|
|
286
|
+
const toggleRef1 = result.current.toggleVerse;
|
|
287
|
+
|
|
288
|
+
rerender();
|
|
289
|
+
|
|
290
|
+
const toggleRef2 = result.current.toggleVerse;
|
|
291
|
+
|
|
292
|
+
expect(toggleRef1).toBe(toggleRef2);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should have stable clearSelection reference across renders', () => {
|
|
296
|
+
const { result, rerender } = renderHook(() => useVerseSelection(), { wrapper });
|
|
297
|
+
|
|
298
|
+
const clearRef1 = result.current.clearSelection;
|
|
299
|
+
|
|
300
|
+
rerender();
|
|
301
|
+
|
|
302
|
+
const clearRef2 = result.current.clearSelection;
|
|
303
|
+
|
|
304
|
+
expect(clearRef1).toBe(clearRef2);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should have stable toggleVerse reference after state changes', () => {
|
|
308
|
+
const { result } = renderHook(() => useVerseSelection(), { wrapper });
|
|
309
|
+
|
|
310
|
+
const toggleRef1 = result.current.toggleVerse;
|
|
311
|
+
|
|
312
|
+
act(() => {
|
|
313
|
+
result.current.toggleVerse('MAT.1.1');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const toggleRef2 = result.current.toggleVerse;
|
|
317
|
+
|
|
318
|
+
expect(toggleRef1).toBe(toggleRef2);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should have stable clearSelection reference after state changes', () => {
|
|
322
|
+
const { result } = renderHook(() => useVerseSelection(), { wrapper });
|
|
323
|
+
|
|
324
|
+
const clearRef1 = result.current.clearSelection;
|
|
325
|
+
|
|
326
|
+
act(() => {
|
|
327
|
+
result.current.toggleVerse('MAT.1.1');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const clearRef2 = result.current.clearSelection;
|
|
331
|
+
|
|
332
|
+
expect(clearRef1).toBe(clearRef2);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should update isSelected reference when selectedVerseUsfms changes', () => {
|
|
336
|
+
const { result } = renderHook(() => useVerseSelection(), { wrapper });
|
|
337
|
+
|
|
338
|
+
const isSelectedRef1 = result.current.isSelected;
|
|
339
|
+
|
|
340
|
+
act(() => {
|
|
341
|
+
result.current.toggleVerse('MAT.1.1');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const isSelectedRef2 = result.current.isSelected;
|
|
345
|
+
|
|
346
|
+
// isSelected has selectedVerseUsfms in its dependency array, so it should change
|
|
347
|
+
expect(isSelectedRef1).not.toBe(isSelectedRef2);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should have stable isSelected reference when no state changes', () => {
|
|
351
|
+
const { result, rerender } = renderHook(() => useVerseSelection(), { wrapper });
|
|
352
|
+
|
|
353
|
+
const isSelectedRef1 = result.current.isSelected;
|
|
354
|
+
|
|
355
|
+
rerender();
|
|
356
|
+
|
|
357
|
+
const isSelectedRef2 = result.current.isSelected;
|
|
358
|
+
|
|
359
|
+
expect(isSelectedRef1).toBe(isSelectedRef2);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
});
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { renderHook, waitFor, act } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { useVerse } from './useVerse';
|
|
5
|
+
import { YouVersionContext } from './context';
|
|
6
|
+
import { type BibleClient, type BibleVerse } from '@youversion/platform-core';
|
|
7
|
+
import { useBibleClient } from './useBibleClient';
|
|
8
|
+
|
|
9
|
+
vi.mock('./useBibleClient');
|
|
10
|
+
|
|
11
|
+
describe('useVerse', () => {
|
|
12
|
+
const mockAppKey = 'test-app-key';
|
|
13
|
+
const mockGetVerse = vi.fn();
|
|
14
|
+
|
|
15
|
+
const mockVerse: BibleVerse = {
|
|
16
|
+
id: '1',
|
|
17
|
+
passage_id: 'MAT.1.1',
|
|
18
|
+
title: '1',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const createWrapper = (contextValue: { appKey: string }) => {
|
|
22
|
+
return ({ children }: { children: ReactNode }) => (
|
|
23
|
+
<YouVersionContext.Provider value={contextValue}>{children}</YouVersionContext.Provider>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.resetAllMocks();
|
|
29
|
+
|
|
30
|
+
mockGetVerse.mockResolvedValue(mockVerse);
|
|
31
|
+
|
|
32
|
+
const mockClient: Partial<BibleClient> = { getVerse: mockGetVerse };
|
|
33
|
+
vi.mocked(useBibleClient).mockReturnValue(mockClient as BibleClient);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('fetching verse', () => {
|
|
37
|
+
it('should fetch verse with all 4 parameters', async () => {
|
|
38
|
+
const wrapper = createWrapper({
|
|
39
|
+
appKey: mockAppKey,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const { result } = renderHook(() => useVerse(111, 'MAT', 1, 1), { wrapper });
|
|
43
|
+
|
|
44
|
+
expect(result.current.loading).toBe(true);
|
|
45
|
+
expect(result.current.verse).toBe(null);
|
|
46
|
+
|
|
47
|
+
await waitFor(() => {
|
|
48
|
+
expect(result.current.loading).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(mockGetVerse).toHaveBeenCalledWith(111, 'MAT', 1, 1);
|
|
52
|
+
expect(result.current.verse).toEqual(mockVerse);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should refetch when versionId changes', async () => {
|
|
56
|
+
const wrapper = createWrapper({
|
|
57
|
+
appKey: mockAppKey,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const { result, rerender } = renderHook(({ versionId }) => useVerse(versionId, 'MAT', 1, 1), {
|
|
61
|
+
wrapper,
|
|
62
|
+
initialProps: { versionId: 1 },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await waitFor(() => {
|
|
66
|
+
expect(result.current.loading).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(mockGetVerse).toHaveBeenCalledTimes(1);
|
|
70
|
+
expect(mockGetVerse).toHaveBeenLastCalledWith(1, 'MAT', 1, 1);
|
|
71
|
+
|
|
72
|
+
act(() => {
|
|
73
|
+
rerender({ versionId: 111 });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await waitFor(() => {
|
|
77
|
+
expect(result.current.loading).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(mockGetVerse).toHaveBeenCalledTimes(2);
|
|
81
|
+
expect(mockGetVerse).toHaveBeenLastCalledWith(111, 'MAT', 1, 1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should refetch when book changes', async () => {
|
|
85
|
+
const wrapper = createWrapper({
|
|
86
|
+
appKey: mockAppKey,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const { result, rerender } = renderHook(({ book }) => useVerse(1, book, 1, 1), {
|
|
90
|
+
wrapper,
|
|
91
|
+
initialProps: { book: 'MAT' },
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await waitFor(() => {
|
|
95
|
+
expect(result.current.loading).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(mockGetVerse).toHaveBeenCalledTimes(1);
|
|
99
|
+
expect(mockGetVerse).toHaveBeenLastCalledWith(1, 'MAT', 1, 1);
|
|
100
|
+
|
|
101
|
+
act(() => {
|
|
102
|
+
rerender({ book: 'GEN' });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await waitFor(() => {
|
|
106
|
+
expect(result.current.loading).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(mockGetVerse).toHaveBeenCalledTimes(2);
|
|
110
|
+
expect(mockGetVerse).toHaveBeenLastCalledWith(1, 'GEN', 1, 1);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should refetch when chapter changes', async () => {
|
|
114
|
+
const wrapper = createWrapper({
|
|
115
|
+
appKey: mockAppKey,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const { result, rerender } = renderHook(({ chapter }) => useVerse(1, 'MAT', chapter, 1), {
|
|
119
|
+
wrapper,
|
|
120
|
+
initialProps: { chapter: 1 },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await waitFor(() => {
|
|
124
|
+
expect(result.current.loading).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(mockGetVerse).toHaveBeenCalledTimes(1);
|
|
128
|
+
expect(mockGetVerse).toHaveBeenLastCalledWith(1, 'MAT', 1, 1);
|
|
129
|
+
|
|
130
|
+
act(() => {
|
|
131
|
+
rerender({ chapter: 5 });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await waitFor(() => {
|
|
135
|
+
expect(result.current.loading).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(mockGetVerse).toHaveBeenCalledTimes(2);
|
|
139
|
+
expect(mockGetVerse).toHaveBeenLastCalledWith(1, 'MAT', 5, 1);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should refetch when verse changes', async () => {
|
|
143
|
+
const wrapper = createWrapper({
|
|
144
|
+
appKey: mockAppKey,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const { result, rerender } = renderHook(({ verse }) => useVerse(1, 'MAT', 1, verse), {
|
|
148
|
+
wrapper,
|
|
149
|
+
initialProps: { verse: 1 },
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await waitFor(() => {
|
|
153
|
+
expect(result.current.loading).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(mockGetVerse).toHaveBeenCalledTimes(1);
|
|
157
|
+
expect(mockGetVerse).toHaveBeenLastCalledWith(1, 'MAT', 1, 1);
|
|
158
|
+
|
|
159
|
+
act(() => {
|
|
160
|
+
rerender({ verse: 10 });
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await waitFor(() => {
|
|
164
|
+
expect(result.current.loading).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(mockGetVerse).toHaveBeenCalledTimes(2);
|
|
168
|
+
expect(mockGetVerse).toHaveBeenLastCalledWith(1, 'MAT', 1, 10);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should not fetch when enabled is false', async () => {
|
|
172
|
+
const wrapper = createWrapper({
|
|
173
|
+
appKey: mockAppKey,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const { result } = renderHook(() => useVerse(1, 'MAT', 1, 1, { enabled: false }), {
|
|
177
|
+
wrapper,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await waitFor(() => {
|
|
181
|
+
expect(result.current.loading).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(mockGetVerse).not.toHaveBeenCalled();
|
|
185
|
+
expect(result.current.verse).toBe(null);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should handle fetch errors', async () => {
|
|
189
|
+
const error = new Error('Failed to fetch verse');
|
|
190
|
+
mockGetVerse.mockRejectedValueOnce(error);
|
|
191
|
+
|
|
192
|
+
const wrapper = createWrapper({
|
|
193
|
+
appKey: mockAppKey,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const { result } = renderHook(() => useVerse(1, 'MAT', 1, 1), { wrapper });
|
|
197
|
+
|
|
198
|
+
await waitFor(() => {
|
|
199
|
+
expect(result.current.loading).toBe(false);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(result.current.error).toEqual(error);
|
|
203
|
+
expect(result.current.verse).toBe(null);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should support manual refetch', async () => {
|
|
207
|
+
const wrapper = createWrapper({
|
|
208
|
+
appKey: mockAppKey,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const { result } = renderHook(() => useVerse(1, 'MAT', 1, 1), { wrapper });
|
|
212
|
+
|
|
213
|
+
await waitFor(() => {
|
|
214
|
+
expect(result.current.loading).toBe(false);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(mockGetVerse).toHaveBeenCalledTimes(1);
|
|
218
|
+
|
|
219
|
+
act(() => {
|
|
220
|
+
result.current.refetch();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
await waitFor(() => {
|
|
224
|
+
expect(mockGetVerse).toHaveBeenCalledTimes(2);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { renderHook } from '@testing-library/react';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { useVerseSelection } from './useVerseSelection';
|
|
5
|
+
import { VerseSelectionProvider } from './context/VerseSelectionProvider';
|
|
6
|
+
|
|
7
|
+
// Wrapper for renderHook
|
|
8
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
9
|
+
<VerseSelectionProvider>{children}</VerseSelectionProvider>
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
describe('useVerseSelection', () => {
|
|
13
|
+
it('should throw error when used outside provider', () => {
|
|
14
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(vi.fn());
|
|
15
|
+
|
|
16
|
+
expect(() => {
|
|
17
|
+
renderHook(() => useVerseSelection());
|
|
18
|
+
}).toThrow('useVerseSelection must be used within a VerseSelectionProvider');
|
|
19
|
+
|
|
20
|
+
consoleSpy.mockRestore();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should return expected shape with correct initial values', () => {
|
|
24
|
+
const { result } = renderHook(() => useVerseSelection(), { wrapper });
|
|
25
|
+
|
|
26
|
+
expect(result.current.selectedVerseUsfms).toBeInstanceOf(Set);
|
|
27
|
+
expect(result.current.selectedVerseUsfms.size).toBe(0);
|
|
28
|
+
expect(result.current.selectedCount).toBe(0);
|
|
29
|
+
expect(typeof result.current.toggleVerse).toBe('function');
|
|
30
|
+
expect(typeof result.current.isSelected).toBe('function');
|
|
31
|
+
expect(typeof result.current.clearSelection).toBe('function');
|
|
32
|
+
});
|
|
33
|
+
});
|