@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
@@ -1 +1 @@
1
- {"version":3,"file":"useVerseSelection.d.ts","sourceRoot":"","sources":["../src/useVerseSelection.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,yBAAyB,EAC/B,MAAM,iCAAiC,CAAC;AAEzC,wBAAgB,iBAAiB,IAAI,yBAAyB,CAM7D"}
1
+ {"version":3,"file":"useVerseSelection.d.ts","sourceRoot":"","sources":["../src/useVerseSelection.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,yBAAyB,EAC/B,MAAM,iCAAiC,CAAC;AAEzC;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,yBAAyB,CAM7D"}
@@ -1,5 +1,8 @@
1
1
  import { useContext } from 'react';
2
2
  import { VerseSelectionContext, } from './context/VerseSelectionContext';
3
+ /**
4
+ * @deprecated No replacement needed. Remove usage. Will be removed in the next major version.
5
+ */
3
6
  export function useVerseSelection() {
4
7
  const context = useContext(VerseSelectionContext);
5
8
  if (!context) {
@@ -1 +1 @@
1
- {"version":3,"file":"useVerseSelection.js","sourceRoot":"","sources":["../src/useVerseSelection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACnC,OAAO,EACL,qBAAqB,GAEtB,MAAM,iCAAiC,CAAC;AAEzC,MAAM,UAAU,iBAAiB;IAC/B,MAAM,OAAO,GAAG,UAAU,CAAC,qBAAqB,CAAC,CAAC;IAClD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,gEAAgE,CAAC,CAAC;IACpF,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"useVerseSelection.js","sourceRoot":"","sources":["../src/useVerseSelection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACnC,OAAO,EACL,qBAAqB,GAEtB,MAAM,iCAAiC,CAAC;AAEzC;;GAEG;AACH,MAAM,UAAU,iBAAiB;IAC/B,MAAM,OAAO,GAAG,UAAU,CAAC,qBAAqB,CAAC,CAAC;IAClD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,gEAAgE,CAAC,CAAC;IACpF,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youversion/platform-react-hooks",
3
- "version": "1.18.1",
3
+ "version": "1.20.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -22,7 +22,7 @@
22
22
  }
23
23
  },
24
24
  "dependencies": {
25
- "@youversion/platform-core": "1.18.1"
25
+ "@youversion/platform-core": "1.20.0"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "react": ">=19.1.0 <20.0.0"
@@ -1,4 +1,10 @@
1
- import type { BibleBook, BibleChapter, BibleVerse, BibleVersion } from '@youversion/platform-core';
1
+ import type {
2
+ BibleBook,
3
+ BibleChapter,
4
+ BiblePassage,
5
+ BibleVerse,
6
+ BibleVersion,
7
+ } from '@youversion/platform-core';
2
8
 
3
9
  /**
4
10
  * Creates a mock Bible version for testing
@@ -54,3 +60,14 @@ export const createMockVerse = (overrides?: Partial<BibleVerse>): BibleVerse =>
54
60
  title: '1',
55
61
  ...overrides,
56
62
  });
63
+
64
+ /**
65
+ * Creates a mock Bible passage for testing
66
+ * @param overrides Optional properties to override defaults
67
+ */
68
+ export const createMockPassage = (overrides?: Partial<BiblePassage>): BiblePassage => ({
69
+ id: 'JHN.3.16',
70
+ content: '<p>For God so loved the world...</p>',
71
+ reference: 'John 3:16',
72
+ ...overrides,
73
+ });
@@ -0,0 +1,226 @@
1
+ import { vi, type MockedFunction } from 'vitest';
2
+ import type { YouVersionAPIUsers } from '@youversion/platform-core';
3
+
4
+ interface MockUserInfoData {
5
+ name?: string;
6
+ id?: string;
7
+ email?: string;
8
+ avatar_url?: string;
9
+ }
10
+
11
+ interface MockAuthResultProps {
12
+ accessToken?: string;
13
+ expiresIn?: number;
14
+ refreshToken?: string;
15
+ idToken?: string;
16
+ yvpUserId?: string;
17
+ name?: string;
18
+ profilePicture?: string;
19
+ email?: string;
20
+ }
21
+
22
+ export class MockYouVersionUserInfo {
23
+ readonly name?: string;
24
+ readonly userId?: string;
25
+ readonly email?: string;
26
+ readonly avatarUrlFormat?: string;
27
+
28
+ constructor(data: MockUserInfoData) {
29
+ this.name = data.name;
30
+ this.userId = data.id;
31
+ this.email = data.email;
32
+ this.avatarUrlFormat = data.avatar_url;
33
+ }
34
+
35
+ getAvatarUrl(width: number = 200, height: number = 200): URL | null {
36
+ if (!this.avatarUrlFormat) {
37
+ return null;
38
+ }
39
+ try {
40
+ let urlString = this.avatarUrlFormat;
41
+ if (urlString.startsWith('//')) {
42
+ urlString = 'https:' + urlString;
43
+ }
44
+ urlString = urlString.replace('{width}', width.toString());
45
+ urlString = urlString.replace('{height}', height.toString());
46
+ return new URL(urlString);
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ get avatarUrl(): URL | null {
53
+ return this.getAvatarUrl();
54
+ }
55
+ }
56
+
57
+ export class MockSignInWithYouVersionResult {
58
+ readonly accessToken: string | undefined;
59
+ readonly expiryDate: Date | undefined;
60
+ readonly refreshToken: string | undefined;
61
+ readonly idToken: string | undefined;
62
+ readonly yvpUserId: string | undefined;
63
+ readonly name: string | undefined;
64
+ readonly profilePicture: string | undefined;
65
+ readonly email: string | undefined;
66
+
67
+ constructor(props: MockAuthResultProps) {
68
+ this.accessToken = props.accessToken;
69
+ this.expiryDate = props.expiresIn ? new Date(Date.now() + props.expiresIn * 1000) : new Date();
70
+ this.refreshToken = props.refreshToken;
71
+ this.idToken = props.idToken;
72
+ this.yvpUserId = props.yvpUserId;
73
+ this.name = props.name;
74
+ this.profilePicture = props.profilePicture;
75
+ this.email = props.email;
76
+ }
77
+ }
78
+
79
+ interface MockConfiguration {
80
+ accessToken: string | null;
81
+ idToken: string | null;
82
+ refreshToken: string | null;
83
+ appKey: string;
84
+ apiHost: string;
85
+ installationId: string | null;
86
+ clearAuthTokens: MockedFunction<() => void>;
87
+ saveAuthData: MockedFunction<
88
+ (
89
+ accessToken: string | null,
90
+ refreshToken: string | null,
91
+ idToken: string | null,
92
+ installationId: string | null,
93
+ ) => void
94
+ >;
95
+ }
96
+
97
+ interface SimpleCoreMockFactory {
98
+ YouVersionAPIUsers: {
99
+ signIn: MockedFunction<typeof YouVersionAPIUsers.signIn>;
100
+ handleAuthCallback: MockedFunction<typeof YouVersionAPIUsers.handleAuthCallback>;
101
+ userInfo: MockedFunction<typeof YouVersionAPIUsers.userInfo>;
102
+ refreshTokenIfNeeded: MockedFunction<typeof YouVersionAPIUsers.refreshTokenIfNeeded>;
103
+ };
104
+ YouVersionPlatformConfiguration: MockConfiguration;
105
+ SignInWithYouVersionPermission: Record<string, string>;
106
+ YouVersionUserInfo: typeof MockYouVersionUserInfo;
107
+ SignInWithYouVersionResult: typeof MockSignInWithYouVersionResult;
108
+ }
109
+
110
+ interface GetterCoreMockFactory {
111
+ YouVersionAPIUsers: {
112
+ handleAuthCallback: MockedFunction<typeof YouVersionAPIUsers.handleAuthCallback>;
113
+ userInfo: MockedFunction<typeof YouVersionAPIUsers.userInfo>;
114
+ refreshTokenIfNeeded: MockedFunction<typeof YouVersionAPIUsers.refreshTokenIfNeeded>;
115
+ };
116
+ YouVersionPlatformConfiguration: {
117
+ appKey: string;
118
+ installationId: string;
119
+ apiHost: string;
120
+ readonly idToken: string | null;
121
+ readonly refreshToken: string | null;
122
+ readonly accessToken: string | null;
123
+ clearAuthTokens: MockedFunction<() => void>;
124
+ saveAuthData: MockedFunction<
125
+ (accessToken: string | null, refreshToken: string | null, idToken: string | null) => void
126
+ >;
127
+ };
128
+ YouVersionUserInfo: typeof MockYouVersionUserInfo;
129
+ SignInWithYouVersionResult: typeof MockSignInWithYouVersionResult;
130
+ }
131
+
132
+ export function createSimpleCoreMockFactory(): SimpleCoreMockFactory {
133
+ const mockConfiguration: MockConfiguration = {
134
+ accessToken: null,
135
+ idToken: null,
136
+ refreshToken: null,
137
+ appKey: '',
138
+ apiHost: 'test-api.example.com',
139
+ installationId: null,
140
+ clearAuthTokens: vi.fn(() => {
141
+ mockConfiguration.accessToken = null;
142
+ mockConfiguration.idToken = null;
143
+ mockConfiguration.refreshToken = null;
144
+ }),
145
+ saveAuthData: vi.fn(
146
+ (
147
+ accessToken: string | null,
148
+ refreshToken: string | null,
149
+ idToken: string | null,
150
+ installationId: string | null,
151
+ ) => {
152
+ mockConfiguration.accessToken = accessToken;
153
+ mockConfiguration.refreshToken = refreshToken;
154
+ mockConfiguration.idToken = idToken;
155
+ mockConfiguration.installationId = installationId;
156
+ },
157
+ ),
158
+ };
159
+
160
+ return {
161
+ YouVersionAPIUsers: {
162
+ signIn: vi.fn(),
163
+ handleAuthCallback: vi.fn(),
164
+ userInfo: vi.fn(),
165
+ refreshTokenIfNeeded: vi.fn(),
166
+ },
167
+ YouVersionPlatformConfiguration: mockConfiguration,
168
+ SignInWithYouVersionPermission: {
169
+ bibles: 'bibles',
170
+ highlights: 'highlights',
171
+ votd: 'votd',
172
+ demographics: 'demographics',
173
+ bibleActivity: 'bible_activity',
174
+ },
175
+ YouVersionUserInfo: MockYouVersionUserInfo,
176
+ SignInWithYouVersionResult: MockSignInWithYouVersionResult,
177
+ };
178
+ }
179
+
180
+ export function createGetterCoreMockFactory(): GetterCoreMockFactory {
181
+ let mockInstallationId = 'auto-generated-installation-id';
182
+ let mockIdToken: string | null = null;
183
+ let mockRefreshToken: string | null = null;
184
+ let mockAccessToken: string | null = null;
185
+
186
+ return {
187
+ YouVersionAPIUsers: {
188
+ handleAuthCallback: vi.fn(),
189
+ userInfo: vi.fn(),
190
+ refreshTokenIfNeeded: vi.fn(),
191
+ },
192
+ YouVersionPlatformConfiguration: {
193
+ appKey: '',
194
+ get installationId() {
195
+ return mockInstallationId;
196
+ },
197
+ set installationId(value) {
198
+ if (value) mockInstallationId = value;
199
+ },
200
+ apiHost: 'test-api.example.com',
201
+ get idToken() {
202
+ return mockIdToken;
203
+ },
204
+ get refreshToken() {
205
+ return mockRefreshToken;
206
+ },
207
+ get accessToken() {
208
+ return mockAccessToken;
209
+ },
210
+ clearAuthTokens: vi.fn(() => {
211
+ mockIdToken = null;
212
+ mockRefreshToken = null;
213
+ mockAccessToken = null;
214
+ }),
215
+ saveAuthData: vi.fn(
216
+ (accessToken: string | null, refreshToken: string | null, idToken: string | null) => {
217
+ mockAccessToken = accessToken;
218
+ mockRefreshToken = refreshToken;
219
+ mockIdToken = idToken;
220
+ },
221
+ ),
222
+ },
223
+ YouVersionUserInfo: MockYouVersionUserInfo,
224
+ SignInWithYouVersionResult: MockSignInWithYouVersionResult,
225
+ };
226
+ }
@@ -14,8 +14,14 @@ type ReaderContextData = {
14
14
  setVerse: (verse: BibleVerse | null) => void;
15
15
  };
16
16
 
17
+ /**
18
+ * @deprecated No replacement needed. Remove usage. Will be removed in the next major version.
19
+ */
17
20
  export const ReaderContext = createContext<ReaderContextData | null>(null);
18
21
 
22
+ /**
23
+ * @deprecated No replacement needed. Remove usage. Will be removed in the next major version.
24
+ */
19
25
  export function useReaderContext(): ReaderContextData {
20
26
  const context = useContext(ReaderContext);
21
27
 
@@ -11,6 +11,9 @@ type ReaderProviderProps = {
11
11
  currentVerse: BibleVerse | null;
12
12
  };
13
13
 
14
+ /**
15
+ * @deprecated No replacement needed. Remove usage. Will be removed in the next major version.
16
+ */
14
17
  export function ReaderProvider(props: PropsWithChildren<ReaderProviderProps>): React.ReactElement {
15
18
  const [currentVersion, setCurrentVersion] = useState<BibleVersion>(props.currentVersion);
16
19
  const [currentBook, setCurrentBook] = useState<BibleBook>(props.currentBook);
@@ -1,5 +1,8 @@
1
1
  import { createContext } from 'react';
2
2
 
3
+ /**
4
+ * @deprecated No replacement needed. Remove usage. Will be removed in the next major version.
5
+ */
3
6
  export type VerseSelectionContextData = {
4
7
  selectedVerseUsfms: Set<string>;
5
8
  toggleVerse: (usfm: string) => void;
@@ -8,4 +11,7 @@ export type VerseSelectionContextData = {
8
11
  selectedCount: number;
9
12
  };
10
13
 
14
+ /**
15
+ * @deprecated No replacement needed. Remove usage. Will be removed in the next major version.
16
+ */
11
17
  export const VerseSelectionContext = createContext<VerseSelectionContextData | null>(null);
@@ -1,6 +1,9 @@
1
1
  import { type PropsWithChildren, useCallback, useState } from 'react';
2
2
  import { VerseSelectionContext } from './VerseSelectionContext';
3
3
 
4
+ /**
5
+ * @deprecated No replacement needed. Remove usage. Will be removed in the next major version.
6
+ */
4
7
  export function VerseSelectionProvider({ children }: PropsWithChildren): React.ReactElement {
5
8
  const [selectedVerseUsfms, setSelectedVerseUsfms] = useState<Set<string>>(new Set());
6
9
 
@@ -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 { render } from '@testing-library/react';
4
3
  import { YouVersionAPIUsers, YouVersionPlatformConfiguration } from '@youversion/platform-core';
5
4
  import YouVersionAuthProvider from './YouVersionAuthProvider';
@@ -7,118 +6,10 @@ import { useYouVersionAuthContext } from './YouVersionAuthContext';
7
6
  import type { AuthConfig } from '../types/auth';
8
7
  import { createMockUserInfo, createMockAuthResult } from '../__tests__/mocks/auth';
9
8
 
10
- // Mock the core modules
11
- vi.mock('@youversion/platform-core', () => {
12
- let mockInstallationId = 'auto-generated-installation-id';
13
- let mockIdToken: string | null = null;
14
- let mockRefreshToken: string | null = null;
15
- let mockAccessToken: string | null = null;
16
-
17
- return {
18
- YouVersionAPIUsers: {
19
- handleAuthCallback: vi.fn(),
20
- userInfo: vi.fn(),
21
- refreshTokenIfNeeded: vi.fn(),
22
- },
23
- YouVersionPlatformConfiguration: {
24
- appKey: '',
25
- get installationId() {
26
- return mockInstallationId;
27
- },
28
- set installationId(value) {
29
- if (value) mockInstallationId = value;
30
- },
31
- apiHost: 'test-api.example.com',
32
- get idToken() {
33
- return mockIdToken;
34
- },
35
- get refreshToken() {
36
- return mockRefreshToken;
37
- },
38
- get accessToken() {
39
- return mockAccessToken;
40
- },
41
- clearAuthTokens: vi.fn(() => {
42
- mockIdToken = null;
43
- mockRefreshToken = null;
44
- mockAccessToken = null;
45
- }),
46
- saveAuthData: vi.fn(
47
- (accessToken: string | null, refreshToken: string | null, idToken: string | null) => {
48
- mockAccessToken = accessToken;
49
- mockRefreshToken = refreshToken;
50
- mockIdToken = idToken;
51
- },
52
- ),
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
- };
9
+ // Mock the core modules using shared factory
10
+ vi.mock('@youversion/platform-core', async () => {
11
+ const { createGetterCoreMockFactory } = await import('../__tests__/mocks/core-mock-factory');
12
+ return createGetterCoreMockFactory();
122
13
  });
123
14
 
124
15
  const mockConfig: AuthConfig = {
@@ -152,8 +43,6 @@ function TestChild() {
152
43
 
153
44
  describe('YouVersionAuthProvider', () => {
154
45
  beforeEach(() => {
155
- vi.clearAllMocks();
156
-
157
46
  // Setup window mock
158
47
  vi.stubGlobal('window', mockWindow);
159
48
  mockWindow.location.search = '';
@@ -164,11 +53,6 @@ describe('YouVersionAuthProvider', () => {
164
53
  YouVersionPlatformConfiguration.clearAuthTokens();
165
54
  });
166
55
 
167
- afterEach(() => {
168
- vi.clearAllMocks();
169
- vi.unstubAllGlobals();
170
- });
171
-
172
56
  describe('initialization', () => {
173
57
  it('should configure YouVersionPlatformConfiguration on mount', async () => {
174
58
  render(
@@ -225,11 +109,10 @@ describe('YouVersionAuthProvider', () => {
225
109
  describe('OAuth callback handling', () => {
226
110
  it('should detect OAuth callback with state parameter', async () => {
227
111
  mockWindow.location.search = '?state=test-state&code=auth-code';
228
- vi.mocked(YouVersionAPIUsers.handleAuthCallback).mockResolvedValue(mockAuthResult as any);
229
- vi.mocked(YouVersionAPIUsers.userInfo).mockReturnValue(mockUserInfo as any);
112
+ vi.spyOn(YouVersionAPIUsers, 'userInfo').mockReturnValue(mockUserInfo);
230
113
 
231
114
  // Mock the configuration to return the id token after handleAuthCallback
232
- vi.mocked(YouVersionAPIUsers.handleAuthCallback).mockImplementation(() => {
115
+ vi.spyOn(YouVersionAPIUsers, 'handleAuthCallback').mockImplementation(() => {
233
116
  YouVersionPlatformConfiguration.saveAuthData(null, null, 'test-id-token', null);
234
117
  return Promise.resolve(mockAuthResult as any);
235
118
  });
@@ -251,7 +134,7 @@ describe('YouVersionAuthProvider', () => {
251
134
 
252
135
  it('should detect OAuth callback with error parameter', async () => {
253
136
  mockWindow.location.search = '?error=access_denied&error_description=User+denied+access';
254
- vi.mocked(YouVersionAPIUsers.handleAuthCallback).mockResolvedValue(mockAuthResult as any);
137
+ vi.spyOn(YouVersionAPIUsers, 'handleAuthCallback').mockResolvedValue(mockAuthResult);
255
138
 
256
139
  const { getByTestId } = render(
257
140
  <YouVersionAuthProvider config={mockConfig}>
@@ -269,7 +152,7 @@ describe('YouVersionAuthProvider', () => {
269
152
  it('should handle callback error and set error state', async () => {
270
153
  mockWindow.location.search = '?state=test-state&code=auth-code';
271
154
  const callbackError = new Error('Callback processing failed');
272
- vi.mocked(YouVersionAPIUsers.handleAuthCallback).mockRejectedValue(callbackError);
155
+ vi.spyOn(YouVersionAPIUsers, 'handleAuthCallback').mockRejectedValue(callbackError);
273
156
 
274
157
  const { getByTestId } = render(
275
158
  <YouVersionAuthProvider config={mockConfig}>
@@ -286,7 +169,7 @@ describe('YouVersionAuthProvider', () => {
286
169
 
287
170
  it('should handle callback with no idToken', async () => {
288
171
  mockWindow.location.search = '?state=test-state&code=auth-code';
289
- vi.mocked(YouVersionAPIUsers.handleAuthCallback).mockResolvedValue(mockAuthResult as any);
172
+ vi.spyOn(YouVersionAPIUsers, 'handleAuthCallback').mockResolvedValue(mockAuthResult);
290
173
  YouVersionPlatformConfiguration.saveAuthData(null, null, null, null);
291
174
 
292
175
  const { getByTestId } = render(
@@ -310,11 +193,11 @@ describe('YouVersionAuthProvider', () => {
310
193
  YouVersionPlatformConfiguration.saveAuthData(null, 'existing-refresh-token', null, null);
311
194
 
312
195
  // Mock refreshTokenIfNeeded to set the id token after successful refresh
313
- vi.mocked(YouVersionAPIUsers.refreshTokenIfNeeded).mockImplementation(() => {
196
+ vi.spyOn(YouVersionAPIUsers, 'refreshTokenIfNeeded').mockImplementation(() => {
314
197
  YouVersionPlatformConfiguration.saveAuthData(null, null, 'refreshed-id-token', null);
315
198
  return Promise.resolve(true);
316
199
  });
317
- vi.mocked(YouVersionAPIUsers.userInfo).mockReturnValue(mockUserInfo as any);
200
+ vi.spyOn(YouVersionAPIUsers, 'userInfo').mockReturnValue(mockUserInfo);
318
201
 
319
202
  const { getByTestId } = render(
320
203
  <YouVersionAuthProvider config={mockConfig}>
@@ -333,7 +216,7 @@ describe('YouVersionAuthProvider', () => {
333
216
 
334
217
  it('should handle refresh token failure', async () => {
335
218
  YouVersionPlatformConfiguration.saveAuthData(null, 'existing-refresh-token', null, null);
336
- vi.mocked(YouVersionAPIUsers.refreshTokenIfNeeded).mockRejectedValue(
219
+ vi.spyOn(YouVersionAPIUsers, 'refreshTokenIfNeeded').mockRejectedValue(
337
220
  new Error('Refresh failed'),
338
221
  );
339
222
 
@@ -352,7 +235,7 @@ describe('YouVersionAuthProvider', () => {
352
235
 
353
236
  it('should clear user when refresh token exists but no idToken after refresh', async () => {
354
237
  YouVersionPlatformConfiguration.saveAuthData(null, 'existing-refresh-token', null, null);
355
- vi.mocked(YouVersionAPIUsers.refreshTokenIfNeeded).mockResolvedValue(false);
238
+ vi.spyOn(YouVersionAPIUsers, 'refreshTokenIfNeeded').mockResolvedValue(false);
356
239
 
357
240
  const { getByTestId } = render(
358
241
  <YouVersionAuthProvider config={mockConfig}>
@@ -0,0 +1,12 @@
1
+ import type { ReactNode, ComponentType } from 'react';
2
+ import { YouVersionContext } from '../context';
3
+
4
+ export const createYVWrapper = (
5
+ appKey = 'test-app-key',
6
+ { theme }: { theme?: 'light' | 'dark' } = {},
7
+ ): ComponentType<{ children: ReactNode }> => {
8
+ const Wrapper = ({ children }: { children: ReactNode }) => (
9
+ <YouVersionContext.Provider value={{ appKey, theme }}>{children}</YouVersionContext.Provider>
10
+ );
11
+ return Wrapper;
12
+ };
@@ -1,9 +1,10 @@
1
1
  import { renderHook } from '@testing-library/react';
2
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { describe, expect, vi, it } from 'vitest';
3
3
  import type { ReactNode } from 'react';
4
4
  import { useBibleClient } from './useBibleClient';
5
5
  import { YouVersionContext } from './context';
6
6
  import { BibleClient, ApiClient } from '@youversion/platform-core';
7
+ import { createYVWrapper } from './test/utils';
7
8
 
8
9
  vi.mock('@youversion/platform-core', async () => {
9
10
  const actual = await vi.importActual('@youversion/platform-core');
@@ -23,27 +24,12 @@ vi.mock('@youversion/platform-core', async () => {
23
24
  });
24
25
 
25
26
  describe('useBibleClient', () => {
26
- const mockAppKey = 'test-app-key';
27
-
28
- beforeEach(() => {
29
- vi.clearAllMocks();
30
- });
31
-
32
27
  it('should create and return a BibleClient instance when context is valid', () => {
33
- const wrapper = ({ children }: { children: ReactNode }) => (
34
- <YouVersionContext.Provider
35
- value={{
36
- appKey: mockAppKey,
37
- }}
38
- >
39
- {children}
40
- </YouVersionContext.Provider>
41
- );
42
-
28
+ const wrapper = createYVWrapper();
43
29
  const { result } = renderHook(() => useBibleClient(), { wrapper });
44
30
 
45
31
  expect(ApiClient).toHaveBeenCalledWith({
46
- appKey: mockAppKey,
32
+ appKey: 'test-app-key',
47
33
  });
48
34
  expect(BibleClient).toHaveBeenCalledWith(expect.objectContaining({ isApiClient: true }));
49
35
  expect(result.current).toEqual(expect.objectContaining({ isBibleClient: true }));
@@ -57,13 +43,7 @@ describe('useBibleClient', () => {
57
43
 
58
44
  it('should throw error when appKey is missing', () => {
59
45
  const wrapper = ({ children }: { children: ReactNode }) => (
60
- <YouVersionContext.Provider
61
- value={{
62
- appKey: '',
63
- }}
64
- >
65
- {children}
66
- </YouVersionContext.Provider>
46
+ <YouVersionContext.Provider value={{ appKey: '' }}>{children}</YouVersionContext.Provider>
67
47
  );
68
48
 
69
49
  expect(() => renderHook(() => useBibleClient(), { wrapper })).toThrow(
@@ -72,16 +52,7 @@ describe('useBibleClient', () => {
72
52
  });
73
53
 
74
54
  it('should memoize the BibleClient instance', () => {
75
- const wrapper = ({ children }: { children: ReactNode }) => (
76
- <YouVersionContext.Provider
77
- value={{
78
- appKey: mockAppKey,
79
- }}
80
- >
81
- {children}
82
- </YouVersionContext.Provider>
83
- );
84
-
55
+ const wrapper = createYVWrapper();
85
56
  const { result, rerender } = renderHook(() => useBibleClient(), { wrapper });
86
57
  const firstClient = result.current;
87
58
 
@@ -93,7 +64,7 @@ describe('useBibleClient', () => {
93
64
  });
94
65
 
95
66
  it('should create new BibleClient when appKey changes', () => {
96
- let currentAppKey = mockAppKey;
67
+ let currentAppKey = 'test-app-key';
97
68
 
98
69
  const wrapper = ({ children }: { children: ReactNode }) => (
99
70
  <YouVersionContext.Provider
@@ -110,7 +81,7 @@ describe('useBibleClient', () => {
110
81
  const firstClient = result.current;
111
82
  expect(BibleClient).toHaveBeenCalledTimes(1);
112
83
  expect(ApiClient).toHaveBeenCalledWith({
113
- appKey: mockAppKey,
84
+ appKey: 'test-app-key',
114
85
  });
115
86
 
116
87
  currentAppKey = 'new-app-key';