@youversion/platform-react-hooks 1.18.1 → 1.19.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 +17 -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
@@ -1,5 +1,4 @@
1
- /* eslint-disable @typescript-eslint/unbound-method, @typescript-eslint/no-unsafe-argument */
2
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
2
  import { renderHook, act } from '@testing-library/react';
4
3
  import { YouVersionAPIUsers, YouVersionPlatformConfiguration } from '@youversion/platform-core';
5
4
  import { useYVAuth } from './useYVAuth';
@@ -8,117 +7,10 @@ import { createMockUserInfo, createMockAuthResult } from './__tests__/mocks/auth
8
7
  import { createAuthProviderWrapper } from './__tests__/utils/test-utils';
9
8
  import type { AuthenticationScopes } from '@youversion/platform-core';
10
9
 
11
- // Mock the core modules
12
- vi.mock('@youversion/platform-core', () => {
13
- // Create a mock configuration object that can be updated
14
- const mockConfiguration = {
15
- accessToken: null as string | null,
16
- idToken: null as string | null,
17
- refreshToken: null as string | null,
18
- appKey: '',
19
- apiHost: 'test-api.example.com',
20
- installationId: null as string | null,
21
- clearAuthTokens: vi.fn(() => {
22
- mockConfiguration.accessToken = null;
23
- mockConfiguration.idToken = null;
24
- mockConfiguration.refreshToken = null;
25
- }),
26
- saveAuthData: vi.fn(
27
- (
28
- accessToken: string | null,
29
- refreshToken: string | null,
30
- idToken: string | null,
31
- installationId: string | null,
32
- ) => {
33
- mockConfiguration.accessToken = accessToken;
34
- mockConfiguration.refreshToken = refreshToken;
35
- mockConfiguration.idToken = idToken;
36
- mockConfiguration.installationId = installationId;
37
- },
38
- ),
39
- };
40
-
41
- return {
42
- YouVersionAPIUsers: {
43
- signIn: vi.fn(),
44
- handleAuthCallback: vi.fn(),
45
- userInfo: vi.fn(),
46
- refreshTokenIfNeeded: vi.fn(),
47
- },
48
- YouVersionPlatformConfiguration: mockConfiguration,
49
- SignInWithYouVersionPermission: {
50
- bibles: 'bibles',
51
- highlights: 'highlights',
52
- user: 'user',
53
- },
54
- YouVersionUserInfo: class YouVersionUserInfo {
55
- readonly name?: string;
56
- readonly userId?: string;
57
- readonly email?: string;
58
- readonly avatarUrlFormat?: string;
59
-
60
- constructor(data: any) {
61
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
62
- this.name = data.name;
63
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
64
- this.userId = data.id;
65
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
66
- this.email = data.email;
67
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
68
- this.avatarUrlFormat = data.avatar_url;
69
- }
70
-
71
- getAvatarUrl(width: number = 200, height: number = 200): URL | null {
72
- if (!this.avatarUrlFormat) {
73
- return null;
74
- }
75
- try {
76
- let urlString = this.avatarUrlFormat;
77
- urlString = urlString.replace('{width}', width.toString());
78
- urlString = urlString.replace('{height}', height.toString());
79
- return new URL(urlString);
80
- } catch {
81
- return null;
82
- }
83
- }
84
-
85
- get avatarUrl(): URL | null {
86
- return this.getAvatarUrl();
87
- }
88
- },
89
- SignInWithYouVersionResult: class SignInWithYouVersionResult {
90
- accessToken: string | undefined;
91
- expiryDate: Date | undefined;
92
- refreshToken: string | undefined;
93
- idToken: string | undefined;
94
- yvpUserId: string | undefined;
95
- name: string | undefined;
96
- profilePicture: string | undefined;
97
- email: string | undefined;
98
-
99
- constructor(props: {
100
- accessToken?: string;
101
- expiresIn?: number;
102
- refreshToken?: string;
103
- idToken?: string;
104
- yvpUserId?: string;
105
- name?: string;
106
- profilePicture?: string;
107
- email?: string;
108
- }) {
109
- this.accessToken = props.accessToken;
110
- this.expiryDate = props.expiresIn
111
- ? new Date(Date.now() + props.expiresIn * 1000)
112
- : new Date();
113
- this.refreshToken = props.refreshToken;
114
- this.idToken = props.idToken;
115
- this.yvpUserId = props.yvpUserId;
116
- this.name = props.name;
117
- this.profilePicture = props.profilePicture;
118
- this.email = props.email;
119
- }
120
- },
121
- };
10
+ // Mock the core modules using shared factory
11
+ vi.mock('@youversion/platform-core', async () => {
12
+ const { createSimpleCoreMockFactory } = await import('./__tests__/mocks/core-mock-factory');
13
+ return createSimpleCoreMockFactory();
122
14
  });
123
15
 
124
16
  const mockUserInfo = createMockUserInfo();
@@ -150,8 +42,6 @@ const renderAuthHook = async () => {
150
42
 
151
43
  describe('useYVAuth', () => {
152
44
  beforeEach(() => {
153
- vi.resetAllMocks();
154
-
155
45
  // Setup window mock
156
46
  vi.stubGlobal('window', mockWindow);
157
47
  mockWindow.location.search = '';
@@ -161,11 +51,6 @@ describe('useYVAuth', () => {
161
51
  YouVersionPlatformConfiguration.installationId = null;
162
52
  });
163
53
 
164
- afterEach(() => {
165
- vi.clearAllMocks();
166
- vi.unstubAllGlobals();
167
- });
168
-
169
54
  describe('initialization', () => {
170
55
  it('should return unauthenticated state when no user info available', async () => {
171
56
  const { result } = await renderAuthHook();
@@ -198,7 +83,7 @@ describe('useYVAuth', () => {
198
83
  await result.current.signIn({ redirectUrl, scopes: ['profile'] });
199
84
  });
200
85
 
201
- expect(vi.mocked(YouVersionAPIUsers.signIn)).toHaveBeenCalledWith(redirectUrl, ['profile']);
86
+ expect(vi.mocked(YouVersionAPIUsers).signIn).toHaveBeenCalledWith(redirectUrl, ['profile']);
202
87
  });
203
88
 
204
89
  it('should call signIn with empty scopes when not provided', async () => {
@@ -209,7 +94,7 @@ describe('useYVAuth', () => {
209
94
  await result.current.signIn({ redirectUrl });
210
95
  });
211
96
 
212
- expect(vi.mocked(YouVersionAPIUsers.signIn)).toHaveBeenCalledWith(redirectUrl);
97
+ expect(vi.mocked(YouVersionAPIUsers).signIn).toHaveBeenCalledWith(redirectUrl);
213
98
  });
214
99
 
215
100
  it('should call YouVersionAPIUsers.signIn exactly once with scopes', async () => {
@@ -221,8 +106,8 @@ describe('useYVAuth', () => {
221
106
  await result.current.signIn({ redirectUrl, scopes });
222
107
  });
223
108
 
224
- expect(vi.mocked(YouVersionAPIUsers.signIn)).toHaveBeenCalledTimes(1);
225
- expect(vi.mocked(YouVersionAPIUsers.signIn)).toHaveBeenCalledWith(redirectUrl, scopes);
109
+ expect(vi.mocked(YouVersionAPIUsers).signIn).toHaveBeenCalledTimes(1);
110
+ expect(vi.mocked(YouVersionAPIUsers).signIn).toHaveBeenCalledWith(redirectUrl, scopes);
226
111
  });
227
112
 
228
113
  it('should call YouVersionAPIUsers.signIn exactly once without scopes', async () => {
@@ -233,14 +118,15 @@ describe('useYVAuth', () => {
233
118
  await result.current.signIn({ redirectUrl });
234
119
  });
235
120
 
236
- expect(vi.mocked(YouVersionAPIUsers.signIn)).toHaveBeenCalledTimes(1);
237
- expect(vi.mocked(YouVersionAPIUsers.signIn)).toHaveBeenCalledWith(redirectUrl);
121
+ expect(vi.mocked(YouVersionAPIUsers).signIn).toHaveBeenCalledTimes(1);
122
+ expect(vi.mocked(YouVersionAPIUsers).signIn).toHaveBeenCalledWith(redirectUrl);
238
123
  });
239
124
 
240
125
  it('should throw error when signIn fails', async () => {
241
126
  const { result } = await renderAuthHook();
242
127
  const error = new Error('Sign in failed');
243
- vi.mocked(YouVersionAPIUsers.signIn).mockRejectedValue(error);
128
+ const signInMock = vi.spyOn(YouVersionAPIUsers, 'signIn');
129
+ signInMock.mockRejectedValue(error);
244
130
 
245
131
  await expect(
246
132
  act(async () => {
@@ -256,7 +142,7 @@ describe('useYVAuth', () => {
256
142
  await result.current.signIn();
257
143
  });
258
144
 
259
- expect(vi.mocked(YouVersionAPIUsers.signIn)).toHaveBeenCalledWith('http://test.example.com');
145
+ expect(vi.mocked(YouVersionAPIUsers).signIn).toHaveBeenCalledWith('http://test.example.com');
260
146
  });
261
147
 
262
148
  it('should use redirectUri from provider with scopes when redirectUrl is not passed', async () => {
@@ -267,7 +153,7 @@ describe('useYVAuth', () => {
267
153
  await result.current.signIn({ scopes });
268
154
  });
269
155
 
270
- expect(vi.mocked(YouVersionAPIUsers.signIn)).toHaveBeenCalledWith(
156
+ expect(vi.mocked(YouVersionAPIUsers).signIn).toHaveBeenCalledWith(
271
157
  'http://test.example.com',
272
158
  scopes,
273
159
  );
@@ -277,21 +163,23 @@ describe('useYVAuth', () => {
277
163
  describe('processCallback', () => {
278
164
  it('should call handleAuthCallback and return result', async () => {
279
165
  const { result } = await renderAuthHook();
280
- vi.mocked(YouVersionAPIUsers.handleAuthCallback).mockResolvedValue(mockAuthResult as any);
166
+ const callbackMock = vi.spyOn(YouVersionAPIUsers, 'handleAuthCallback');
167
+ callbackMock.mockResolvedValue(mockAuthResult);
281
168
 
282
169
  let callbackResult;
283
170
  await act(async () => {
284
171
  callbackResult = await result.current.processCallback();
285
172
  });
286
173
 
287
- expect(vi.mocked(YouVersionAPIUsers.handleAuthCallback)).toHaveBeenCalled();
174
+ expect(callbackMock).toHaveBeenCalled();
288
175
  expect(callbackResult).toEqual(mockAuthResult);
289
176
  });
290
177
 
291
178
  it('should throw error when callback processing fails', async () => {
292
179
  const { result } = await renderAuthHook();
293
180
  const error = new Error('Callback processing failed');
294
- vi.mocked(YouVersionAPIUsers.handleAuthCallback).mockRejectedValue(error);
181
+ const callbackMock = vi.spyOn(YouVersionAPIUsers, 'handleAuthCallback');
182
+ callbackMock.mockRejectedValue(error);
295
183
 
296
184
  await expect(
297
185
  act(async () => {
@@ -302,7 +190,8 @@ describe('useYVAuth', () => {
302
190
 
303
191
  it('should return null when no result from callback', async () => {
304
192
  const { result } = await renderAuthHook();
305
- vi.mocked(YouVersionAPIUsers.handleAuthCallback).mockResolvedValue(null);
193
+ const callbackMock = vi.spyOn(YouVersionAPIUsers, 'handleAuthCallback');
194
+ callbackMock.mockResolvedValue(null);
306
195
 
307
196
  let callbackResult;
308
197
  await act(async () => {
@@ -317,11 +206,14 @@ describe('useYVAuth', () => {
317
206
  it('should call clearAuthTokens and reset user info', async () => {
318
207
  const { result } = await renderAuthHook();
319
208
 
209
+ const clearAuthTokensSpy = vi.spyOn(YouVersionPlatformConfiguration, 'clearAuthTokens');
210
+ clearAuthTokensSpy.mockClear();
211
+
320
212
  act(() => {
321
213
  result.current.signOut();
322
214
  });
323
215
 
324
- expect(YouVersionPlatformConfiguration.clearAuthTokens).toHaveBeenCalled();
216
+ expect(clearAuthTokensSpy).toHaveBeenCalledTimes(1);
325
217
  });
326
218
  });
327
219
 
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect, test } from 'vitest';
2
+ import { getDayOfYear } from './getDayOfYear';
3
+
4
+ describe('getDayOfYear', () => {
5
+ // Group 1: Standard days
6
+ test.for([
7
+ { date: new Date(2025, 0, 1), expected: 1, label: 'Jan 1' },
8
+ { date: new Date(2025, 0, 2), expected: 2, label: 'Jan 2' },
9
+ { date: new Date(2025, 1, 1), expected: 32, label: 'Feb 1' },
10
+ { date: new Date(2025, 6, 4), expected: 185, label: 'Jul 4 (non-leap)' },
11
+ { date: new Date(2025, 11, 31), expected: 365, label: 'Dec 31 (non-leap)' },
12
+ ])('should return $expected for $label', ({ date, expected }, { expect }) => {
13
+ expect(getDayOfYear(date)).toBe(expected);
14
+ });
15
+
16
+ // Group 2: Leap year handling
17
+ test.for([
18
+ { date: new Date(2024, 1, 28), expected: 59, label: 'Feb 28 (leap)' },
19
+ { date: new Date(2024, 1, 29), expected: 60, label: 'Feb 29 (leap)' },
20
+ { date: new Date(2024, 2, 1), expected: 61, label: 'Mar 1 (leap)' },
21
+ { date: new Date(2025, 2, 1), expected: 60, label: 'Mar 1 (non-leap)' },
22
+ { date: new Date(2024, 11, 31), expected: 366, label: 'Dec 31 (leap)' },
23
+ ])('should return $expected for $label', ({ date, expected }, { expect }) => {
24
+ expect(getDayOfYear(date)).toBe(expected);
25
+ });
26
+
27
+ // Group 3: Time-of-day stability
28
+ test.for([
29
+ { date: new Date(2025, 0, 1, 0, 0, 0), expected: 1, label: 'midnight' },
30
+ { date: new Date(2025, 0, 1, 23, 59, 59), expected: 1, label: 'end of day' },
31
+ { date: new Date(2025, 5, 15, 12, 0, 0), expected: 166, label: 'noon Jun 15' },
32
+ ])('should return $expected at $label', ({ date, expected }, { expect }) => {
33
+ expect(getDayOfYear(date)).toBe(expected);
34
+ });
35
+
36
+ // Group 4: Edge cases
37
+ it('should handle century non-leap year (1900)', () => {
38
+ expect(getDayOfYear(new Date(1900, 2, 1))).toBe(60);
39
+ });
40
+
41
+ it('should handle century leap year (2000)', () => {
42
+ expect(getDayOfYear(new Date(2000, 2, 1))).toBe(61);
43
+ });
44
+
45
+ it('should handle far future dates', () => {
46
+ expect(getDayOfYear(new Date(2100, 0, 1))).toBe(1);
47
+ });
48
+ });
package/vitest.config.ts CHANGED
@@ -4,15 +4,27 @@ import react from '@vitejs/plugin-react';
4
4
  export default defineConfig({
5
5
  plugins: [react()],
6
6
  test: {
7
+ env: { TZ: 'UTC' }, // Pin to UTC so date-arithmetic tests are timezone-independent
7
8
  environment: 'jsdom',
8
9
  setupFiles: ['./src/test/setup.ts'],
9
10
  globals: true,
11
+ mockReset: true,
12
+ unstubGlobals: true,
10
13
  coverage: {
11
14
  provider: 'v8',
12
15
  reporter: ['text', 'json-summary'],
13
16
  reportsDirectory: './coverage',
14
17
  all: true,
15
18
  include: ['src/**/*.{ts,tsx}'],
19
+ exclude: [
20
+ 'src/useInitData.ts',
21
+ 'src/useChapterNavigation.ts',
22
+ 'src/useVerseSelection.ts',
23
+ 'src/context/ReaderContext.tsx',
24
+ 'src/context/ReaderProvider.tsx',
25
+ 'src/context/VerseSelectionContext.tsx',
26
+ 'src/context/VerseSelectionProvider.tsx',
27
+ ],
16
28
  },
17
29
  },
18
30
  });
@@ -1,264 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { renderHook, act } from '@testing-library/react';
3
- import React from 'react';
4
- import { useReaderContext } from './ReaderContext';
5
- import { ReaderProvider } from './ReaderProvider';
6
- import {
7
- createMockBook,
8
- createMockChapter,
9
- createMockVerse,
10
- createMockVersion,
11
- } from '../__tests__/mocks/bibles';
12
-
13
- // Mock Bible data
14
- const mockVersion = createMockVersion();
15
- const mockBook = createMockBook();
16
- const mockChapter = createMockChapter();
17
- const mockVerse = createMockVerse();
18
-
19
- // Alternative mock data for update tests
20
- const mockVersion2 = createMockVersion({
21
- id: 2,
22
- title: 'New International Version',
23
- abbreviation: 'NIV',
24
- localized_abbreviation: 'NIV',
25
- localized_title: 'New International Version',
26
- youversion_deep_link: 'https://www.bible.com/versions/2',
27
- });
28
-
29
- const mockBook2 = createMockBook({
30
- id: 'JHN',
31
- title: 'John',
32
- full_title: 'The Gospel According to John',
33
- abbreviation: 'Jn',
34
- canon: 'new_testament',
35
- });
36
-
37
- const mockChapter2 = createMockChapter({
38
- id: '3',
39
- passage_id: 'JHN.3',
40
- title: '3',
41
- });
42
-
43
- const mockVerse2 = createMockVerse({
44
- id: '16',
45
- passage_id: 'JHN.3.16',
46
- title: '16',
47
- });
48
-
49
- // Test wrappers
50
- const wrapper = ({ children }: { children: React.ReactNode }) => (
51
- <ReaderProvider
52
- currentVersion={mockVersion}
53
- currentBook={mockBook}
54
- currentChapter={mockChapter}
55
- currentVerse={mockVerse}
56
- >
57
- {children}
58
- </ReaderProvider>
59
- );
60
-
61
- const wrapperWithNullVerse = ({ children }: { children: React.ReactNode }) => (
62
- <ReaderProvider
63
- currentVersion={mockVersion}
64
- currentBook={mockBook}
65
- currentChapter={mockChapter}
66
- currentVerse={null}
67
- >
68
- {children}
69
- </ReaderProvider>
70
- );
71
-
72
- describe('ReaderProvider', () => {
73
- describe('initialization', () => {
74
- it('should initialize with provided version', () => {
75
- const { result } = renderHook(() => useReaderContext(), { wrapper });
76
-
77
- expect(result.current.currentVersion).toEqual(mockVersion);
78
- });
79
-
80
- it('should initialize with provided book', () => {
81
- const { result } = renderHook(() => useReaderContext(), { wrapper });
82
-
83
- expect(result.current.currentBook).toEqual(mockBook);
84
- });
85
-
86
- it('should initialize with provided chapter', () => {
87
- const { result } = renderHook(() => useReaderContext(), { wrapper });
88
-
89
- expect(result.current.currentChapter).toEqual(mockChapter);
90
- });
91
-
92
- it('should initialize with provided verse', () => {
93
- const { result } = renderHook(() => useReaderContext(), { wrapper });
94
-
95
- expect(result.current.currentVerse).toEqual(mockVerse);
96
- });
97
-
98
- it('should initialize with null verse when provided', () => {
99
- const { result } = renderHook(() => useReaderContext(), { wrapper: wrapperWithNullVerse });
100
-
101
- expect(result.current.currentVerse).toBeNull();
102
- });
103
-
104
- it('should throw error when useReaderContext is used outside provider', () => {
105
- const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
106
-
107
- expect(() => renderHook(() => useReaderContext())).toThrow(
108
- 'useReaderContext() must be used within a ReaderProvider',
109
- );
110
-
111
- consoleError.mockRestore();
112
- });
113
- });
114
-
115
- describe('setVersion', () => {
116
- it('should update version when setVersion is called', () => {
117
- const { result } = renderHook(() => useReaderContext(), { wrapper });
118
-
119
- act(() => {
120
- result.current.setVersion(mockVersion2);
121
- });
122
-
123
- expect(result.current.currentVersion).toEqual(mockVersion2);
124
- });
125
-
126
- it('should preserve other state when version changes', () => {
127
- const { result } = renderHook(() => useReaderContext(), { wrapper });
128
-
129
- act(() => {
130
- result.current.setVersion(mockVersion2);
131
- });
132
-
133
- expect(result.current.currentVersion).toMatchObject({ abbreviation: 'NIV' });
134
- expect(result.current.currentBook).toEqual(mockBook);
135
- expect(result.current.currentChapter).toEqual(mockChapter);
136
- expect(result.current.currentVerse).toEqual(mockVerse);
137
- });
138
- });
139
-
140
- describe('setBook', () => {
141
- it('should update book when setBook is called', () => {
142
- const { result } = renderHook(() => useReaderContext(), { wrapper });
143
-
144
- act(() => {
145
- result.current.setBook(mockBook2);
146
- });
147
-
148
- expect(result.current.currentBook).toEqual(mockBook2);
149
- });
150
-
151
- it('should preserve other state when book changes', () => {
152
- const { result } = renderHook(() => useReaderContext(), { wrapper });
153
-
154
- act(() => {
155
- result.current.setBook(mockBook2);
156
- });
157
-
158
- expect(result.current.currentBook).toMatchObject({ id: 'JHN' });
159
- expect(result.current.currentVersion).toEqual(mockVersion);
160
- expect(result.current.currentChapter).toEqual(mockChapter);
161
- expect(result.current.currentVerse).toEqual(mockVerse);
162
- });
163
- });
164
-
165
- describe('setChapter', () => {
166
- it('should update chapter when setChapter is called', () => {
167
- const { result } = renderHook(() => useReaderContext(), { wrapper });
168
-
169
- act(() => {
170
- result.current.setChapter(mockChapter2);
171
- });
172
-
173
- expect(result.current.currentChapter).toEqual(mockChapter2);
174
- });
175
-
176
- it('should preserve other state when chapter changes', () => {
177
- const { result } = renderHook(() => useReaderContext(), { wrapper });
178
-
179
- act(() => {
180
- result.current.setChapter(mockChapter2);
181
- });
182
-
183
- expect(result.current.currentChapter).toMatchObject({ id: '3' });
184
- expect(result.current.currentVersion).toEqual(mockVersion);
185
- expect(result.current.currentBook).toEqual(mockBook);
186
- expect(result.current.currentVerse).toEqual(mockVerse);
187
- });
188
- });
189
-
190
- describe('setVerse', () => {
191
- it('should update verse when setVerse is called', () => {
192
- const { result } = renderHook(() => useReaderContext(), { wrapper });
193
-
194
- act(() => {
195
- result.current.setVerse(mockVerse2);
196
- });
197
-
198
- expect(result.current.currentVerse).toEqual(mockVerse2);
199
- });
200
-
201
- it('should update verse to null when setVerse is called with null', () => {
202
- const { result } = renderHook(() => useReaderContext(), { wrapper });
203
-
204
- act(() => {
205
- result.current.setVerse(null);
206
- });
207
-
208
- expect(result.current.currentVerse).toBeNull();
209
- });
210
-
211
- it('should preserve other state when verse changes', () => {
212
- const { result } = renderHook(() => useReaderContext(), { wrapper });
213
-
214
- act(() => {
215
- result.current.setVerse(mockVerse2);
216
- });
217
-
218
- expect(result.current.currentVerse).toMatchObject({ id: '16' });
219
- expect(result.current.currentVersion).toEqual(mockVersion);
220
- expect(result.current.currentBook).toEqual(mockBook);
221
- expect(result.current.currentChapter).toEqual(mockChapter);
222
- });
223
-
224
- it('should preserve other state when verse changes to null', () => {
225
- const { result } = renderHook(() => useReaderContext(), { wrapper });
226
-
227
- act(() => {
228
- result.current.setVerse(null);
229
- });
230
-
231
- expect(result.current.currentVerse).toBeNull();
232
- expect(result.current.currentVersion).toEqual(mockVersion);
233
- expect(result.current.currentBook).toEqual(mockBook);
234
- expect(result.current.currentChapter).toEqual(mockChapter);
235
- });
236
- });
237
-
238
- describe('state persistence', () => {
239
- it('should initialize with all provided props', () => {
240
- const { result } = renderHook(() => useReaderContext(), { wrapper });
241
-
242
- expect(result.current.currentVersion).toEqual(mockVersion);
243
- expect(result.current.currentBook).toEqual(mockBook);
244
- expect(result.current.currentChapter).toEqual(mockChapter);
245
- expect(result.current.currentVerse).toEqual(mockVerse);
246
- });
247
-
248
- it('should handle rapid successive state updates without corruption', () => {
249
- const { result } = renderHook(() => useReaderContext(), { wrapper });
250
-
251
- act(() => {
252
- result.current.setVersion(mockVersion2);
253
- result.current.setBook(mockBook2);
254
- result.current.setChapter(mockChapter2);
255
- result.current.setVerse(mockVerse2);
256
- });
257
-
258
- expect(result.current.currentVersion).toEqual(mockVersion2);
259
- expect(result.current.currentBook).toEqual(mockBook2);
260
- expect(result.current.currentChapter).toEqual(mockChapter2);
261
- expect(result.current.currentVerse).toEqual(mockVerse2);
262
- });
263
- });
264
- });