@youversion/platform-react-hooks 0.4.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 +4 -0
- package/CHANGELOG.md +9 -0
- package/LICENSE +201 -0
- package/dist/context/BibleSDKContext.d.ts +6 -0
- package/dist/context/BibleSDKContext.d.ts.map +1 -0
- package/dist/context/BibleSDKContext.js +4 -0
- package/dist/context/BibleSDKContext.js.map +1 -0
- package/dist/context/BibleSDKProvider.d.ts +8 -0
- package/dist/context/BibleSDKProvider.d.ts.map +1 -0
- package/dist/context/BibleSDKProvider.js +7 -0
- package/dist/context/BibleSDKProvider.js.map +1 -0
- package/dist/context/ReaderContext.d.ts +15 -0
- package/dist/context/ReaderContext.d.ts.map +1 -0
- package/dist/context/ReaderContext.js +11 -0
- package/dist/context/ReaderContext.js.map +1 -0
- package/dist/context/ReaderProvider.d.ts +11 -0
- package/dist/context/ReaderProvider.d.ts.map +1 -0
- package/dist/context/ReaderProvider.js +21 -0
- package/dist/context/ReaderProvider.js.map +1 -0
- package/dist/context/VerseSelectionContext.d.ts +9 -0
- package/dist/context/VerseSelectionContext.d.ts.map +1 -0
- package/dist/context/VerseSelectionContext.js +3 -0
- package/dist/context/VerseSelectionContext.js.map +1 -0
- package/dist/context/VerseSelectionProvider.d.ts +3 -0
- package/dist/context/VerseSelectionProvider.d.ts.map +1 -0
- package/dist/context/VerseSelectionProvider.js +33 -0
- package/dist/context/VerseSelectionProvider.js.map +1 -0
- package/dist/context/index.d.ts +7 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +7 -0
- package/dist/context/index.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/test/setup.d.ts +2 -0
- package/dist/test/setup.d.ts.map +1 -0
- package/dist/test/setup.js +9 -0
- package/dist/test/setup.js.map +1 -0
- package/dist/useApiData.d.ts +12 -0
- package/dist/useApiData.d.ts.map +1 -0
- package/dist/useApiData.js +46 -0
- package/dist/useApiData.js.map +1 -0
- package/dist/useBibleClient.d.ts +3 -0
- package/dist/useBibleClient.d.ts.map +1 -0
- package/dist/useBibleClient.js +17 -0
- package/dist/useBibleClient.js.map +1 -0
- package/dist/useBook.d.ts +9 -0
- package/dist/useBook.d.ts.map +1 -0
- package/dist/useBook.js +11 -0
- package/dist/useBook.js.map +1 -0
- package/dist/useBooks.d.ts +9 -0
- package/dist/useBooks.d.ts.map +1 -0
- package/dist/useBooks.js +11 -0
- package/dist/useBooks.js.map +1 -0
- package/dist/useChapter.d.ts +9 -0
- package/dist/useChapter.d.ts.map +1 -0
- package/dist/useChapter.js +11 -0
- package/dist/useChapter.js.map +1 -0
- package/dist/useChapterNavigation.d.ts +23 -0
- package/dist/useChapterNavigation.d.ts.map +1 -0
- package/dist/useChapterNavigation.js +52 -0
- package/dist/useChapterNavigation.js.map +1 -0
- package/dist/useChapters.d.ts +9 -0
- package/dist/useChapters.d.ts.map +1 -0
- package/dist/useChapters.js +13 -0
- package/dist/useChapters.js.map +1 -0
- package/dist/useFilteredVersions.d.ts +6 -0
- package/dist/useFilteredVersions.d.ts.map +1 -0
- package/dist/useFilteredVersions.js +24 -0
- package/dist/useFilteredVersions.js.map +1 -0
- package/dist/useHighlights.d.ts +11 -0
- package/dist/useHighlights.d.ts.map +1 -0
- package/dist/useHighlights.js +40 -0
- package/dist/useHighlights.js.map +1 -0
- package/dist/useInitData.d.ts +23 -0
- package/dist/useInitData.d.ts.map +1 -0
- package/dist/useInitData.js +24 -0
- package/dist/useInitData.js.map +1 -0
- package/dist/useLanguages.d.ts +9 -0
- package/dist/useLanguages.d.ts.map +1 -0
- package/dist/useLanguages.js +29 -0
- package/dist/useLanguages.js.map +1 -0
- package/dist/usePassage.d.ts +18 -0
- package/dist/usePassage.d.ts.map +1 -0
- package/dist/usePassage.js +11 -0
- package/dist/usePassage.js.map +1 -0
- package/dist/useVOTD.d.ts +9 -0
- package/dist/useVOTD.d.ts.map +1 -0
- package/dist/useVOTD.js +9 -0
- package/dist/useVOTD.js.map +1 -0
- package/dist/useVerse.d.ts +9 -0
- package/dist/useVerse.d.ts.map +1 -0
- package/dist/useVerse.js +11 -0
- package/dist/useVerse.js.map +1 -0
- package/dist/useVerseSelection.d.ts +3 -0
- package/dist/useVerseSelection.d.ts.map +1 -0
- package/dist/useVerseSelection.js +10 -0
- package/dist/useVerseSelection.js.map +1 -0
- package/dist/useVerses.d.ts +9 -0
- package/dist/useVerses.d.ts.map +1 -0
- package/dist/useVerses.js +11 -0
- package/dist/useVerses.js.map +1 -0
- package/dist/useVersion.d.ts +9 -0
- package/dist/useVersion.d.ts.map +1 -0
- package/dist/useVersion.js +11 -0
- package/dist/useVersion.js.map +1 -0
- package/dist/useVersions.d.ts +9 -0
- package/dist/useVersions.d.ts.map +1 -0
- package/dist/useVersions.js +11 -0
- package/dist/useVersions.js.map +1 -0
- package/dist/utility/extractTextFromHTML.d.ts +9 -0
- package/dist/utility/extractTextFromHTML.d.ts.map +1 -0
- package/dist/utility/extractTextFromHTML.js +21 -0
- package/dist/utility/extractTextFromHTML.js.map +1 -0
- package/dist/utility/extractVersesFromHTML.d.ts +9 -0
- package/dist/utility/extractVersesFromHTML.d.ts.map +1 -0
- package/dist/utility/extractVersesFromHTML.js +26 -0
- package/dist/utility/extractVersesFromHTML.js.map +1 -0
- package/dist/utility/getDayOfYear.d.ts +2 -0
- package/dist/utility/getDayOfYear.d.ts.map +1 -0
- package/dist/utility/getDayOfYear.js +5 -0
- package/dist/utility/getDayOfYear.js.map +1 -0
- package/dist/utility/index.d.ts +6 -0
- package/dist/utility/index.d.ts.map +1 -0
- package/dist/utility/index.js +6 -0
- package/dist/utility/index.js.map +1 -0
- package/dist/utility/useDebounce.d.ts +14 -0
- package/dist/utility/useDebounce.d.ts.map +1 -0
- package/dist/utility/useDebounce.js +24 -0
- package/dist/utility/useDebounce.js.map +1 -0
- package/dist/utility/version.d.ts +3 -0
- package/dist/utility/version.d.ts.map +1 -0
- package/dist/utility/version.js +4 -0
- package/dist/utility/version.js.map +1 -0
- package/package.json +50 -0
- package/src/context/BibleSDKContext.tsx +9 -0
- package/src/context/BibleSDKProvider.tsx +16 -0
- package/src/context/ReaderContext.tsx +27 -0
- package/src/context/ReaderProvider.tsx +36 -0
- package/src/context/VerseSelectionContext.tsx +11 -0
- package/src/context/VerseSelectionProvider.tsx +39 -0
- package/src/context/index.ts +6 -0
- package/src/index.ts +20 -0
- package/src/test/setup.ts +9 -0
- package/src/useApiData.ts +71 -0
- package/src/useBibleClient.test.tsx +151 -0
- package/src/useBibleClient.ts +22 -0
- package/src/useBook.ts +28 -0
- package/src/useBooks.ts +31 -0
- package/src/useChapter.ts +33 -0
- package/src/useChapterNavigation.ts +77 -0
- package/src/useChapters.ts +36 -0
- package/src/useFilteredVersions.test.tsx +248 -0
- package/src/useFilteredVersions.ts +38 -0
- package/src/useHighlights.test.tsx +448 -0
- package/src/useHighlights.ts +80 -0
- package/src/useInitData.ts +54 -0
- package/src/useLanguages.test.tsx +296 -0
- package/src/useLanguages.ts +57 -0
- package/src/usePassage.ts +48 -0
- package/src/useVOTD.test.tsx +269 -0
- package/src/useVOTD.ts +19 -0
- package/src/useVerse.ts +35 -0
- package/src/useVerseSelection.ts +13 -0
- package/src/useVerses.ts +34 -0
- package/src/useVersion.ts +28 -0
- package/src/useVersions.ts +33 -0
- package/src/utility/extractTextFromHTML.test.ts +112 -0
- package/src/utility/extractTextFromHTML.ts +22 -0
- package/src/utility/extractVersesFromHTML.test.ts +186 -0
- package/src/utility/extractVersesFromHTML.ts +31 -0
- package/src/utility/getDayOfYear.ts +6 -0
- package/src/utility/index.ts +5 -0
- package/src/utility/useDebounce.test.tsx +95 -0
- package/src/utility/useDebounce.ts +27 -0
- package/src/utility/version.ts +5 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +13 -0
- package/vitest.config.ts +11 -0
- package/vitest.setup.ts +1 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { useLanguages } from './useLanguages';
|
|
5
|
+
import { BibleSDKContext } from './context';
|
|
6
|
+
import {
|
|
7
|
+
LanguagesClient,
|
|
8
|
+
ApiClient,
|
|
9
|
+
type Collection,
|
|
10
|
+
type Language,
|
|
11
|
+
type GetLanguagesOptions,
|
|
12
|
+
} from '@youversion/platform-core';
|
|
13
|
+
|
|
14
|
+
// Mock the core package
|
|
15
|
+
vi.mock('@youversion/platform-core', async () => {
|
|
16
|
+
const actual = await vi.importActual('@youversion/platform-core');
|
|
17
|
+
return {
|
|
18
|
+
...actual,
|
|
19
|
+
LanguagesClient: vi.fn(function () {
|
|
20
|
+
return {};
|
|
21
|
+
}),
|
|
22
|
+
ApiClient: vi.fn(function () {
|
|
23
|
+
return { isApiClient: true };
|
|
24
|
+
}),
|
|
25
|
+
YouVersionPlatformConfiguration: {
|
|
26
|
+
get installationId() {
|
|
27
|
+
return 'auto-generated-uuid';
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('useLanguages', () => {
|
|
34
|
+
const mockAppId = 'test-app-id';
|
|
35
|
+
|
|
36
|
+
const mockLanguages: Collection<Language> = {
|
|
37
|
+
data: [
|
|
38
|
+
{
|
|
39
|
+
id: 'en',
|
|
40
|
+
language: 'en',
|
|
41
|
+
script: null,
|
|
42
|
+
script_name: null,
|
|
43
|
+
aliases: [],
|
|
44
|
+
display_names: {
|
|
45
|
+
en: 'English',
|
|
46
|
+
es: 'Inglés',
|
|
47
|
+
},
|
|
48
|
+
scripts: ['Latn'],
|
|
49
|
+
variants: [],
|
|
50
|
+
countries: ['US'],
|
|
51
|
+
text_direction: 'ltr',
|
|
52
|
+
writing_population: 370000000,
|
|
53
|
+
speaking_population: 1500000000,
|
|
54
|
+
default_bible_version_id: 1,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'es',
|
|
58
|
+
language: 'es',
|
|
59
|
+
script: null,
|
|
60
|
+
script_name: null,
|
|
61
|
+
aliases: [],
|
|
62
|
+
display_names: {
|
|
63
|
+
en: 'Spanish',
|
|
64
|
+
es: 'Español',
|
|
65
|
+
},
|
|
66
|
+
scripts: ['Latn'],
|
|
67
|
+
variants: [],
|
|
68
|
+
countries: ['ES'],
|
|
69
|
+
text_direction: 'ltr',
|
|
70
|
+
writing_population: 470000000,
|
|
71
|
+
speaking_population: 580000000,
|
|
72
|
+
default_bible_version_id: 128,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
next_page_token: null,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
let mockGetLanguages: Mock;
|
|
79
|
+
|
|
80
|
+
const createWrapper = (contextValue: { appId: string }) => {
|
|
81
|
+
return ({ children }: { children: ReactNode }) => (
|
|
82
|
+
<BibleSDKContext.Provider value={contextValue}>{children}</BibleSDKContext.Provider>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
vi.clearAllMocks();
|
|
88
|
+
|
|
89
|
+
mockGetLanguages = vi.fn().mockResolvedValue(mockLanguages);
|
|
90
|
+
|
|
91
|
+
(LanguagesClient as unknown as ReturnType<typeof vi.fn>).mockImplementation(function () {
|
|
92
|
+
return {
|
|
93
|
+
getLanguages: mockGetLanguages,
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
(ApiClient as unknown as ReturnType<typeof vi.fn>).mockImplementation(function () {
|
|
98
|
+
return {
|
|
99
|
+
isApiClient: true,
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('context validation', () => {
|
|
105
|
+
it('should throw error when context is not provided', () => {
|
|
106
|
+
expect(() => renderHook(() => useLanguages({ country: 'US' }))).toThrow(
|
|
107
|
+
'BibleSDK context not found. Make sure your component is wrapped with BibleSDKProvider and an API key is provided.',
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should throw error when appId is missing', () => {
|
|
112
|
+
const wrapper = createWrapper({
|
|
113
|
+
appId: '',
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(() => renderHook(() => useLanguages({ country: 'US' }), { wrapper })).toThrow(
|
|
117
|
+
'BibleSDK context not found. Make sure your component is wrapped with BibleSDKProvider and an API key is provided.',
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('client creation', () => {
|
|
123
|
+
it('should create LanguagesClient with correct ApiClient config', () => {
|
|
124
|
+
const wrapper = createWrapper({
|
|
125
|
+
appId: mockAppId,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
renderHook(() => useLanguages({ country: 'US' }), { wrapper });
|
|
129
|
+
|
|
130
|
+
expect(ApiClient).toHaveBeenCalledWith({
|
|
131
|
+
appId: mockAppId,
|
|
132
|
+
installationId: 'auto-generated-uuid',
|
|
133
|
+
});
|
|
134
|
+
expect(LanguagesClient).toHaveBeenCalledWith(expect.objectContaining({ isApiClient: true }));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should memoize LanguagesClient instance', () => {
|
|
138
|
+
const wrapper = createWrapper({
|
|
139
|
+
appId: mockAppId,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const { result, rerender } = renderHook(() => useLanguages({ country: 'US' }), { wrapper });
|
|
143
|
+
const _firstRefetch = result.current.refetch;
|
|
144
|
+
|
|
145
|
+
rerender();
|
|
146
|
+
const _secondRefetch = result.current.refetch;
|
|
147
|
+
|
|
148
|
+
expect(LanguagesClient).toHaveBeenCalledTimes(1);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should create new LanguagesClient when context values change', () => {
|
|
152
|
+
let currentAppId = mockAppId;
|
|
153
|
+
|
|
154
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
155
|
+
<BibleSDKContext.Provider
|
|
156
|
+
value={{
|
|
157
|
+
appId: currentAppId,
|
|
158
|
+
}}
|
|
159
|
+
>
|
|
160
|
+
{children}
|
|
161
|
+
</BibleSDKContext.Provider>
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const { rerender } = renderHook(() => useLanguages({ country: 'US' }), { wrapper });
|
|
165
|
+
|
|
166
|
+
expect(LanguagesClient).toHaveBeenCalledTimes(1);
|
|
167
|
+
|
|
168
|
+
currentAppId = 'new-app-id';
|
|
169
|
+
rerender();
|
|
170
|
+
|
|
171
|
+
expect(LanguagesClient).toHaveBeenCalledTimes(2);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('fetching languages', () => {
|
|
176
|
+
it('should fetch languages with required country', async () => {
|
|
177
|
+
const wrapper = createWrapper({
|
|
178
|
+
appId: mockAppId,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const { result } = renderHook(() => useLanguages({ country: 'US' }), { wrapper });
|
|
182
|
+
|
|
183
|
+
expect(result.current.loading).toBe(true);
|
|
184
|
+
expect(result.current.languages).toBe(null);
|
|
185
|
+
|
|
186
|
+
await waitFor(() => {
|
|
187
|
+
expect(result.current.loading).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(mockGetLanguages).toHaveBeenCalledWith({ country: 'US' });
|
|
191
|
+
expect(result.current.languages).toEqual(mockLanguages);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should fetch languages with all options', async () => {
|
|
195
|
+
const wrapper = createWrapper({
|
|
196
|
+
appId: mockAppId,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const options: GetLanguagesOptions = {
|
|
200
|
+
country: 'US',
|
|
201
|
+
page_size: 10,
|
|
202
|
+
page_token: 'test_token',
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const { result } = renderHook(() => useLanguages(options), { wrapper });
|
|
206
|
+
|
|
207
|
+
await waitFor(() => {
|
|
208
|
+
expect(result.current.loading).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(mockGetLanguages).toHaveBeenCalledWith(options);
|
|
212
|
+
expect(result.current.languages).toEqual(mockLanguages);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should refetch when options change', async () => {
|
|
216
|
+
const wrapper = createWrapper({
|
|
217
|
+
appId: mockAppId,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const { result, rerender } = renderHook(({ options }) => useLanguages(options), {
|
|
221
|
+
wrapper,
|
|
222
|
+
initialProps: { options: { country: 'US' } },
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
await waitFor(() => {
|
|
226
|
+
expect(result.current.loading).toBe(false);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(mockGetLanguages).toHaveBeenCalledTimes(1);
|
|
230
|
+
|
|
231
|
+
rerender({ options: { country: 'ES' } });
|
|
232
|
+
|
|
233
|
+
await waitFor(() => {
|
|
234
|
+
expect(result.current.loading).toBe(false);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(mockGetLanguages).toHaveBeenCalledTimes(2);
|
|
238
|
+
expect(mockGetLanguages).toHaveBeenLastCalledWith({ country: 'ES' });
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should not fetch when enabled is false', async () => {
|
|
242
|
+
const wrapper = createWrapper({
|
|
243
|
+
appId: mockAppId,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const { result } = renderHook(() => useLanguages({ country: 'US' }, { enabled: false }), {
|
|
247
|
+
wrapper,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
await waitFor(() => {
|
|
251
|
+
expect(result.current.loading).toBe(false);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
expect(mockGetLanguages).not.toHaveBeenCalled();
|
|
255
|
+
expect(result.current.languages).toBe(null);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should handle fetch errors', async () => {
|
|
259
|
+
const error = new Error('Failed to fetch languages');
|
|
260
|
+
mockGetLanguages.mockRejectedValueOnce(error);
|
|
261
|
+
|
|
262
|
+
const wrapper = createWrapper({
|
|
263
|
+
appId: mockAppId,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const { result } = renderHook(() => useLanguages({ country: 'US' }), { wrapper });
|
|
267
|
+
|
|
268
|
+
await waitFor(() => {
|
|
269
|
+
expect(result.current.loading).toBe(false);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
expect(result.current.error).toEqual(error);
|
|
273
|
+
expect(result.current.languages).toBe(null);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should support manual refetch', async () => {
|
|
277
|
+
const wrapper = createWrapper({
|
|
278
|
+
appId: mockAppId,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const { result } = renderHook(() => useLanguages({ country: 'US' }), { wrapper });
|
|
282
|
+
|
|
283
|
+
await waitFor(() => {
|
|
284
|
+
expect(result.current.loading).toBe(false);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
expect(mockGetLanguages).toHaveBeenCalledTimes(1);
|
|
288
|
+
|
|
289
|
+
result.current.refetch();
|
|
290
|
+
|
|
291
|
+
await waitFor(() => {
|
|
292
|
+
expect(mockGetLanguages).toHaveBeenCalledTimes(2);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { useContext } from 'react';
|
|
5
|
+
import { BibleSDKContext } from './context';
|
|
6
|
+
import {
|
|
7
|
+
LanguagesClient,
|
|
8
|
+
ApiClient,
|
|
9
|
+
YouVersionPlatformConfiguration,
|
|
10
|
+
} from '@youversion/platform-core';
|
|
11
|
+
import { useApiData, type UseApiDataOptions } from './useApiData';
|
|
12
|
+
import {
|
|
13
|
+
type GetLanguagesOptions,
|
|
14
|
+
type Collection,
|
|
15
|
+
type Language,
|
|
16
|
+
} from '@youversion/platform-core';
|
|
17
|
+
|
|
18
|
+
export function useLanguages(
|
|
19
|
+
options: GetLanguagesOptions,
|
|
20
|
+
apiOptions?: UseApiDataOptions,
|
|
21
|
+
): {
|
|
22
|
+
languages: Collection<Language> | null;
|
|
23
|
+
loading: boolean;
|
|
24
|
+
error: Error | null;
|
|
25
|
+
refetch: () => void;
|
|
26
|
+
} {
|
|
27
|
+
const context = useContext(BibleSDKContext);
|
|
28
|
+
|
|
29
|
+
const languagesClient = useMemo(() => {
|
|
30
|
+
if (!context?.appId) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
'BibleSDK context not found. Make sure your component is wrapped with BibleSDKProvider and an API key is provided.',
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return new LanguagesClient(
|
|
36
|
+
new ApiClient({
|
|
37
|
+
appId: context.appId,
|
|
38
|
+
installationId: YouVersionPlatformConfiguration.installationId,
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
}, [context?.appId]);
|
|
42
|
+
|
|
43
|
+
const { data, loading, error, refetch } = useApiData<Collection<Language>>(
|
|
44
|
+
() => languagesClient.getLanguages(options),
|
|
45
|
+
[languagesClient, options?.country, options?.page_size, options?.page_token],
|
|
46
|
+
{
|
|
47
|
+
enabled: apiOptions?.enabled !== false,
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
languages: data,
|
|
53
|
+
loading,
|
|
54
|
+
error,
|
|
55
|
+
refetch,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useBibleClient } from "./useBibleClient";
|
|
4
|
+
import { useApiData, type UseApiDataOptions } from "./useApiData";
|
|
5
|
+
import type { BiblePassage } from "@youversion/platform-core";
|
|
6
|
+
|
|
7
|
+
type usePassageProps = {
|
|
8
|
+
versionId: number;
|
|
9
|
+
usfm: string;
|
|
10
|
+
format?: "html" | "text";
|
|
11
|
+
include_headings?: boolean;
|
|
12
|
+
include_notes?: boolean;
|
|
13
|
+
options?: UseApiDataOptions;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function usePassage({
|
|
17
|
+
versionId,
|
|
18
|
+
usfm,
|
|
19
|
+
format = "html",
|
|
20
|
+
include_headings = false,
|
|
21
|
+
include_notes = false,
|
|
22
|
+
options,
|
|
23
|
+
}: usePassageProps): {
|
|
24
|
+
passage: BiblePassage | null;
|
|
25
|
+
loading: boolean;
|
|
26
|
+
error: Error | null;
|
|
27
|
+
refetch: () => void;
|
|
28
|
+
} {
|
|
29
|
+
const bibleClient = useBibleClient();
|
|
30
|
+
|
|
31
|
+
// Don't attempt to fetch if usfm is invalid
|
|
32
|
+
const isValidUsfm = Boolean(usfm) && usfm !== "undefined" && usfm !== "null";
|
|
33
|
+
|
|
34
|
+
const { data, loading, error, refetch } = useApiData<BiblePassage>(
|
|
35
|
+
() =>
|
|
36
|
+
bibleClient.getPassage(
|
|
37
|
+
versionId,
|
|
38
|
+
usfm,
|
|
39
|
+
format,
|
|
40
|
+
include_headings,
|
|
41
|
+
include_notes,
|
|
42
|
+
),
|
|
43
|
+
[bibleClient, versionId, usfm, format, include_headings, include_notes],
|
|
44
|
+
{ enabled: options?.enabled !== false && isValidUsfm },
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return { passage: data, loading, error, refetch };
|
|
48
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { useVerseOfTheDay } from './useVOTD';
|
|
5
|
+
import { BibleSDKContext } from './context';
|
|
6
|
+
import { BibleClient, ApiClient, type VOTD } from '@youversion/platform-core';
|
|
7
|
+
|
|
8
|
+
const MOCK_INSTALLATION_ID = 'auto-generated-uuid';
|
|
9
|
+
|
|
10
|
+
// Mock the core package
|
|
11
|
+
vi.mock('@youversion/platform-core', async () => {
|
|
12
|
+
const actual = await vi.importActual('@youversion/platform-core');
|
|
13
|
+
return {
|
|
14
|
+
...actual,
|
|
15
|
+
BibleClient: vi.fn(function () {
|
|
16
|
+
return {};
|
|
17
|
+
}),
|
|
18
|
+
ApiClient: vi.fn(function () {
|
|
19
|
+
return { isApiClient: true };
|
|
20
|
+
}),
|
|
21
|
+
YouVersionPlatformConfiguration: {
|
|
22
|
+
get installationId() {
|
|
23
|
+
return MOCK_INSTALLATION_ID;
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('useVerseOfTheDay', () => {
|
|
30
|
+
const mockAppId = 'test-app-id';
|
|
31
|
+
|
|
32
|
+
const mockVOTD: VOTD = {
|
|
33
|
+
day: 1,
|
|
34
|
+
passage_id: 'ISA.43.19',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
let mockGetVOTD: Mock;
|
|
38
|
+
|
|
39
|
+
const createWrapper = (contextValue: { appId: string }) => {
|
|
40
|
+
return ({ children }: { children: ReactNode }) => (
|
|
41
|
+
<BibleSDKContext.Provider value={contextValue}>{children}</BibleSDKContext.Provider>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
vi.clearAllMocks();
|
|
47
|
+
|
|
48
|
+
mockGetVOTD = vi.fn().mockResolvedValue(mockVOTD);
|
|
49
|
+
|
|
50
|
+
(BibleClient as unknown as ReturnType<typeof vi.fn>).mockImplementation(function () {
|
|
51
|
+
return {
|
|
52
|
+
getVOTD: mockGetVOTD,
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
(ApiClient as unknown as ReturnType<typeof vi.fn>).mockImplementation(function () {
|
|
57
|
+
return {
|
|
58
|
+
isApiClient: true,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('context validation', () => {
|
|
64
|
+
it('should throw error when context is not provided', () => {
|
|
65
|
+
expect(() => renderHook(() => useVerseOfTheDay(1))).toThrow(
|
|
66
|
+
'BibleSDK context not found. Make sure your component is wrapped with BibleSDKProvider and an API key is provided.',
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should throw error when appId is missing', () => {
|
|
71
|
+
const wrapper = createWrapper({
|
|
72
|
+
appId: '',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(() => renderHook(() => useVerseOfTheDay(1), { wrapper })).toThrow(
|
|
76
|
+
'BibleSDK context not found. Make sure your component is wrapped with BibleSDKProvider and an API key is provided.',
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('client creation', () => {
|
|
82
|
+
it('should create BibleClient with correct ApiClient config', () => {
|
|
83
|
+
const wrapper = createWrapper({
|
|
84
|
+
appId: mockAppId,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
renderHook(() => useVerseOfTheDay(1), { wrapper });
|
|
88
|
+
|
|
89
|
+
expect(ApiClient).toHaveBeenCalledWith({
|
|
90
|
+
appId: mockAppId,
|
|
91
|
+
installationId: MOCK_INSTALLATION_ID,
|
|
92
|
+
});
|
|
93
|
+
expect(BibleClient).toHaveBeenCalledWith(expect.objectContaining({ isApiClient: true }));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should memoize BibleClient instance', () => {
|
|
97
|
+
const wrapper = createWrapper({
|
|
98
|
+
appId: mockAppId,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const { result, rerender } = renderHook(() => useVerseOfTheDay(1), { wrapper });
|
|
102
|
+
const _firstRefetch = result.current.refetch;
|
|
103
|
+
|
|
104
|
+
rerender();
|
|
105
|
+
const _secondRefetch = result.current.refetch;
|
|
106
|
+
|
|
107
|
+
expect(BibleClient).toHaveBeenCalledTimes(1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should create new BibleClient when context values change', () => {
|
|
111
|
+
let currentAppId = mockAppId;
|
|
112
|
+
|
|
113
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
114
|
+
<BibleSDKContext.Provider
|
|
115
|
+
value={{
|
|
116
|
+
appId: currentAppId,
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
{children}
|
|
120
|
+
</BibleSDKContext.Provider>
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const { rerender } = renderHook(() => useVerseOfTheDay(1), { wrapper });
|
|
124
|
+
|
|
125
|
+
expect(BibleClient).toHaveBeenCalledTimes(1);
|
|
126
|
+
|
|
127
|
+
currentAppId = 'new-app-id';
|
|
128
|
+
rerender();
|
|
129
|
+
|
|
130
|
+
expect(BibleClient).toHaveBeenCalledTimes(2);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('fetching VOTD', () => {
|
|
135
|
+
it('should fetch VOTD for day 1', async () => {
|
|
136
|
+
const wrapper = createWrapper({
|
|
137
|
+
appId: mockAppId,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const { result } = renderHook(() => useVerseOfTheDay(1), { wrapper });
|
|
141
|
+
|
|
142
|
+
expect(result.current.loading).toBe(true);
|
|
143
|
+
expect(result.current.data).toBe(null);
|
|
144
|
+
|
|
145
|
+
await waitFor(() => {
|
|
146
|
+
expect(result.current.loading).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(mockGetVOTD).toHaveBeenCalledWith(1);
|
|
150
|
+
expect(result.current.data).toEqual(mockVOTD);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should fetch VOTD for day 100', async () => {
|
|
154
|
+
const mockVOTD100: VOTD = { day: 100, passage_id: 'PSA.23.1' };
|
|
155
|
+
mockGetVOTD.mockResolvedValueOnce(mockVOTD100);
|
|
156
|
+
|
|
157
|
+
const wrapper = createWrapper({
|
|
158
|
+
appId: mockAppId,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const { result } = renderHook(() => useVerseOfTheDay(100), { wrapper });
|
|
162
|
+
|
|
163
|
+
await waitFor(() => {
|
|
164
|
+
expect(result.current.loading).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(mockGetVOTD).toHaveBeenCalledWith(100);
|
|
168
|
+
expect(result.current.data).toEqual(mockVOTD100);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should fetch VOTD for day 366', async () => {
|
|
172
|
+
const mockVOTD366: VOTD = { day: 366, passage_id: 'REV.22.21' };
|
|
173
|
+
mockGetVOTD.mockResolvedValueOnce(mockVOTD366);
|
|
174
|
+
|
|
175
|
+
const wrapper = createWrapper({
|
|
176
|
+
appId: mockAppId,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const { result } = renderHook(() => useVerseOfTheDay(366), { wrapper });
|
|
180
|
+
|
|
181
|
+
await waitFor(() => {
|
|
182
|
+
expect(result.current.loading).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(mockGetVOTD).toHaveBeenCalledWith(366);
|
|
186
|
+
expect(result.current.data).toEqual(mockVOTD366);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should refetch when day changes', async () => {
|
|
190
|
+
const wrapper = createWrapper({
|
|
191
|
+
appId: mockAppId,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const { result, rerender } = renderHook(({ day }) => useVerseOfTheDay(day), {
|
|
195
|
+
wrapper,
|
|
196
|
+
initialProps: { day: 1 },
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await waitFor(() => {
|
|
200
|
+
expect(result.current.loading).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(mockGetVOTD).toHaveBeenCalledTimes(1);
|
|
204
|
+
expect(mockGetVOTD).toHaveBeenLastCalledWith(1);
|
|
205
|
+
|
|
206
|
+
rerender({ day: 100 });
|
|
207
|
+
|
|
208
|
+
await waitFor(() => {
|
|
209
|
+
expect(result.current.loading).toBe(false);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
expect(mockGetVOTD).toHaveBeenCalledTimes(2);
|
|
213
|
+
expect(mockGetVOTD).toHaveBeenLastCalledWith(100);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should not fetch when enabled is false', async () => {
|
|
217
|
+
const wrapper = createWrapper({
|
|
218
|
+
appId: mockAppId,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const { result } = renderHook(() => useVerseOfTheDay(1, { enabled: false }), { wrapper });
|
|
222
|
+
|
|
223
|
+
await waitFor(() => {
|
|
224
|
+
expect(result.current.loading).toBe(false);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(mockGetVOTD).not.toHaveBeenCalled();
|
|
228
|
+
expect(result.current.data).toBe(null);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should handle fetch errors', async () => {
|
|
232
|
+
const error = new Error('Failed to fetch VOTD');
|
|
233
|
+
mockGetVOTD.mockRejectedValueOnce(error);
|
|
234
|
+
|
|
235
|
+
const wrapper = createWrapper({
|
|
236
|
+
appId: mockAppId,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const { result } = renderHook(() => useVerseOfTheDay(1), { wrapper });
|
|
240
|
+
|
|
241
|
+
await waitFor(() => {
|
|
242
|
+
expect(result.current.loading).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(result.current.error).toEqual(error);
|
|
246
|
+
expect(result.current.data).toBe(null);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should support manual refetch', async () => {
|
|
250
|
+
const wrapper = createWrapper({
|
|
251
|
+
appId: mockAppId,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const { result } = renderHook(() => useVerseOfTheDay(1), { wrapper });
|
|
255
|
+
|
|
256
|
+
await waitFor(() => {
|
|
257
|
+
expect(result.current.loading).toBe(false);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(mockGetVOTD).toHaveBeenCalledTimes(1);
|
|
261
|
+
|
|
262
|
+
result.current.refetch();
|
|
263
|
+
|
|
264
|
+
await waitFor(() => {
|
|
265
|
+
expect(mockGetVOTD).toHaveBeenCalledTimes(2);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
});
|
package/src/useVOTD.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { VOTD } from '@youversion/platform-core';
|
|
4
|
+
import { useBibleClient } from './useBibleClient';
|
|
5
|
+
import { useApiData, type UseApiDataOptions } from './useApiData';
|
|
6
|
+
|
|
7
|
+
export function useVerseOfTheDay(
|
|
8
|
+
day: number,
|
|
9
|
+
options?: UseApiDataOptions,
|
|
10
|
+
): { data: VOTD | null; loading: boolean; error: Error | null; refetch: () => void } {
|
|
11
|
+
const bibleClient = useBibleClient();
|
|
12
|
+
|
|
13
|
+
const { data, loading, error, refetch } = useApiData<VOTD>(
|
|
14
|
+
() => bibleClient.getVOTD(day),
|
|
15
|
+
[bibleClient, day],
|
|
16
|
+
{ enabled: options?.enabled !== false },
|
|
17
|
+
);
|
|
18
|
+
return { data, loading, error, refetch };
|
|
19
|
+
}
|