@youversion/platform-react-hooks 1.18.1 → 1.20.0

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 (79) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +3 -7
  3. package/CHANGELOG.md +32 -0
  4. package/dist/__tests__/mocks/bibles.d.ts +6 -1
  5. package/dist/__tests__/mocks/bibles.d.ts.map +1 -1
  6. package/dist/__tests__/mocks/bibles.js +10 -0
  7. package/dist/__tests__/mocks/bibles.js.map +1 -1
  8. package/dist/__tests__/mocks/core-mock-factory.d.ts +83 -0
  9. package/dist/__tests__/mocks/core-mock-factory.d.ts.map +1 -0
  10. package/dist/__tests__/mocks/core-mock-factory.js +138 -0
  11. package/dist/__tests__/mocks/core-mock-factory.js.map +1 -0
  12. package/dist/context/ReaderContext.d.ts +6 -0
  13. package/dist/context/ReaderContext.d.ts.map +1 -1
  14. package/dist/context/ReaderContext.js +6 -0
  15. package/dist/context/ReaderContext.js.map +1 -1
  16. package/dist/context/ReaderProvider.d.ts +3 -0
  17. package/dist/context/ReaderProvider.d.ts.map +1 -1
  18. package/dist/context/ReaderProvider.js +3 -0
  19. package/dist/context/ReaderProvider.js.map +1 -1
  20. package/dist/context/VerseSelectionContext.d.ts +6 -0
  21. package/dist/context/VerseSelectionContext.d.ts.map +1 -1
  22. package/dist/context/VerseSelectionContext.js +3 -0
  23. package/dist/context/VerseSelectionContext.js.map +1 -1
  24. package/dist/context/VerseSelectionProvider.d.ts +3 -0
  25. package/dist/context/VerseSelectionProvider.d.ts.map +1 -1
  26. package/dist/context/VerseSelectionProvider.js +3 -0
  27. package/dist/context/VerseSelectionProvider.js.map +1 -1
  28. package/dist/test/utils.d.ts +7 -0
  29. package/dist/test/utils.d.ts.map +1 -0
  30. package/dist/test/utils.js +7 -0
  31. package/dist/test/utils.js.map +1 -0
  32. package/dist/useChapterNavigation.d.ts +3 -0
  33. package/dist/useChapterNavigation.d.ts.map +1 -1
  34. package/dist/useChapterNavigation.js +3 -0
  35. package/dist/useChapterNavigation.js.map +1 -1
  36. package/dist/useInitData.d.ts +4 -0
  37. package/dist/useInitData.d.ts.map +1 -1
  38. package/dist/useInitData.js +4 -0
  39. package/dist/useInitData.js.map +1 -1
  40. package/dist/useVerseSelection.d.ts +3 -0
  41. package/dist/useVerseSelection.d.ts.map +1 -1
  42. package/dist/useVerseSelection.js +3 -0
  43. package/dist/useVerseSelection.js.map +1 -1
  44. package/package.json +2 -2
  45. package/src/__tests__/mocks/bibles.ts +18 -1
  46. package/src/__tests__/mocks/core-mock-factory.ts +226 -0
  47. package/src/context/ReaderContext.tsx +6 -0
  48. package/src/context/ReaderProvider.tsx +3 -0
  49. package/src/context/VerseSelectionContext.tsx +6 -0
  50. package/src/context/VerseSelectionProvider.tsx +3 -0
  51. package/src/context/YouVersionAuthProvider.test.tsx +14 -131
  52. package/src/test/utils.tsx +12 -0
  53. package/src/useBibleClient.test.tsx +8 -37
  54. package/src/useBook.test.tsx +158 -0
  55. package/src/useBooks.test.tsx +148 -0
  56. package/src/useChapter.test.tsx +70 -128
  57. package/src/useChapterNavigation.ts +3 -0
  58. package/src/useChapters.test.tsx +80 -150
  59. package/src/useHighlights.test.tsx +33 -104
  60. package/src/useInitData.ts +4 -0
  61. package/src/useLanguage.test.tsx +8 -10
  62. package/src/useLanguageClient.test.tsx +9 -25
  63. package/src/useLanguages.test.tsx +27 -64
  64. package/src/usePassage.test.tsx +304 -0
  65. package/src/useTheme.test.tsx +32 -0
  66. package/src/useVOTD.test.tsx +28 -67
  67. package/src/useVerse.test.tsx +73 -149
  68. package/src/useVerseSelection.ts +3 -0
  69. package/src/useVerses.test.tsx +37 -104
  70. package/src/useVersion.test.tsx +29 -66
  71. package/src/useVersions.test.tsx +72 -154
  72. package/src/useYVAuth.test.tsx +26 -134
  73. package/src/utility/getDayOfYear.test.ts +48 -0
  74. package/vitest.config.ts +12 -0
  75. package/src/context/ReaderProvider.test.tsx +0 -264
  76. package/src/context/VerseSelectionProvider.test.tsx +0 -362
  77. package/src/useChapterNavigation.test.tsx +0 -160
  78. package/src/useVerseSelection.test.tsx +0 -33
  79. package/vitest.setup.ts +0 -1
@@ -26,8 +26,6 @@ describe('useLanguage', () => {
26
26
  };
27
27
 
28
28
  beforeEach(() => {
29
- vi.resetAllMocks();
30
-
31
29
  mockGetLanguage.mockResolvedValue(mockLanguage);
32
30
 
33
31
  const mockClient: Partial<LanguagesClient> = { getLanguage: mockGetLanguage };
@@ -45,8 +43,8 @@ describe('useLanguage', () => {
45
43
  expect(result.current.loading).toBe(false);
46
44
  });
47
45
 
48
- expect(mockGetLanguage).toHaveBeenCalledWith('en');
49
- expect(result.current.language).toEqual(mockLanguage);
46
+ expect.soft(mockGetLanguage).toHaveBeenCalledWith('en');
47
+ expect.soft(result.current.language).toEqual(mockLanguage);
50
48
  });
51
49
 
52
50
  it('should refetch when languageId changes', async () => {
@@ -58,8 +56,8 @@ describe('useLanguage', () => {
58
56
  expect(result.current.loading).toBe(false);
59
57
  });
60
58
 
61
- expect(mockGetLanguage).toHaveBeenCalledTimes(1);
62
- expect(mockGetLanguage).toHaveBeenCalledWith('en');
59
+ expect.soft(mockGetLanguage).toHaveBeenCalledTimes(1);
60
+ expect.soft(mockGetLanguage).toHaveBeenCalledWith('en');
63
61
 
64
62
  rerender({ languageId: 'es' });
65
63
 
@@ -77,8 +75,8 @@ describe('useLanguage', () => {
77
75
  expect(result.current.loading).toBe(false);
78
76
  });
79
77
 
80
- expect(mockGetLanguage).not.toHaveBeenCalled();
81
- expect(result.current.language).toBe(null);
78
+ expect.soft(mockGetLanguage).not.toHaveBeenCalled();
79
+ expect.soft(result.current.language).toBe(null);
82
80
  });
83
81
 
84
82
  it('should handle fetch errors', async () => {
@@ -91,8 +89,8 @@ describe('useLanguage', () => {
91
89
  expect(result.current.loading).toBe(false);
92
90
  });
93
91
 
94
- expect(result.current.error).toEqual(error);
95
- expect(result.current.language).toBe(null);
92
+ expect.soft(result.current.error).toEqual(error);
93
+ expect.soft(result.current.language).toBe(null);
96
94
  });
97
95
 
98
96
  it('should support manual refetch', async () => {
@@ -1,11 +1,11 @@
1
1
  import { renderHook } from '@testing-library/react';
2
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { describe, expect, vi, beforeEach, it } from 'vitest';
3
3
  import type { ReactNode } from 'react';
4
4
  import { useLanguagesClient } from './useLanguageClient';
5
5
  import { YouVersionContext } from './context';
6
6
  import { LanguagesClient, ApiClient } from '@youversion/platform-core';
7
+ import { createYVWrapper } from './test/utils';
7
8
 
8
- // Mock the core package
9
9
  vi.mock('@youversion/platform-core', async () => {
10
10
  const actual = await vi.importActual('@youversion/platform-core');
11
11
  return {
@@ -20,17 +20,7 @@ vi.mock('@youversion/platform-core', async () => {
20
20
  });
21
21
 
22
22
  describe('useLanguagesClient', () => {
23
- const mockAppKey = 'test-app-key';
24
-
25
- const createWrapper = (contextValue: { appKey: string }) => {
26
- return ({ children }: { children: ReactNode }) => (
27
- <YouVersionContext.Provider value={contextValue}>{children}</YouVersionContext.Provider>
28
- );
29
- };
30
-
31
23
  beforeEach(() => {
32
- vi.resetAllMocks();
33
-
34
24
  vi.mocked(LanguagesClient).mockImplementation(function () {
35
25
  const mockClient: Partial<LanguagesClient> = { getLanguages: vi.fn() };
36
26
  return mockClient;
@@ -50,9 +40,9 @@ describe('useLanguagesClient', () => {
50
40
  });
51
41
 
52
42
  it('should throw error when appKey is missing', () => {
53
- const wrapper = createWrapper({
54
- appKey: '',
55
- });
43
+ const wrapper = ({ children }: { children: ReactNode }) => (
44
+ <YouVersionContext.Provider value={{ appKey: '' }}>{children}</YouVersionContext.Provider>
45
+ );
56
46
 
57
47
  expect(() => renderHook(() => useLanguagesClient(), { wrapper })).toThrow(
58
48
  'YouVersion context not found. Make sure your component is wrapped with YouVersionProvider and an API key is provided.',
@@ -62,23 +52,17 @@ describe('useLanguagesClient', () => {
62
52
 
63
53
  describe('client creation', () => {
64
54
  it('should create LanguagesClient with correct ApiClient config', () => {
65
- const wrapper = createWrapper({
66
- appKey: mockAppKey,
67
- });
68
-
55
+ const wrapper = createYVWrapper();
69
56
  renderHook(() => useLanguagesClient(), { wrapper });
70
57
 
71
58
  expect(ApiClient).toHaveBeenCalledWith({
72
- appKey: mockAppKey,
59
+ appKey: 'test-app-key',
73
60
  });
74
61
  expect(LanguagesClient).toHaveBeenCalledWith(expect.objectContaining({ isApiClient: true }));
75
62
  });
76
63
 
77
64
  it('should memoize LanguagesClient instance', () => {
78
- const wrapper = createWrapper({
79
- appKey: mockAppKey,
80
- });
81
-
65
+ const wrapper = createYVWrapper();
82
66
  const { rerender } = renderHook(() => useLanguagesClient(), { wrapper });
83
67
 
84
68
  rerender();
@@ -87,7 +71,7 @@ describe('useLanguagesClient', () => {
87
71
  });
88
72
 
89
73
  it('should create new LanguagesClient when context values change', () => {
90
- let currentAppKey = mockAppKey;
74
+ let currentAppKey = 'test-app-key';
91
75
 
92
76
  const wrapper = ({ children }: { children: ReactNode }) => (
93
77
  <YouVersionContext.Provider
@@ -1,8 +1,6 @@
1
1
  import { renderHook, waitFor, act } from '@testing-library/react';
2
- import { describe, it, expect, vi, beforeEach } from 'vitest';
3
- import type { ReactNode } from 'react';
2
+ import { describe, expect, vi, beforeEach, it } from 'vitest';
4
3
  import { useLanguages } from './useLanguages';
5
- import { YouVersionContext } from './context';
6
4
  import {
7
5
  type LanguagesClient,
8
6
  type Collection,
@@ -10,11 +8,11 @@ import {
10
8
  type GetLanguagesOptions,
11
9
  } from '@youversion/platform-core';
12
10
  import { useLanguagesClient } from './useLanguageClient';
11
+ import { createYVWrapper } from './test/utils';
13
12
 
14
13
  vi.mock('./useLanguageClient');
15
14
 
16
15
  describe('useLanguages', () => {
17
- const mockAppKey = 'test-app-key';
18
16
  const mockGetLanguages = vi.fn();
19
17
 
20
18
  const mockLanguages: Collection<Language> = {
@@ -59,15 +57,7 @@ describe('useLanguages', () => {
59
57
  next_page_token: null,
60
58
  };
61
59
 
62
- const createWrapper = (contextValue: { appKey: string }) => {
63
- return ({ children }: { children: ReactNode }) => (
64
- <YouVersionContext.Provider value={contextValue}>{children}</YouVersionContext.Provider>
65
- );
66
- };
67
-
68
60
  beforeEach(() => {
69
- vi.resetAllMocks();
70
-
71
61
  mockGetLanguages.mockResolvedValue(mockLanguages);
72
62
 
73
63
  const mockClient: Partial<LanguagesClient> = { getLanguages: mockGetLanguages };
@@ -76,10 +66,7 @@ describe('useLanguages', () => {
76
66
 
77
67
  describe('fetching languages', () => {
78
68
  it('should fetch languages without country filter', async () => {
79
- const wrapper = createWrapper({
80
- appKey: mockAppKey,
81
- });
82
-
69
+ const wrapper = createYVWrapper();
83
70
  const { result } = renderHook(() => useLanguages(), { wrapper });
84
71
 
85
72
  expect(result.current.loading).toBe(true);
@@ -89,15 +76,12 @@ describe('useLanguages', () => {
89
76
  expect(result.current.loading).toBe(false);
90
77
  });
91
78
 
92
- expect(mockGetLanguages).toHaveBeenCalledWith({});
93
- expect(result.current.languages).toEqual(mockLanguages);
79
+ expect.soft(mockGetLanguages).toHaveBeenCalledWith({});
80
+ expect.soft(result.current.languages).toEqual(mockLanguages);
94
81
  });
95
82
 
96
83
  it('should fetch languages with provided country', async () => {
97
- const wrapper = createWrapper({
98
- appKey: mockAppKey,
99
- });
100
-
84
+ const wrapper = createYVWrapper();
101
85
  const { result } = renderHook(() => useLanguages({ country: 'US' }), { wrapper });
102
86
 
103
87
  expect(result.current.loading).toBe(true);
@@ -107,15 +91,12 @@ describe('useLanguages', () => {
107
91
  expect(result.current.loading).toBe(false);
108
92
  });
109
93
 
110
- expect(mockGetLanguages).toHaveBeenCalledWith({ country: 'US' });
111
- expect(result.current.languages).toEqual(mockLanguages);
94
+ expect.soft(mockGetLanguages).toHaveBeenCalledWith({ country: 'US' });
95
+ expect.soft(result.current.languages).toEqual(mockLanguages);
112
96
  });
113
97
 
114
98
  it('should fetch languages with all options', async () => {
115
- const wrapper = createWrapper({
116
- appKey: mockAppKey,
117
- });
118
-
99
+ const wrapper = createYVWrapper();
119
100
  const options: GetLanguagesOptions = {
120
101
  country: 'US',
121
102
  page_size: 10,
@@ -128,15 +109,12 @@ describe('useLanguages', () => {
128
109
  expect(result.current.loading).toBe(false);
129
110
  });
130
111
 
131
- expect(mockGetLanguages).toHaveBeenCalledWith(options);
132
- expect(result.current.languages).toEqual(mockLanguages);
112
+ expect.soft(mockGetLanguages).toHaveBeenCalledWith(options);
113
+ expect.soft(result.current.languages).toEqual(mockLanguages);
133
114
  });
134
115
 
135
116
  it('should refetch when options change', async () => {
136
- const wrapper = createWrapper({
137
- appKey: mockAppKey,
138
- });
139
-
117
+ const wrapper = createYVWrapper();
140
118
  const { result, rerender } = renderHook(({ options }) => useLanguages(options), {
141
119
  wrapper,
142
120
  initialProps: { options: { country: 'US' } },
@@ -154,15 +132,12 @@ describe('useLanguages', () => {
154
132
  expect(result.current.loading).toBe(false);
155
133
  });
156
134
 
157
- expect(mockGetLanguages).toHaveBeenCalledTimes(2);
158
- expect(mockGetLanguages).toHaveBeenLastCalledWith({ country: 'ES' });
135
+ expect.soft(mockGetLanguages).toHaveBeenCalledTimes(2);
136
+ expect.soft(mockGetLanguages).toHaveBeenLastCalledWith({ country: 'ES' });
159
137
  });
160
138
 
161
139
  it('should not fetch when enabled is false', async () => {
162
- const wrapper = createWrapper({
163
- appKey: mockAppKey,
164
- });
165
-
140
+ const wrapper = createYVWrapper();
166
141
  const { result } = renderHook(() => useLanguages({ country: 'US' }, { enabled: false }), {
167
142
  wrapper,
168
143
  });
@@ -171,33 +146,27 @@ describe('useLanguages', () => {
171
146
  expect(result.current.loading).toBe(false);
172
147
  });
173
148
 
174
- expect(mockGetLanguages).not.toHaveBeenCalled();
175
- expect(result.current.languages).toBe(null);
149
+ expect.soft(mockGetLanguages).not.toHaveBeenCalled();
150
+ expect.soft(result.current.languages).toBe(null);
176
151
  });
177
152
 
178
153
  it('should handle fetch errors', async () => {
154
+ const wrapper = createYVWrapper();
179
155
  const error = new Error('Failed to fetch languages');
180
156
  mockGetLanguages.mockRejectedValueOnce(error);
181
157
 
182
- const wrapper = createWrapper({
183
- appKey: mockAppKey,
184
- });
185
-
186
158
  const { result } = renderHook(() => useLanguages({ country: 'US' }), { wrapper });
187
159
 
188
160
  await waitFor(() => {
189
161
  expect(result.current.loading).toBe(false);
190
162
  });
191
163
 
192
- expect(result.current.error).toEqual(error);
193
- expect(result.current.languages).toBe(null);
164
+ expect.soft(result.current.error).toEqual(error);
165
+ expect.soft(result.current.languages).toBe(null);
194
166
  });
195
167
 
196
168
  it('should support manual refetch', async () => {
197
- const wrapper = createWrapper({
198
- appKey: mockAppKey,
199
- });
200
-
169
+ const wrapper = createYVWrapper();
201
170
  const { result } = renderHook(() => useLanguages({ country: 'US' }), { wrapper });
202
171
 
203
172
  await waitFor(() => {
@@ -216,10 +185,7 @@ describe('useLanguages', () => {
216
185
  });
217
186
 
218
187
  it('should fetch languages with fields filter', async () => {
219
- const wrapper = createWrapper({
220
- appKey: mockAppKey,
221
- });
222
-
188
+ const wrapper = createYVWrapper();
223
189
  const options: GetLanguagesOptions = {
224
190
  fields: ['id', 'language', 'script'],
225
191
  page_size: '*',
@@ -231,15 +197,12 @@ describe('useLanguages', () => {
231
197
  expect(result.current.loading).toBe(false);
232
198
  });
233
199
 
234
- expect(mockGetLanguages).toHaveBeenCalledWith(options);
235
- expect(result.current.languages).toEqual(mockLanguages);
200
+ expect.soft(mockGetLanguages).toHaveBeenCalledWith(options);
201
+ expect.soft(result.current.languages).toEqual(mockLanguages);
236
202
  });
237
203
 
238
204
  it('should refetch when fields change', async () => {
239
- const wrapper = createWrapper({
240
- appKey: mockAppKey,
241
- });
242
-
205
+ const wrapper = createYVWrapper();
243
206
  const { result, rerender } = renderHook(({ options }) => useLanguages(options), {
244
207
  wrapper,
245
208
  initialProps: { options: { fields: ['id', 'language'] } as GetLanguagesOptions },
@@ -257,8 +220,8 @@ describe('useLanguages', () => {
257
220
  expect(result.current.loading).toBe(false);
258
221
  });
259
222
 
260
- expect(mockGetLanguages).toHaveBeenCalledTimes(2);
261
- expect(mockGetLanguages).toHaveBeenLastCalledWith({
223
+ expect.soft(mockGetLanguages).toHaveBeenCalledTimes(2);
224
+ expect.soft(mockGetLanguages).toHaveBeenLastCalledWith({
262
225
  fields: ['id', 'language', 'script'],
263
226
  });
264
227
  });
@@ -0,0 +1,304 @@
1
+ import { renderHook, waitFor, act } from '@testing-library/react';
2
+ import { describe, expect, vi, beforeEach, it } from 'vitest';
3
+ import { usePassage } from './usePassage';
4
+ import { type BibleClient } from '@youversion/platform-core';
5
+ import { useBibleClient } from './useBibleClient';
6
+ import { createYVWrapper } from './test/utils';
7
+ import { createMockPassage } from './__tests__/mocks/bibles';
8
+
9
+ vi.mock('./useBibleClient');
10
+
11
+ describe('usePassage', () => {
12
+ const mockGetPassage = vi.fn();
13
+ const mockPassage = createMockPassage();
14
+
15
+ beforeEach(() => {
16
+ mockGetPassage.mockResolvedValue(mockPassage);
17
+
18
+ const mockClient: Partial<BibleClient> = { getPassage: mockGetPassage };
19
+ vi.mocked(useBibleClient).mockReturnValue(mockClient as BibleClient);
20
+ });
21
+
22
+ describe('basic fetching', () => {
23
+ it('should show loading then passage data', async () => {
24
+ const wrapper = createYVWrapper();
25
+ const { result } = renderHook(() => usePassage({ versionId: 3034, usfm: 'JHN.3.16' }), {
26
+ wrapper,
27
+ });
28
+
29
+ expect(result.current.loading).toBe(true);
30
+ expect(result.current.passage).toBe(null);
31
+
32
+ await waitFor(() => {
33
+ expect(result.current.loading).toBe(false);
34
+ });
35
+
36
+ expect.soft(result.current.passage).toEqual(mockPassage);
37
+ });
38
+
39
+ it('should call getPassage with correct default args', async () => {
40
+ const wrapper = createYVWrapper();
41
+ const { result } = renderHook(() => usePassage({ versionId: 3034, usfm: 'JHN.3.16' }), {
42
+ wrapper,
43
+ });
44
+
45
+ await waitFor(() => {
46
+ expect(result.current.loading).toBe(false);
47
+ });
48
+
49
+ expect.soft(mockGetPassage).toHaveBeenCalledWith(3034, 'JHN.3.16', 'html', false, false);
50
+ });
51
+ });
52
+
53
+ describe('USFM validation', () => {
54
+ it.each(['', 'undefined', 'null'])('should skip fetch when usfm is "%s"', async (usfm) => {
55
+ const wrapper = createYVWrapper();
56
+ const { result } = renderHook(() => usePassage({ versionId: 3034, usfm }), { wrapper });
57
+
58
+ await waitFor(() => {
59
+ expect(result.current.loading).toBe(false);
60
+ });
61
+
62
+ expect.soft(mockGetPassage).not.toHaveBeenCalled();
63
+ expect.soft(result.current.passage).toBe(null);
64
+ });
65
+ });
66
+
67
+ describe('format options', () => {
68
+ it('should fetch with format text when specified', async () => {
69
+ const wrapper = createYVWrapper();
70
+ const { result } = renderHook(
71
+ () => usePassage({ versionId: 3034, usfm: 'JHN.3.16', format: 'text' }),
72
+ { wrapper },
73
+ );
74
+
75
+ await waitFor(() => {
76
+ expect(result.current.loading).toBe(false);
77
+ });
78
+
79
+ expect.soft(mockGetPassage).toHaveBeenCalledWith(3034, 'JHN.3.16', 'text', false, false);
80
+ });
81
+ });
82
+
83
+ describe('heading and notes options', () => {
84
+ it('should pass include_headings=true', async () => {
85
+ const wrapper = createYVWrapper();
86
+ const { result } = renderHook(
87
+ () => usePassage({ versionId: 3034, usfm: 'JHN.3.16', include_headings: true }),
88
+ { wrapper },
89
+ );
90
+
91
+ await waitFor(() => {
92
+ expect(result.current.loading).toBe(false);
93
+ });
94
+
95
+ expect.soft(mockGetPassage).toHaveBeenCalledWith(3034, 'JHN.3.16', 'html', true, false);
96
+ });
97
+
98
+ it('should pass include_notes=true', async () => {
99
+ const wrapper = createYVWrapper();
100
+ const { result } = renderHook(
101
+ () => usePassage({ versionId: 3034, usfm: 'JHN.3.16', include_notes: true }),
102
+ { wrapper },
103
+ );
104
+
105
+ await waitFor(() => {
106
+ expect(result.current.loading).toBe(false);
107
+ });
108
+
109
+ expect.soft(mockGetPassage).toHaveBeenCalledWith(3034, 'JHN.3.16', 'html', false, true);
110
+ });
111
+
112
+ it('should pass all options combined', async () => {
113
+ const wrapper = createYVWrapper();
114
+ const { result } = renderHook(
115
+ () =>
116
+ usePassage({
117
+ versionId: 3034,
118
+ usfm: 'JHN.3',
119
+ format: 'text',
120
+ include_headings: true,
121
+ include_notes: true,
122
+ }),
123
+ { wrapper },
124
+ );
125
+
126
+ await waitFor(() => {
127
+ expect(result.current.loading).toBe(false);
128
+ });
129
+
130
+ expect.soft(mockGetPassage).toHaveBeenCalledWith(3034, 'JHN.3', 'text', true, true);
131
+ });
132
+ });
133
+
134
+ describe('parameter change refetching', () => {
135
+ it.each([
136
+ {
137
+ param: 'versionId',
138
+ HookFn: ({ val }: { val: number | string }) =>
139
+ usePassage({ versionId: val as number, usfm: 'JHN.3.16' }),
140
+ initial: { val: 1 },
141
+ updated: { val: 3034 },
142
+ expectedInitial: [1, 'JHN.3.16', 'html', false, false],
143
+ expectedUpdated: [3034, 'JHN.3.16', 'html', false, false],
144
+ },
145
+ {
146
+ param: 'usfm',
147
+ HookFn: ({ val }: { val: number | string }) =>
148
+ usePassage({ versionId: 3034, usfm: val as string }),
149
+ initial: { val: 'JHN.3.16' },
150
+ updated: { val: 'GEN.1.1' },
151
+ expectedInitial: [3034, 'JHN.3.16', 'html', false, false],
152
+ expectedUpdated: [3034, 'GEN.1.1', 'html', false, false],
153
+ },
154
+ {
155
+ param: 'format',
156
+ HookFn: ({ val }: { val: number | string }) =>
157
+ usePassage({ versionId: 3034, usfm: 'JHN.3.16', format: val as 'html' | 'text' }),
158
+ initial: { val: 'html' },
159
+ updated: { val: 'text' },
160
+ expectedInitial: [3034, 'JHN.3.16', 'html', false, false],
161
+ expectedUpdated: [3034, 'JHN.3.16', 'text', false, false],
162
+ },
163
+ ])(
164
+ 'should refetch when $param changes',
165
+ async ({ HookFn, initial, updated, expectedInitial, expectedUpdated }) => {
166
+ const wrapper = createYVWrapper();
167
+ const { result, rerender } = renderHook(HookFn, {
168
+ wrapper,
169
+ initialProps: initial,
170
+ });
171
+
172
+ await waitFor(() => {
173
+ expect(result.current.loading).toBe(false);
174
+ });
175
+
176
+ expect.soft(mockGetPassage).toHaveBeenCalledTimes(1);
177
+ expect.soft(mockGetPassage).toHaveBeenLastCalledWith(...expectedInitial);
178
+
179
+ act(() => {
180
+ rerender(updated);
181
+ });
182
+
183
+ await waitFor(() => {
184
+ expect(result.current.loading).toBe(false);
185
+ });
186
+
187
+ expect.soft(mockGetPassage).toHaveBeenCalledTimes(2);
188
+ expect.soft(mockGetPassage).toHaveBeenLastCalledWith(...expectedUpdated);
189
+ },
190
+ );
191
+ });
192
+
193
+ describe('enabled option', () => {
194
+ it('should not fetch when enabled is false', async () => {
195
+ const wrapper = createYVWrapper();
196
+ const { result } = renderHook(
197
+ () =>
198
+ usePassage({
199
+ versionId: 3034,
200
+ usfm: 'JHN.3.16',
201
+ options: { enabled: false },
202
+ }),
203
+ { wrapper },
204
+ );
205
+
206
+ await waitFor(() => {
207
+ expect(result.current.loading).toBe(false);
208
+ });
209
+
210
+ expect.soft(mockGetPassage).not.toHaveBeenCalled();
211
+ expect.soft(result.current.passage).toBe(null);
212
+ });
213
+
214
+ it('should not fetch when both enabled is false and usfm is invalid', async () => {
215
+ const wrapper = createYVWrapper();
216
+ const { result } = renderHook(
217
+ () =>
218
+ usePassage({
219
+ versionId: 3034,
220
+ usfm: '',
221
+ options: { enabled: false },
222
+ }),
223
+ { wrapper },
224
+ );
225
+
226
+ await waitFor(() => {
227
+ expect(result.current.loading).toBe(false);
228
+ });
229
+
230
+ expect.soft(mockGetPassage).not.toHaveBeenCalled();
231
+ expect.soft(result.current.passage).toBe(null);
232
+ });
233
+ });
234
+
235
+ describe('error handling', () => {
236
+ it('should surface fetch errors', async () => {
237
+ const wrapper = createYVWrapper();
238
+ const error = new Error('Failed to fetch passage');
239
+ mockGetPassage.mockRejectedValueOnce(error);
240
+
241
+ const { result } = renderHook(() => usePassage({ versionId: 3034, usfm: 'JHN.3.16' }), {
242
+ wrapper,
243
+ });
244
+
245
+ await waitFor(() => {
246
+ expect(result.current.loading).toBe(false);
247
+ });
248
+
249
+ expect.soft(result.current.error).toEqual(error);
250
+ expect.soft(result.current.passage).toBe(null);
251
+ });
252
+
253
+ it('should clear error on successful refetch', async () => {
254
+ const wrapper = createYVWrapper();
255
+ const error = new Error('Failed to fetch passage');
256
+ mockGetPassage.mockRejectedValueOnce(error).mockResolvedValueOnce(mockPassage);
257
+
258
+ const { result } = renderHook(() => usePassage({ versionId: 3034, usfm: 'JHN.3.16' }), {
259
+ wrapper,
260
+ });
261
+
262
+ await waitFor(() => {
263
+ expect(result.current.loading).toBe(false);
264
+ });
265
+
266
+ expect.soft(result.current.error).toEqual(error);
267
+ expect.soft(result.current.passage).toBe(null);
268
+
269
+ act(() => {
270
+ result.current.refetch();
271
+ });
272
+
273
+ await waitFor(() => {
274
+ expect(result.current.loading).toBe(false);
275
+ });
276
+
277
+ expect.soft(result.current.error).toBe(null);
278
+ expect.soft(result.current.passage).toEqual(mockPassage);
279
+ });
280
+ });
281
+
282
+ describe('refetch', () => {
283
+ it('should support manual refetch', async () => {
284
+ const wrapper = createYVWrapper();
285
+ const { result } = renderHook(() => usePassage({ versionId: 3034, usfm: 'JHN.3.16' }), {
286
+ wrapper,
287
+ });
288
+
289
+ await waitFor(() => {
290
+ expect(result.current.loading).toBe(false);
291
+ });
292
+
293
+ expect(mockGetPassage).toHaveBeenCalledTimes(1);
294
+
295
+ act(() => {
296
+ result.current.refetch();
297
+ });
298
+
299
+ await waitFor(() => {
300
+ expect(mockGetPassage).toHaveBeenCalledTimes(2);
301
+ });
302
+ });
303
+ });
304
+ });
@@ -0,0 +1,32 @@
1
+ import { renderHook } from '@testing-library/react';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { useTheme } from './useTheme';
4
+ import { createYVWrapper } from './test/utils';
5
+
6
+ describe('useTheme', () => {
7
+ it('should return light when used outside provider', () => {
8
+ const { result } = renderHook(() => useTheme());
9
+ expect(result.current).toBe('light');
10
+ });
11
+
12
+ it('should return light when theme is undefined in provider', () => {
13
+ const { result } = renderHook(() => useTheme(), {
14
+ wrapper: createYVWrapper(),
15
+ });
16
+ expect(result.current).toBe('light');
17
+ });
18
+
19
+ it('should return light when theme is light', () => {
20
+ const { result } = renderHook(() => useTheme(), {
21
+ wrapper: createYVWrapper('test-app-key', { theme: 'light' }),
22
+ });
23
+ expect(result.current).toBe('light');
24
+ });
25
+
26
+ it('should return dark when theme is dark', () => {
27
+ const { result } = renderHook(() => useTheme(), {
28
+ wrapper: createYVWrapper('test-app-key', { theme: 'dark' }),
29
+ });
30
+ expect(result.current).toBe('dark');
31
+ });
32
+ });