From df323dfc9d4b19248fc8de3d3c7aeda81b157bb2 Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Wed, 4 Mar 2026 12:02:11 -0600 Subject: [PATCH 1/8] test(hooks): add comprehensive tests for useInitData Co-Authored-By: Claude Opus 4.6 --- packages/hooks/src/useInitData.test.tsx | 245 ++++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 packages/hooks/src/useInitData.test.tsx diff --git a/packages/hooks/src/useInitData.test.tsx b/packages/hooks/src/useInitData.test.tsx new file mode 100644 index 00000000..1157ef29 --- /dev/null +++ b/packages/hooks/src/useInitData.test.tsx @@ -0,0 +1,245 @@ +import { renderHook } from '@testing-library/react'; +import { describe, expect, vi, beforeEach, it } from 'vitest'; +import { useInitData, DEFAULT } from './useInitData'; +import { useVersion } from './useVersion'; +import { useBook } from './useBook'; +import { useChapter } from './useChapter'; +import { createYVWrapper } from './test/utils'; +import { createMockVersion, createMockBook, createMockChapter } from './__tests__/mocks/bibles'; + +vi.mock('./useVersion'); +vi.mock('./useBook'); +vi.mock('./useChapter'); + +const mockVersion = createMockVersion(); +const mockBook = createMockBook(); +const mockChapter = createMockChapter(); + +function createLoadingState() { + return { loading: true, error: null, refetch: vi.fn() }; +} + +function createErrorState(error: Error) { + return { loading: false, error, refetch: vi.fn() }; +} + +function mockAllLoaded() { + vi.mocked(useVersion).mockReturnValue({ + version: mockVersion, + loading: false, + error: null, + refetch: vi.fn(), + }); + vi.mocked(useBook).mockReturnValue({ + book: mockBook, + loading: false, + error: null, + refetch: vi.fn(), + }); + vi.mocked(useChapter).mockReturnValue({ + chapter: mockChapter, + loading: false, + error: null, + refetch: vi.fn(), + }); +} + +function mockAllLoading() { + vi.mocked(useVersion).mockReturnValue({ + ...createLoadingState(), + version: null, + }); + vi.mocked(useBook).mockReturnValue({ + ...createLoadingState(), + book: null, + }); + vi.mocked(useChapter).mockReturnValue({ + ...createLoadingState(), + chapter: null, + }); +} + +describe('useInitData', () => { + beforeEach(() => { + mockAllLoaded(); + }); + + describe('loading state aggregation', () => { + it('should return loading=false when all sub-hooks are done', () => { + const wrapper = createYVWrapper(); + const { result } = renderHook(() => useInitData(), { wrapper }); + expect(result.current.loading).toBe(false); + }); + + it.each([ + { + hook: 'useVersion', + setup: () => + vi.mocked(useVersion).mockReturnValue({ + ...createLoadingState(), + version: null, + }), + }, + { + hook: 'useBook', + setup: () => + vi.mocked(useBook).mockReturnValue({ + ...createLoadingState(), + book: null, + }), + }, + { + hook: 'useChapter', + setup: () => + vi.mocked(useChapter).mockReturnValue({ + ...createLoadingState(), + chapter: null, + }), + }, + ])('should return loading=true when $hook is loading', ({ setup }) => { + setup(); + const wrapper = createYVWrapper(); + const { result } = renderHook(() => useInitData(), { wrapper }); + expect(result.current.loading).toBe(true); + }); + + it('should return loading=true when all sub-hooks are loading', () => { + mockAllLoading(); + const wrapper = createYVWrapper(); + const { result } = renderHook(() => useInitData(), { wrapper }); + expect(result.current.loading).toBe(true); + }); + }); + + describe('data composition', () => { + it('should return combined data when all sub-hooks succeed', () => { + const wrapper = createYVWrapper(); + const { result } = renderHook(() => useInitData(), { wrapper }); + + expect.soft(result.current.data).toEqual({ + version: mockVersion, + book: mockBook, + chapter: mockChapter, + }); + }); + + it.each([ + { + missing: 'version', + setup: () => + vi.mocked(useVersion).mockReturnValue({ + version: null, + loading: false, + error: null, + refetch: vi.fn(), + }), + }, + { + missing: 'book', + setup: () => + vi.mocked(useBook).mockReturnValue({ + book: null, + loading: false, + error: null, + refetch: vi.fn(), + }), + }, + { + missing: 'chapter', + setup: () => + vi.mocked(useChapter).mockReturnValue({ + chapter: null, + loading: false, + error: null, + refetch: vi.fn(), + }), + }, + ])('should return data=null when $missing is missing', ({ setup }) => { + setup(); + const wrapper = createYVWrapper(); + const { result } = renderHook(() => useInitData(), { wrapper }); + expect(result.current.data).toBe(null); + }); + }); + + describe('error aggregation', () => { + it('should return empty string when no errors', () => { + const wrapper = createYVWrapper(); + const { result } = renderHook(() => useInitData(), { wrapper }); + expect(result.current.error).toBe(''); + }); + + it('should return single error as string', () => { + vi.mocked(useVersion).mockReturnValue({ + ...createErrorState(new Error('version failed')), + version: null, + }); + + const wrapper = createYVWrapper(); + const { result } = renderHook(() => useInitData(), { wrapper }); + expect(result.current.error).toBe('Error: version failed'); + }); + + it('should join multiple errors with space', () => { + vi.mocked(useVersion).mockReturnValue({ + ...createErrorState(new Error('version failed')), + version: null, + }); + vi.mocked(useBook).mockReturnValue({ + ...createErrorState(new Error('book failed')), + book: null, + }); + + const wrapper = createYVWrapper(); + const { result } = renderHook(() => useInitData(), { wrapper }); + expect(result.current.error).toBe('Error: version failed Error: book failed'); + }); + + it('should join all three errors when all fail', () => { + vi.mocked(useVersion).mockReturnValue({ + ...createErrorState(new Error('v')), + version: null, + }); + vi.mocked(useBook).mockReturnValue({ + ...createErrorState(new Error('b')), + book: null, + }); + vi.mocked(useChapter).mockReturnValue({ + ...createErrorState(new Error('c')), + chapter: null, + }); + + const wrapper = createYVWrapper(); + const { result } = renderHook(() => useInitData(), { wrapper }); + expect(result.current.error).toBe('Error: v Error: b Error: c'); + }); + }); + + describe('default parameters', () => { + it('should use defaults when no args provided', () => { + const wrapper = createYVWrapper(); + renderHook(() => useInitData(), { wrapper }); + + expect.soft(useVersion).toHaveBeenCalledWith(DEFAULT.VERSION); + expect.soft(useBook).toHaveBeenCalledWith(DEFAULT.VERSION, DEFAULT.BOOK); + expect.soft(useChapter).toHaveBeenCalledWith(DEFAULT.VERSION, DEFAULT.BOOK, DEFAULT.CHAPTER); + }); + + it('should have correct default values', () => { + expect.soft(DEFAULT.VERSION).toBe(3034); + expect.soft(DEFAULT.BOOK).toBe('GEN'); + expect.soft(DEFAULT.CHAPTER).toBe(1); + }); + }); + + describe('custom parameters', () => { + it('should pass custom params to sub-hooks', () => { + const wrapper = createYVWrapper(); + renderHook(() => useInitData({ version: 111, book: 'MAT', chapter: 5 }), { wrapper }); + + expect.soft(useVersion).toHaveBeenCalledWith(111); + expect.soft(useBook).toHaveBeenCalledWith(111, 'MAT'); + expect.soft(useChapter).toHaveBeenCalledWith(111, 'MAT', 5); + }); + }); +}); From 183fcfa24f876fc2f995c88238b552a61298f216 Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Wed, 4 Mar 2026 12:25:15 -0600 Subject: [PATCH 2/8] deprecate(hooks): mark useInitData as deprecated and remove tests - Add @deprecated JSDoc directing users to useVersion, useBook, useChapter - Remove useInitData.test.tsx (no tests for deprecated code) - Exclude useInitData.ts from coverage reporting Amp-Thread-ID: https://ampcode.com/threads/T-019cba0e-451d-74d4-9208-f6958c558b50 Co-authored-by: Amp --- packages/hooks/src/useInitData.test.tsx | 245 ------------------------ packages/hooks/src/useInitData.ts | 4 + packages/hooks/vitest.config.ts | 1 + 3 files changed, 5 insertions(+), 245 deletions(-) delete mode 100644 packages/hooks/src/useInitData.test.tsx diff --git a/packages/hooks/src/useInitData.test.tsx b/packages/hooks/src/useInitData.test.tsx deleted file mode 100644 index 1157ef29..00000000 --- a/packages/hooks/src/useInitData.test.tsx +++ /dev/null @@ -1,245 +0,0 @@ -import { renderHook } from '@testing-library/react'; -import { describe, expect, vi, beforeEach, it } from 'vitest'; -import { useInitData, DEFAULT } from './useInitData'; -import { useVersion } from './useVersion'; -import { useBook } from './useBook'; -import { useChapter } from './useChapter'; -import { createYVWrapper } from './test/utils'; -import { createMockVersion, createMockBook, createMockChapter } from './__tests__/mocks/bibles'; - -vi.mock('./useVersion'); -vi.mock('./useBook'); -vi.mock('./useChapter'); - -const mockVersion = createMockVersion(); -const mockBook = createMockBook(); -const mockChapter = createMockChapter(); - -function createLoadingState() { - return { loading: true, error: null, refetch: vi.fn() }; -} - -function createErrorState(error: Error) { - return { loading: false, error, refetch: vi.fn() }; -} - -function mockAllLoaded() { - vi.mocked(useVersion).mockReturnValue({ - version: mockVersion, - loading: false, - error: null, - refetch: vi.fn(), - }); - vi.mocked(useBook).mockReturnValue({ - book: mockBook, - loading: false, - error: null, - refetch: vi.fn(), - }); - vi.mocked(useChapter).mockReturnValue({ - chapter: mockChapter, - loading: false, - error: null, - refetch: vi.fn(), - }); -} - -function mockAllLoading() { - vi.mocked(useVersion).mockReturnValue({ - ...createLoadingState(), - version: null, - }); - vi.mocked(useBook).mockReturnValue({ - ...createLoadingState(), - book: null, - }); - vi.mocked(useChapter).mockReturnValue({ - ...createLoadingState(), - chapter: null, - }); -} - -describe('useInitData', () => { - beforeEach(() => { - mockAllLoaded(); - }); - - describe('loading state aggregation', () => { - it('should return loading=false when all sub-hooks are done', () => { - const wrapper = createYVWrapper(); - const { result } = renderHook(() => useInitData(), { wrapper }); - expect(result.current.loading).toBe(false); - }); - - it.each([ - { - hook: 'useVersion', - setup: () => - vi.mocked(useVersion).mockReturnValue({ - ...createLoadingState(), - version: null, - }), - }, - { - hook: 'useBook', - setup: () => - vi.mocked(useBook).mockReturnValue({ - ...createLoadingState(), - book: null, - }), - }, - { - hook: 'useChapter', - setup: () => - vi.mocked(useChapter).mockReturnValue({ - ...createLoadingState(), - chapter: null, - }), - }, - ])('should return loading=true when $hook is loading', ({ setup }) => { - setup(); - const wrapper = createYVWrapper(); - const { result } = renderHook(() => useInitData(), { wrapper }); - expect(result.current.loading).toBe(true); - }); - - it('should return loading=true when all sub-hooks are loading', () => { - mockAllLoading(); - const wrapper = createYVWrapper(); - const { result } = renderHook(() => useInitData(), { wrapper }); - expect(result.current.loading).toBe(true); - }); - }); - - describe('data composition', () => { - it('should return combined data when all sub-hooks succeed', () => { - const wrapper = createYVWrapper(); - const { result } = renderHook(() => useInitData(), { wrapper }); - - expect.soft(result.current.data).toEqual({ - version: mockVersion, - book: mockBook, - chapter: mockChapter, - }); - }); - - it.each([ - { - missing: 'version', - setup: () => - vi.mocked(useVersion).mockReturnValue({ - version: null, - loading: false, - error: null, - refetch: vi.fn(), - }), - }, - { - missing: 'book', - setup: () => - vi.mocked(useBook).mockReturnValue({ - book: null, - loading: false, - error: null, - refetch: vi.fn(), - }), - }, - { - missing: 'chapter', - setup: () => - vi.mocked(useChapter).mockReturnValue({ - chapter: null, - loading: false, - error: null, - refetch: vi.fn(), - }), - }, - ])('should return data=null when $missing is missing', ({ setup }) => { - setup(); - const wrapper = createYVWrapper(); - const { result } = renderHook(() => useInitData(), { wrapper }); - expect(result.current.data).toBe(null); - }); - }); - - describe('error aggregation', () => { - it('should return empty string when no errors', () => { - const wrapper = createYVWrapper(); - const { result } = renderHook(() => useInitData(), { wrapper }); - expect(result.current.error).toBe(''); - }); - - it('should return single error as string', () => { - vi.mocked(useVersion).mockReturnValue({ - ...createErrorState(new Error('version failed')), - version: null, - }); - - const wrapper = createYVWrapper(); - const { result } = renderHook(() => useInitData(), { wrapper }); - expect(result.current.error).toBe('Error: version failed'); - }); - - it('should join multiple errors with space', () => { - vi.mocked(useVersion).mockReturnValue({ - ...createErrorState(new Error('version failed')), - version: null, - }); - vi.mocked(useBook).mockReturnValue({ - ...createErrorState(new Error('book failed')), - book: null, - }); - - const wrapper = createYVWrapper(); - const { result } = renderHook(() => useInitData(), { wrapper }); - expect(result.current.error).toBe('Error: version failed Error: book failed'); - }); - - it('should join all three errors when all fail', () => { - vi.mocked(useVersion).mockReturnValue({ - ...createErrorState(new Error('v')), - version: null, - }); - vi.mocked(useBook).mockReturnValue({ - ...createErrorState(new Error('b')), - book: null, - }); - vi.mocked(useChapter).mockReturnValue({ - ...createErrorState(new Error('c')), - chapter: null, - }); - - const wrapper = createYVWrapper(); - const { result } = renderHook(() => useInitData(), { wrapper }); - expect(result.current.error).toBe('Error: v Error: b Error: c'); - }); - }); - - describe('default parameters', () => { - it('should use defaults when no args provided', () => { - const wrapper = createYVWrapper(); - renderHook(() => useInitData(), { wrapper }); - - expect.soft(useVersion).toHaveBeenCalledWith(DEFAULT.VERSION); - expect.soft(useBook).toHaveBeenCalledWith(DEFAULT.VERSION, DEFAULT.BOOK); - expect.soft(useChapter).toHaveBeenCalledWith(DEFAULT.VERSION, DEFAULT.BOOK, DEFAULT.CHAPTER); - }); - - it('should have correct default values', () => { - expect.soft(DEFAULT.VERSION).toBe(3034); - expect.soft(DEFAULT.BOOK).toBe('GEN'); - expect.soft(DEFAULT.CHAPTER).toBe(1); - }); - }); - - describe('custom parameters', () => { - it('should pass custom params to sub-hooks', () => { - const wrapper = createYVWrapper(); - renderHook(() => useInitData({ version: 111, book: 'MAT', chapter: 5 }), { wrapper }); - - expect.soft(useVersion).toHaveBeenCalledWith(111); - expect.soft(useBook).toHaveBeenCalledWith(111, 'MAT'); - expect.soft(useChapter).toHaveBeenCalledWith(111, 'MAT', 5); - }); - }); -}); diff --git a/packages/hooks/src/useInitData.ts b/packages/hooks/src/useInitData.ts index 062ec800..5120b5d5 100644 --- a/packages/hooks/src/useInitData.ts +++ b/packages/hooks/src/useInitData.ts @@ -22,6 +22,10 @@ interface InitData { chapter: BibleChapter; } +/** + * @deprecated This hook will be removed in the next major version. + * Use `useVersion`, `useBook`, and `useChapter` directly instead. + */ export function useInitData( { version, book, chapter }: Props = { version: DEFAULT.VERSION, diff --git a/packages/hooks/vitest.config.ts b/packages/hooks/vitest.config.ts index 89b8a63d..b63cb838 100644 --- a/packages/hooks/vitest.config.ts +++ b/packages/hooks/vitest.config.ts @@ -16,6 +16,7 @@ export default defineConfig({ reportsDirectory: './coverage', all: true, include: ['src/**/*.{ts,tsx}'], + exclude: ['src/useInitData.ts'], }, }, }); From f2807801cf5eca065dec5b6fd79bdf2dfbee38b5 Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Wed, 4 Mar 2026 12:46:00 -0600 Subject: [PATCH 3/8] deprecate(hooks): mark unused hooks, providers, and contexts for removal Deprecate dead code not used by the UI package or any known consumers: - useChapterNavigation (UI uses getAdjacentChapter from core directly) - ReaderProvider, ReaderContext, useReaderContext (UI has its own BibleReaderContext) - VerseSelectionProvider, VerseSelectionContext, useVerseSelection (UI handles selection via props) Remove tests for deprecated code and exclude from coverage. Amp-Thread-ID: https://ampcode.com/threads/T-019cba0e-451d-74d4-9208-f6958c558b50 Co-authored-by: Amp --- .../hooks/src/context/ReaderContext.test.tsx | 153 -------- packages/hooks/src/context/ReaderContext.tsx | 6 + .../hooks/src/context/ReaderProvider.test.tsx | 264 ------------- packages/hooks/src/context/ReaderProvider.tsx | 3 + .../src/context/VerseSelectionContext.tsx | 3 + .../context/VerseSelectionProvider.test.tsx | 362 ------------------ .../src/context/VerseSelectionProvider.tsx | 3 + .../hooks/src/useChapterNavigation.test.tsx | 161 -------- packages/hooks/src/useChapterNavigation.ts | 4 + packages/hooks/src/useVerseSelection.test.tsx | 33 -- packages/hooks/src/useVerseSelection.ts | 3 + packages/hooks/vitest.config.ts | 10 +- 12 files changed, 31 insertions(+), 974 deletions(-) delete mode 100644 packages/hooks/src/context/ReaderContext.test.tsx delete mode 100644 packages/hooks/src/context/ReaderProvider.test.tsx delete mode 100644 packages/hooks/src/context/VerseSelectionProvider.test.tsx delete mode 100644 packages/hooks/src/useChapterNavigation.test.tsx delete mode 100644 packages/hooks/src/useVerseSelection.test.tsx diff --git a/packages/hooks/src/context/ReaderContext.test.tsx b/packages/hooks/src/context/ReaderContext.test.tsx deleted file mode 100644 index 4b441a41..00000000 --- a/packages/hooks/src/context/ReaderContext.test.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { describe, it, expect, vi } from 'vitest'; -import { renderHook, act } from '@testing-library/react'; -import React, { useContext } from 'react'; -import { ReaderContext, useReaderContext } from './ReaderContext'; -import { ReaderProvider } from './ReaderProvider'; -import { - createMockBook, - createMockChapter, - createMockVerse, - createMockVersion, -} from '../__tests__/mocks/bibles'; - -const mockVersion = createMockVersion(); -const mockBook = createMockBook(); -const mockChapter = createMockChapter(); -const mockVerse = createMockVerse(); - -const mockVersion2 = createMockVersion({ - id: 2, - title: 'New International Version', - abbreviation: 'NIV', - localized_abbreviation: 'NIV', - localized_title: 'New International Version', - youversion_deep_link: 'https://www.bible.com/versions/2', -}); - -const mockBook2 = createMockBook({ - id: 'JHN', - title: 'John', - full_title: 'The Gospel According to John', - abbreviation: 'Jn', - canon: 'new_testament', -}); - -const mockChapter2 = createMockChapter({ - id: '3', - passage_id: 'JHN.3', - title: '3', -}); - -const mockVerse2 = createMockVerse({ - id: '16', - passage_id: 'JHN.3.16', - title: '16', -}); - -const wrapper = ({ children }: { children: React.ReactNode }) => ( - - {children} - -); - -describe('ReaderContext', () => { - describe('createContext default', () => { - it('should have null as default context value', () => { - const { result } = renderHook(() => useContext(ReaderContext)); - - expect(result.current).toBeNull(); - }); - }); - - describe('useReaderContext', () => { - it('should throw error when used outside ReaderProvider', () => { - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined); - - expect(() => renderHook(() => useReaderContext())).toThrow( - 'useReaderContext() must be used within a ReaderProvider', - ); - - consoleError.mockRestore(); - }); - - it('should return context with all expected keys', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper }); - const keys = Object.keys(result.current); - - expect(keys).toContain('currentVersion'); - expect(keys).toContain('currentChapter'); - expect(keys).toContain('currentBook'); - expect(keys).toContain('currentVerse'); - expect(keys).toContain('setVersion'); - expect(keys).toContain('setChapter'); - expect(keys).toContain('setBook'); - expect(keys).toContain('setVerse'); - }); - - it('should return functions for all setters', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper }); - - expect(typeof result.current.setVersion).toBe('function'); - expect(typeof result.current.setChapter).toBe('function'); - expect(typeof result.current.setBook).toBe('function'); - expect(typeof result.current.setVerse).toBe('function'); - }); - - describe('setter updates', () => { - it('should update currentVersion when setVersion is called', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper }); - - act(() => { - result.current.setVersion(mockVersion2); - }); - - expect(result.current.currentVersion).toEqual(mockVersion2); - }); - - it('should update currentBook when setBook is called', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper }); - - act(() => { - result.current.setBook(mockBook2); - }); - - expect(result.current.currentBook).toEqual(mockBook2); - }); - - it('should update currentChapter when setChapter is called', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper }); - - act(() => { - result.current.setChapter(mockChapter2); - }); - - expect(result.current.currentChapter).toEqual(mockChapter2); - }); - - it('should update currentVerse when setVerse is called', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper }); - - act(() => { - result.current.setVerse(mockVerse2); - }); - - expect(result.current.currentVerse).toEqual(mockVerse2); - }); - - it('should set currentVerse to null', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper }); - - act(() => { - result.current.setVerse(null); - }); - - expect(result.current.currentVerse).toBeNull(); - }); - }); - }); -}); diff --git a/packages/hooks/src/context/ReaderContext.tsx b/packages/hooks/src/context/ReaderContext.tsx index 1b92b067..6cf33c30 100644 --- a/packages/hooks/src/context/ReaderContext.tsx +++ b/packages/hooks/src/context/ReaderContext.tsx @@ -14,8 +14,14 @@ type ReaderContextData = { setVerse: (verse: BibleVerse | null) => void; }; +/** + * @deprecated ReaderContext will be removed in the next major version. + */ export const ReaderContext = createContext(null); +/** + * @deprecated This hook will be removed in the next major version. + */ export function useReaderContext(): ReaderContextData { const context = useContext(ReaderContext); diff --git a/packages/hooks/src/context/ReaderProvider.test.tsx b/packages/hooks/src/context/ReaderProvider.test.tsx deleted file mode 100644 index 1a992fe6..00000000 --- a/packages/hooks/src/context/ReaderProvider.test.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import { describe, it, expect, vi } from 'vitest'; -import { renderHook, act } from '@testing-library/react'; -import React from 'react'; -import { useReaderContext } from './ReaderContext'; -import { ReaderProvider } from './ReaderProvider'; -import { - createMockBook, - createMockChapter, - createMockVerse, - createMockVersion, -} from '../__tests__/mocks/bibles'; - -// Mock Bible data -const mockVersion = createMockVersion(); -const mockBook = createMockBook(); -const mockChapter = createMockChapter(); -const mockVerse = createMockVerse(); - -// Alternative mock data for update tests -const mockVersion2 = createMockVersion({ - id: 2, - title: 'New International Version', - abbreviation: 'NIV', - localized_abbreviation: 'NIV', - localized_title: 'New International Version', - youversion_deep_link: 'https://www.bible.com/versions/2', -}); - -const mockBook2 = createMockBook({ - id: 'JHN', - title: 'John', - full_title: 'The Gospel According to John', - abbreviation: 'Jn', - canon: 'new_testament', -}); - -const mockChapter2 = createMockChapter({ - id: '3', - passage_id: 'JHN.3', - title: '3', -}); - -const mockVerse2 = createMockVerse({ - id: '16', - passage_id: 'JHN.3.16', - title: '16', -}); - -// Test wrappers -const wrapper = ({ children }: { children: React.ReactNode }) => ( - - {children} - -); - -const wrapperWithNullVerse = ({ children }: { children: React.ReactNode }) => ( - - {children} - -); - -describe('ReaderProvider', () => { - describe('initialization', () => { - it('should initialize with provided version', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper }); - - expect(result.current.currentVersion).toEqual(mockVersion); - }); - - it('should initialize with provided book', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper }); - - expect(result.current.currentBook).toEqual(mockBook); - }); - - it('should initialize with provided chapter', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper }); - - expect(result.current.currentChapter).toEqual(mockChapter); - }); - - it('should initialize with provided verse', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper }); - - expect(result.current.currentVerse).toEqual(mockVerse); - }); - - it('should initialize with null verse when provided', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper: wrapperWithNullVerse }); - - expect(result.current.currentVerse).toBeNull(); - }); - - it('should throw error when useReaderContext is used outside provider', () => { - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined); - - expect(() => renderHook(() => useReaderContext())).toThrow( - 'useReaderContext() must be used within a ReaderProvider', - ); - - consoleError.mockRestore(); - }); - }); - - describe('setVersion', () => { - it('should update version when setVersion is called', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper }); - - act(() => { - result.current.setVersion(mockVersion2); - }); - - expect(result.current.currentVersion).toEqual(mockVersion2); - }); - - it('should preserve other state when version changes', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper }); - - act(() => { - result.current.setVersion(mockVersion2); - }); - - expect(result.current.currentVersion).toMatchObject({ abbreviation: 'NIV' }); - expect(result.current.currentBook).toEqual(mockBook); - expect(result.current.currentChapter).toEqual(mockChapter); - expect(result.current.currentVerse).toEqual(mockVerse); - }); - }); - - describe('setBook', () => { - it('should update book when setBook is called', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper }); - - act(() => { - result.current.setBook(mockBook2); - }); - - expect(result.current.currentBook).toEqual(mockBook2); - }); - - it('should preserve other state when book changes', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper }); - - act(() => { - result.current.setBook(mockBook2); - }); - - expect(result.current.currentBook).toMatchObject({ id: 'JHN' }); - expect(result.current.currentVersion).toEqual(mockVersion); - expect(result.current.currentChapter).toEqual(mockChapter); - expect(result.current.currentVerse).toEqual(mockVerse); - }); - }); - - describe('setChapter', () => { - it('should update chapter when setChapter is called', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper }); - - act(() => { - result.current.setChapter(mockChapter2); - }); - - expect(result.current.currentChapter).toEqual(mockChapter2); - }); - - it('should preserve other state when chapter changes', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper }); - - act(() => { - result.current.setChapter(mockChapter2); - }); - - expect(result.current.currentChapter).toMatchObject({ id: '3' }); - expect(result.current.currentVersion).toEqual(mockVersion); - expect(result.current.currentBook).toEqual(mockBook); - expect(result.current.currentVerse).toEqual(mockVerse); - }); - }); - - describe('setVerse', () => { - it('should update verse when setVerse is called', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper }); - - act(() => { - result.current.setVerse(mockVerse2); - }); - - expect(result.current.currentVerse).toEqual(mockVerse2); - }); - - it('should update verse to null when setVerse is called with null', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper }); - - act(() => { - result.current.setVerse(null); - }); - - expect(result.current.currentVerse).toBeNull(); - }); - - it('should preserve other state when verse changes', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper }); - - act(() => { - result.current.setVerse(mockVerse2); - }); - - expect(result.current.currentVerse).toMatchObject({ id: '16' }); - expect(result.current.currentVersion).toEqual(mockVersion); - expect(result.current.currentBook).toEqual(mockBook); - expect(result.current.currentChapter).toEqual(mockChapter); - }); - - it('should preserve other state when verse changes to null', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper }); - - act(() => { - result.current.setVerse(null); - }); - - expect(result.current.currentVerse).toBeNull(); - expect(result.current.currentVersion).toEqual(mockVersion); - expect(result.current.currentBook).toEqual(mockBook); - expect(result.current.currentChapter).toEqual(mockChapter); - }); - }); - - describe('state persistence', () => { - it('should initialize with all provided props', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper }); - - expect(result.current.currentVersion).toEqual(mockVersion); - expect(result.current.currentBook).toEqual(mockBook); - expect(result.current.currentChapter).toEqual(mockChapter); - expect(result.current.currentVerse).toEqual(mockVerse); - }); - - it('should handle rapid successive state updates without corruption', () => { - const { result } = renderHook(() => useReaderContext(), { wrapper }); - - act(() => { - result.current.setVersion(mockVersion2); - result.current.setBook(mockBook2); - result.current.setChapter(mockChapter2); - result.current.setVerse(mockVerse2); - }); - - expect(result.current.currentVersion).toEqual(mockVersion2); - expect(result.current.currentBook).toEqual(mockBook2); - expect(result.current.currentChapter).toEqual(mockChapter2); - expect(result.current.currentVerse).toEqual(mockVerse2); - }); - }); -}); diff --git a/packages/hooks/src/context/ReaderProvider.tsx b/packages/hooks/src/context/ReaderProvider.tsx index 1f07347b..18325cfa 100644 --- a/packages/hooks/src/context/ReaderProvider.tsx +++ b/packages/hooks/src/context/ReaderProvider.tsx @@ -11,6 +11,9 @@ type ReaderProviderProps = { currentVerse: BibleVerse | null; }; +/** + * @deprecated ReaderProvider will be removed in the next major version. + */ export function ReaderProvider(props: PropsWithChildren): React.ReactElement { const [currentVersion, setCurrentVersion] = useState(props.currentVersion); const [currentBook, setCurrentBook] = useState(props.currentBook); diff --git a/packages/hooks/src/context/VerseSelectionContext.tsx b/packages/hooks/src/context/VerseSelectionContext.tsx index 86eacf38..417542b3 100644 --- a/packages/hooks/src/context/VerseSelectionContext.tsx +++ b/packages/hooks/src/context/VerseSelectionContext.tsx @@ -8,4 +8,7 @@ export type VerseSelectionContextData = { selectedCount: number; }; +/** + * @deprecated VerseSelectionContext will be removed in the next major version. + */ export const VerseSelectionContext = createContext(null); diff --git a/packages/hooks/src/context/VerseSelectionProvider.test.tsx b/packages/hooks/src/context/VerseSelectionProvider.test.tsx deleted file mode 100644 index ed825ef5..00000000 --- a/packages/hooks/src/context/VerseSelectionProvider.test.tsx +++ /dev/null @@ -1,362 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { renderHook, act } from '@testing-library/react'; -import type { ReactNode } from 'react'; -import { VerseSelectionProvider } from './VerseSelectionProvider'; -import { useVerseSelection } from '../useVerseSelection'; - -// Wrapper for renderHook -const wrapper = ({ children }: { children: ReactNode }) => ( - {children} -); - -describe('VerseSelectionProvider', () => { - describe('initial state', () => { - it('should provide an empty Set instance', () => { - const { result } = renderHook(() => useVerseSelection(), { wrapper }); - - expect(result.current.selectedVerseUsfms).toBeInstanceOf(Set); - expect(result.current.selectedVerseUsfms.size).toBe(0); - }); - - it('should have selectedCount of 0', () => { - const { result } = renderHook(() => useVerseSelection(), { wrapper }); - - expect(result.current.selectedCount).toBe(0); - }); - - it('should return false for isSelected on any verse', () => { - const { result } = renderHook(() => useVerseSelection(), { wrapper }); - - expect(result.current.isSelected('MAT.1.1')).toBe(false); - expect(result.current.isSelected('GEN.1.1')).toBe(false); - }); - }); - - describe('toggleVerse', () => { - it('should add verse to selection when not present', () => { - const { result } = renderHook(() => useVerseSelection(), { wrapper }); - - act(() => { - result.current.toggleVerse('MAT.1.1'); - }); - - expect(result.current.selectedVerseUsfms.has('MAT.1.1')).toBe(true); - expect(result.current.selectedCount).toBe(1); - }); - - it('should remove verse from selection when already present', () => { - const { result } = renderHook(() => useVerseSelection(), { wrapper }); - - act(() => { - result.current.toggleVerse('MAT.1.1'); - }); - - expect(result.current.selectedCount).toBe(1); - - act(() => { - result.current.toggleVerse('MAT.1.1'); - }); - - expect(result.current.selectedVerseUsfms.has('MAT.1.1')).toBe(false); - expect(result.current.selectedCount).toBe(0); - }); - - it('should support multiple verses selected simultaneously', () => { - const { result } = renderHook(() => useVerseSelection(), { wrapper }); - - act(() => { - result.current.toggleVerse('MAT.1.1'); - result.current.toggleVerse('GEN.1.1'); - result.current.toggleVerse('JHN.3.16'); - }); - - expect(result.current.selectedCount).toBe(3); - expect(result.current.selectedVerseUsfms.has('MAT.1.1')).toBe(true); - expect(result.current.selectedVerseUsfms.has('GEN.1.1')).toBe(true); - expect(result.current.selectedVerseUsfms.has('JHN.3.16')).toBe(true); - }); - - it('should create new Set reference on each update (immutability)', () => { - const { result } = renderHook(() => useVerseSelection(), { wrapper }); - - const set1 = result.current.selectedVerseUsfms; - - act(() => { - result.current.toggleVerse('MAT.1.1'); - }); - - const set2 = result.current.selectedVerseUsfms; - - expect(set1).not.toBe(set2); - expect(set1.size).toBe(0); - expect(set2.size).toBe(1); - }); - - it('should create new Set reference when removing a verse', () => { - const { result } = renderHook(() => useVerseSelection(), { wrapper }); - - act(() => { - result.current.toggleVerse('MAT.1.1'); - }); - - const setWithVerse = result.current.selectedVerseUsfms; - - act(() => { - result.current.toggleVerse('MAT.1.1'); - }); - - const setWithoutVerse = result.current.selectedVerseUsfms; - - expect(setWithVerse).not.toBe(setWithoutVerse); - expect(setWithVerse.size).toBe(1); - expect(setWithoutVerse.size).toBe(0); - }); - - it('should not add duplicate verses', () => { - const { result } = renderHook(() => useVerseSelection(), { wrapper }); - - act(() => { - result.current.toggleVerse('MAT.1.1'); - result.current.toggleVerse('MAT.1.1'); - }); - - // Should toggle off, not add twice - expect(result.current.selectedCount).toBe(0); - }); - }); - - describe('isSelected', () => { - it('should return true for selected verses', () => { - const { result } = renderHook(() => useVerseSelection(), { wrapper }); - - act(() => { - result.current.toggleVerse('MAT.1.1'); - }); - - expect(result.current.isSelected('MAT.1.1')).toBe(true); - }); - - it('should return false for unselected verses', () => { - const { result } = renderHook(() => useVerseSelection(), { wrapper }); - - act(() => { - result.current.toggleVerse('MAT.1.1'); - }); - - expect(result.current.isSelected('GEN.1.1')).toBe(false); - }); - - it('should work correctly after toggle operations', () => { - const { result } = renderHook(() => useVerseSelection(), { wrapper }); - - // Initially not selected - expect(result.current.isSelected('MAT.1.1')).toBe(false); - - // Add verse - now selected - act(() => { - result.current.toggleVerse('MAT.1.1'); - }); - expect(result.current.isSelected('MAT.1.1')).toBe(true); - - // Remove verse - not selected again - act(() => { - result.current.toggleVerse('MAT.1.1'); - }); - expect(result.current.isSelected('MAT.1.1')).toBe(false); - }); - }); - - describe('clearSelection', () => { - it('should clear all selected verses', () => { - const { result } = renderHook(() => useVerseSelection(), { wrapper }); - - act(() => { - result.current.toggleVerse('MAT.1.1'); - result.current.toggleVerse('GEN.1.1'); - }); - - expect(result.current.selectedCount).toBe(2); - - act(() => { - result.current.clearSelection(); - }); - - expect(result.current.selectedCount).toBe(0); - expect(result.current.selectedVerseUsfms.size).toBe(0); - }); - - it('should create new Set reference', () => { - const { result } = renderHook(() => useVerseSelection(), { wrapper }); - - act(() => { - result.current.toggleVerse('MAT.1.1'); - }); - - const setBeforeClear = result.current.selectedVerseUsfms; - - act(() => { - result.current.clearSelection(); - }); - - const setAfterClear = result.current.selectedVerseUsfms; - - expect(setBeforeClear).not.toBe(setAfterClear); - expect(setBeforeClear.size).toBe(1); - expect(setAfterClear.size).toBe(0); - }); - - it('should work when already empty', () => { - const { result } = renderHook(() => useVerseSelection(), { wrapper }); - - const setBeforeClear = result.current.selectedVerseUsfms; - - act(() => { - result.current.clearSelection(); - }); - - const setAfterClear = result.current.selectedVerseUsfms; - - expect(setBeforeClear).not.toBe(setAfterClear); - expect(setAfterClear.size).toBe(0); - }); - }); - - describe('selectedCount', () => { - it('should start at 0', () => { - const { result } = renderHook(() => useVerseSelection(), { wrapper }); - - expect(result.current.selectedCount).toBe(0); - }); - - it('should increment when verse added', () => { - const { result } = renderHook(() => useVerseSelection(), { wrapper }); - - act(() => { - result.current.toggleVerse('MAT.1.1'); - }); - expect(result.current.selectedCount).toBe(1); - - act(() => { - result.current.toggleVerse('GEN.1.1'); - }); - expect(result.current.selectedCount).toBe(2); - - act(() => { - result.current.toggleVerse('JHN.3.16'); - }); - expect(result.current.selectedCount).toBe(3); - }); - - it('should decrement when verse removed', () => { - const { result } = renderHook(() => useVerseSelection(), { wrapper }); - - act(() => { - result.current.toggleVerse('MAT.1.1'); - result.current.toggleVerse('GEN.1.1'); - }); - expect(result.current.selectedCount).toBe(2); - - act(() => { - result.current.toggleVerse('MAT.1.1'); - }); - expect(result.current.selectedCount).toBe(1); - }); - - it('should return to 0 after clear', () => { - const { result } = renderHook(() => useVerseSelection(), { wrapper }); - - act(() => { - result.current.toggleVerse('MAT.1.1'); - result.current.toggleVerse('GEN.1.1'); - result.current.toggleVerse('JHN.3.16'); - }); - expect(result.current.selectedCount).toBe(3); - - act(() => { - result.current.clearSelection(); - }); - expect(result.current.selectedCount).toBe(0); - }); - }); - - describe('callback stability', () => { - it('should have stable toggleVerse reference across renders', () => { - const { result, rerender } = renderHook(() => useVerseSelection(), { wrapper }); - - const toggleRef1 = result.current.toggleVerse; - - rerender(); - - const toggleRef2 = result.current.toggleVerse; - - expect(toggleRef1).toBe(toggleRef2); - }); - - it('should have stable clearSelection reference across renders', () => { - const { result, rerender } = renderHook(() => useVerseSelection(), { wrapper }); - - const clearRef1 = result.current.clearSelection; - - rerender(); - - const clearRef2 = result.current.clearSelection; - - expect(clearRef1).toBe(clearRef2); - }); - - it('should have stable toggleVerse reference after state changes', () => { - const { result } = renderHook(() => useVerseSelection(), { wrapper }); - - const toggleRef1 = result.current.toggleVerse; - - act(() => { - result.current.toggleVerse('MAT.1.1'); - }); - - const toggleRef2 = result.current.toggleVerse; - - expect(toggleRef1).toBe(toggleRef2); - }); - - it('should have stable clearSelection reference after state changes', () => { - const { result } = renderHook(() => useVerseSelection(), { wrapper }); - - const clearRef1 = result.current.clearSelection; - - act(() => { - result.current.toggleVerse('MAT.1.1'); - }); - - const clearRef2 = result.current.clearSelection; - - expect(clearRef1).toBe(clearRef2); - }); - - it('should update isSelected reference when selectedVerseUsfms changes', () => { - const { result } = renderHook(() => useVerseSelection(), { wrapper }); - - const isSelectedRef1 = result.current.isSelected; - - act(() => { - result.current.toggleVerse('MAT.1.1'); - }); - - const isSelectedRef2 = result.current.isSelected; - - // isSelected has selectedVerseUsfms in its dependency array, so it should change - expect(isSelectedRef1).not.toBe(isSelectedRef2); - }); - - it('should have stable isSelected reference when no state changes', () => { - const { result, rerender } = renderHook(() => useVerseSelection(), { wrapper }); - - const isSelectedRef1 = result.current.isSelected; - - rerender(); - - const isSelectedRef2 = result.current.isSelected; - - expect(isSelectedRef1).toBe(isSelectedRef2); - }); - }); -}); diff --git a/packages/hooks/src/context/VerseSelectionProvider.tsx b/packages/hooks/src/context/VerseSelectionProvider.tsx index c5937660..9f057d43 100644 --- a/packages/hooks/src/context/VerseSelectionProvider.tsx +++ b/packages/hooks/src/context/VerseSelectionProvider.tsx @@ -1,6 +1,9 @@ import { type PropsWithChildren, useCallback, useState } from 'react'; import { VerseSelectionContext } from './VerseSelectionContext'; +/** + * @deprecated VerseSelectionProvider will be removed in the next major version. + */ export function VerseSelectionProvider({ children }: PropsWithChildren): React.ReactElement { const [selectedVerseUsfms, setSelectedVerseUsfms] = useState>(new Set()); diff --git a/packages/hooks/src/useChapterNavigation.test.tsx b/packages/hooks/src/useChapterNavigation.test.tsx deleted file mode 100644 index 1fcd9c78..00000000 --- a/packages/hooks/src/useChapterNavigation.test.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { renderHook, act } from '@testing-library/react'; -import React from 'react'; -import { useChapterNavigation } from './useChapterNavigation'; -import { useReaderContext } from './context/ReaderContext'; -import { ReaderProvider } from './context/ReaderProvider'; -import { createMockBook, createMockVersion } from './__tests__/mocks/bibles'; -import type { BibleBook, BibleChapter } from '@youversion/platform-core'; - -function makeChapters(bookId: string, count: number): BibleChapter[] { - return Array.from({ length: count }, (_, i) => ({ - id: (i + 1).toString(), - passage_id: `${bookId}.${i + 1}`, - title: (i + 1).toString(), - })); -} - -const genChapters = makeChapters('GEN', 50); -const exoChapters = makeChapters('EXO', 40); -const revChapters = makeChapters('REV', 22); - -const mockBooks: BibleBook[] = [ - createMockBook({ - id: 'GEN', - title: 'Genesis', - chapters: genChapters, - intro: { id: 'INTRO', passage_id: 'GEN.INTRO', title: 'Intro' }, - }), - createMockBook({ - id: 'EXO', - title: 'Exodus', - canon: 'old_testament', - chapters: exoChapters, - }), - createMockBook({ - id: 'REV', - title: 'Revelation', - canon: 'new_testament', - chapters: revChapters, - }), -]; - -const { mockUseBooks } = vi.hoisted(() => ({ - mockUseBooks: vi.fn(), -})); -vi.mock('./useBooks', () => ({ - useBooks: mockUseBooks, -})); - -function useNavWithContext() { - const nav = useChapterNavigation(); - const ctx = useReaderContext(); - return { nav, ctx }; -} - -function wrapper(book: BibleBook, chapter: BibleChapter) { - return ({ children }: { children: React.ReactNode }) => ( - - {children} - - ); -} - -const genBook = mockBooks[0]!; -const exoBook = mockBooks[1]!; -const revBook = mockBooks[2]!; - -describe('useChapterNavigation', () => { - beforeEach(() => { - mockUseBooks.mockReturnValue({ - books: { data: mockBooks }, - loading: false, - error: null, - refetch: vi.fn(), - }); - }); - - it('navigateToNext within same book updates chapter, keeps book', () => { - const { result } = renderHook(useNavWithContext, { - wrapper: wrapper(genBook, genChapters[0]!), - }); - - act(() => result.current.nav.navigateToNext()); - - expect(result.current.ctx.currentChapter.id).toBe('2'); - expect(result.current.ctx.currentBook.id).toBe('GEN'); - }); - - it('navigateToNext cross-book updates both book and chapter', () => { - const { result } = renderHook(useNavWithContext, { - wrapper: wrapper(genBook, genChapters.at(-1)!), - }); - - act(() => result.current.nav.navigateToNext()); - - expect(result.current.ctx.currentBook.id).toBe('EXO'); - expect(result.current.ctx.currentChapter.id).toBe('1'); - }); - - it('navigateToPrevious to intro sets chapter to INTRO, keeps book', () => { - const { result } = renderHook(useNavWithContext, { - wrapper: wrapper(genBook, genChapters[0]!), - }); - - act(() => result.current.nav.navigateToPrevious()); - - expect(result.current.ctx.currentChapter.id).toBe('INTRO'); - expect(result.current.ctx.currentBook.id).toBe('GEN'); - }); - - it('navigateToPrevious cross-book updates both book and chapter', () => { - const { result } = renderHook(useNavWithContext, { - wrapper: wrapper(exoBook, exoChapters[0]!), - }); - - act(() => result.current.nav.navigateToPrevious()); - - expect(result.current.ctx.currentBook.id).toBe('GEN'); - expect(result.current.ctx.currentChapter.id).toBe('50'); - }); - - it('canNavigatePrevious is false at Bible start', () => { - const introChapter: BibleChapter = { id: 'INTRO', passage_id: 'GEN.INTRO', title: 'Intro' }; - - const { result } = renderHook(useNavWithContext, { - wrapper: wrapper(genBook, introChapter), - }); - - expect(result.current.nav.canNavigatePrevious).toBe(false); - }); - - it('canNavigateNext is false at Bible end', () => { - const { result } = renderHook(useNavWithContext, { - wrapper: wrapper(revBook, revChapters[21]!), - }); - - expect(result.current.nav.canNavigateNext).toBe(false); - }); - - it('both disabled while loading', () => { - mockUseBooks.mockReturnValue({ - books: null, - loading: true, - error: null, - refetch: vi.fn(), - }); - - const { result } = renderHook(useNavWithContext, { - wrapper: wrapper(genBook, genChapters[0]!), - }); - - expect(result.current.nav.canNavigateNext).toBe(false); - expect(result.current.nav.canNavigatePrevious).toBe(false); - expect(result.current.nav.isLoading).toBe(true); - }); -}); diff --git a/packages/hooks/src/useChapterNavigation.ts b/packages/hooks/src/useChapterNavigation.ts index 6c5d2ad3..daa50002 100644 --- a/packages/hooks/src/useChapterNavigation.ts +++ b/packages/hooks/src/useChapterNavigation.ts @@ -17,6 +17,10 @@ interface UseChapterNavigationResult { * Provides navigation functionality for chapters across book boundaries, * including intro chapter support. */ +/** + * @deprecated This hook will be removed in the next major version. + * Use `getAdjacentChapter` from `@youversion/platform-core` directly instead. + */ export function useChapterNavigation(): UseChapterNavigationResult { const { currentChapter, currentVersion, currentBook, setChapter, setBook } = useReaderContext(); diff --git a/packages/hooks/src/useVerseSelection.test.tsx b/packages/hooks/src/useVerseSelection.test.tsx deleted file mode 100644 index 7e610560..00000000 --- a/packages/hooks/src/useVerseSelection.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { describe, it, expect, vi } from 'vitest'; -import { renderHook } from '@testing-library/react'; -import type { ReactNode } from 'react'; -import { useVerseSelection } from './useVerseSelection'; -import { VerseSelectionProvider } from './context/VerseSelectionProvider'; - -// Wrapper for renderHook -const wrapper = ({ children }: { children: ReactNode }) => ( - {children} -); - -describe('useVerseSelection', () => { - it('should throw error when used outside provider', () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(vi.fn()); - - expect(() => { - renderHook(() => useVerseSelection()); - }).toThrow('useVerseSelection must be used within a VerseSelectionProvider'); - - consoleSpy.mockRestore(); - }); - - it('should return expected shape with correct initial values', () => { - const { result } = renderHook(() => useVerseSelection(), { wrapper }); - - expect(result.current.selectedVerseUsfms).toBeInstanceOf(Set); - expect(result.current.selectedVerseUsfms.size).toBe(0); - expect(result.current.selectedCount).toBe(0); - expect(typeof result.current.toggleVerse).toBe('function'); - expect(typeof result.current.isSelected).toBe('function'); - expect(typeof result.current.clearSelection).toBe('function'); - }); -}); diff --git a/packages/hooks/src/useVerseSelection.ts b/packages/hooks/src/useVerseSelection.ts index 64b334ee..7a4ca0c1 100644 --- a/packages/hooks/src/useVerseSelection.ts +++ b/packages/hooks/src/useVerseSelection.ts @@ -4,6 +4,9 @@ import { type VerseSelectionContextData, } from './context/VerseSelectionContext'; +/** + * @deprecated This hook will be removed in the next major version. + */ export function useVerseSelection(): VerseSelectionContextData { const context = useContext(VerseSelectionContext); if (!context) { diff --git a/packages/hooks/vitest.config.ts b/packages/hooks/vitest.config.ts index b63cb838..64a97bfb 100644 --- a/packages/hooks/vitest.config.ts +++ b/packages/hooks/vitest.config.ts @@ -16,7 +16,15 @@ export default defineConfig({ reportsDirectory: './coverage', all: true, include: ['src/**/*.{ts,tsx}'], - exclude: ['src/useInitData.ts'], + exclude: [ + 'src/useInitData.ts', + 'src/useChapterNavigation.ts', + 'src/useVerseSelection.ts', + 'src/context/ReaderContext.tsx', + 'src/context/ReaderProvider.tsx', + 'src/context/VerseSelectionContext.tsx', + 'src/context/VerseSelectionProvider.tsx', + ], }, }, }); From fab54d511dd1ac924b54ff87f9b5efa0d37d15ea Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Wed, 4 Mar 2026 12:55:04 -0600 Subject: [PATCH 4/8] Added changeset --- .changeset/twenty-onions-wish.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .changeset/twenty-onions-wish.md diff --git a/.changeset/twenty-onions-wish.md b/.changeset/twenty-onions-wish.md new file mode 100644 index 00000000..476b957d --- /dev/null +++ b/.changeset/twenty-onions-wish.md @@ -0,0 +1,13 @@ +--- +'@youversion/platform-react-hooks': minor +'@youversion/platform-core': minor +'@youversion/platform-react-ui': minor +--- + +Deprecates hooks and providers that are not used by the UI package or any known consumers. These were inherited from a hackathon project and never adopted. These will be fully removed in the next major version bump. + +Deprecated: +- `useInitData` — convenience wrapper over `useVersion`, `useBook`, `useChapter` that loses error granularity, drops `refetch`, and has zero consumers. Use the three hooks directly. +- `useChapterNavigation` — coupled to `ReaderProvider` which nobody uses. The UI package calls `getAdjacentChapter` from core directly. +- `ReaderProvider`, `ReaderContext`, `useReaderContext` — the UI package built its own `BibleReaderContext` instead. Zero consumers. +- `VerseSelectionProvider`, `VerseSelectionContext`, `useVerseSelection` — the UI package handles verse selection via props/callbacks. Zero consumers. From 5886a9d165fd0d730d1d311f6bb381d5e4734ee2 Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Wed, 4 Mar 2026 13:08:34 -0600 Subject: [PATCH 5/8] Remove Reader and Verse Selection contexts The ReaderContext, ReaderProvider, VerseSelectionContext, and VerseSelectionProvider, along with their associated hooks, are deprecated and no longer necessary. These components have been removed to simplify the hooks package. --- packages/hooks/AGENTS.md | 10 +++------- packages/hooks/src/context/ReaderContext.tsx | 4 ++-- packages/hooks/src/context/ReaderProvider.tsx | 2 +- packages/hooks/src/context/VerseSelectionContext.tsx | 2 +- packages/hooks/src/context/VerseSelectionProvider.tsx | 2 +- packages/hooks/src/useVerseSelection.ts | 2 +- 6 files changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/hooks/AGENTS.md b/packages/hooks/AGENTS.md index 0ee13a52..d7af5adb 100644 --- a/packages/hooks/AGENTS.md +++ b/packages/hooks/AGENTS.md @@ -1,7 +1,7 @@ # @youversion/platform-react-hooks ## OVERVIEW -React integration layer providing data fetching hooks with 3 core providers: YouVersionProvider, YouVersionAuthProvider, and ReaderProvider. +React integration layer providing data fetching hooks with 2 core providers: YouVersionProvider and YouVersionAuthProvider. **Depends on `@youversion/platform-core` for all API calls.** Hooks delegate to core clients; do not implement raw HTTP here. @@ -15,12 +15,12 @@ React integration layer providing data fetching hooks with 3 core providers: You - `utility/` - Helper functions (useDebounce, extractTextFromHTML, extractVersesFromHTML) ## PUBLIC API -- Data fetching hooks: useBook, useChapter, usePassage, useVersion, useVOTD, useVerse, useChapterNavigation, etc. +- Data fetching hooks: useBook, useChapter, usePassage, useVersion, useVOTD, useVerse, etc. - YouVersionProvider - Core SDK configuration - YouVersionAuthProvider - Authentication state -- ReaderProvider - Reading session context - Utility functions exported from utility/index + ## PROVIDERS - **YouVersionProvider** @@ -31,10 +31,6 @@ React integration layer providing data fetching hooks with 3 core providers: You - Manages authentication state (userInfo, tokens, isLoading, error) - Auth hooks like `useYVAuth` depend on this provider -- **ReaderProvider** - - Manages Bible reading session state (currentVersion, currentChapter, currentBook, currentVerse) - - Hooks like `useChapterNavigation` depend on this provider - ## DOs / DON'Ts ✅ Do: Use `YouVersionProvider` for configuration and access that config in hooks diff --git a/packages/hooks/src/context/ReaderContext.tsx b/packages/hooks/src/context/ReaderContext.tsx index 6cf33c30..2691c6d8 100644 --- a/packages/hooks/src/context/ReaderContext.tsx +++ b/packages/hooks/src/context/ReaderContext.tsx @@ -15,12 +15,12 @@ type ReaderContextData = { }; /** - * @deprecated ReaderContext will be removed in the next major version. + * @deprecated No replacement needed. Remove usage. Will be removed in the next major version. */ export const ReaderContext = createContext(null); /** - * @deprecated This hook will be removed in the next major version. + * @deprecated No replacement needed. Remove usage. Will be removed in the next major version. */ export function useReaderContext(): ReaderContextData { const context = useContext(ReaderContext); diff --git a/packages/hooks/src/context/ReaderProvider.tsx b/packages/hooks/src/context/ReaderProvider.tsx index 18325cfa..cdf5dbb9 100644 --- a/packages/hooks/src/context/ReaderProvider.tsx +++ b/packages/hooks/src/context/ReaderProvider.tsx @@ -12,7 +12,7 @@ type ReaderProviderProps = { }; /** - * @deprecated ReaderProvider will be removed in the next major version. + * @deprecated No replacement needed. Remove usage. Will be removed in the next major version. */ export function ReaderProvider(props: PropsWithChildren): React.ReactElement { const [currentVersion, setCurrentVersion] = useState(props.currentVersion); diff --git a/packages/hooks/src/context/VerseSelectionContext.tsx b/packages/hooks/src/context/VerseSelectionContext.tsx index 417542b3..76f786cd 100644 --- a/packages/hooks/src/context/VerseSelectionContext.tsx +++ b/packages/hooks/src/context/VerseSelectionContext.tsx @@ -9,6 +9,6 @@ export type VerseSelectionContextData = { }; /** - * @deprecated VerseSelectionContext will be removed in the next major version. + * @deprecated No replacement needed. Remove usage. Will be removed in the next major version. */ export const VerseSelectionContext = createContext(null); diff --git a/packages/hooks/src/context/VerseSelectionProvider.tsx b/packages/hooks/src/context/VerseSelectionProvider.tsx index 9f057d43..dc025b8b 100644 --- a/packages/hooks/src/context/VerseSelectionProvider.tsx +++ b/packages/hooks/src/context/VerseSelectionProvider.tsx @@ -2,7 +2,7 @@ import { type PropsWithChildren, useCallback, useState } from 'react'; import { VerseSelectionContext } from './VerseSelectionContext'; /** - * @deprecated VerseSelectionProvider will be removed in the next major version. + * @deprecated No replacement needed. Remove usage. Will be removed in the next major version. */ export function VerseSelectionProvider({ children }: PropsWithChildren): React.ReactElement { const [selectedVerseUsfms, setSelectedVerseUsfms] = useState>(new Set()); diff --git a/packages/hooks/src/useVerseSelection.ts b/packages/hooks/src/useVerseSelection.ts index 7a4ca0c1..edf3fb87 100644 --- a/packages/hooks/src/useVerseSelection.ts +++ b/packages/hooks/src/useVerseSelection.ts @@ -5,7 +5,7 @@ import { } from './context/VerseSelectionContext'; /** - * @deprecated This hook will be removed in the next major version. + * @deprecated No replacement needed. Remove usage. Will be removed in the next major version. */ export function useVerseSelection(): VerseSelectionContextData { const context = useContext(VerseSelectionContext); From fb319ebe88ecf135b1283226f90f5595ed1991ad Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Wed, 4 Mar 2026 13:09:57 -0600 Subject: [PATCH 6/8] docs(hooks): fix duplicate JSDoc and add missing deprecation tag - Merge duplicate JSDoc blocks on useChapterNavigation into one - Add @deprecated to VerseSelectionContextData type for consistency Amp-Thread-ID: https://ampcode.com/threads/T-019cba40-e17d-779a-aef7-d1a5ea3fd36b Co-authored-by: Amp --- packages/hooks/src/context/VerseSelectionContext.tsx | 3 +++ packages/hooks/src/useChapterNavigation.ts | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/hooks/src/context/VerseSelectionContext.tsx b/packages/hooks/src/context/VerseSelectionContext.tsx index 76f786cd..d77a6363 100644 --- a/packages/hooks/src/context/VerseSelectionContext.tsx +++ b/packages/hooks/src/context/VerseSelectionContext.tsx @@ -1,5 +1,8 @@ import { createContext } from 'react'; +/** + * @deprecated VerseSelectionContextData will be removed in the next major version. + */ export type VerseSelectionContextData = { selectedVerseUsfms: Set; toggleVerse: (usfm: string) => void; diff --git a/packages/hooks/src/useChapterNavigation.ts b/packages/hooks/src/useChapterNavigation.ts index daa50002..cf3f37ed 100644 --- a/packages/hooks/src/useChapterNavigation.ts +++ b/packages/hooks/src/useChapterNavigation.ts @@ -16,8 +16,7 @@ interface UseChapterNavigationResult { /** * Provides navigation functionality for chapters across book boundaries, * including intro chapter support. - */ -/** + * * @deprecated This hook will be removed in the next major version. * Use `getAdjacentChapter` from `@youversion/platform-core` directly instead. */ From 9f4f7d09184a218e70c441e81006bb85d295b8ed Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Wed, 4 Mar 2026 13:17:30 -0600 Subject: [PATCH 7/8] feat: Include .changeset directory in unified versioning rule This ensures that any changes within the .changeset directory are also subject to the unified versioning rule, maintaining consistency across all packages. --- greptile.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/greptile.json b/greptile.json index 2c1059eb..da913071 100644 --- a/greptile.json +++ b/greptile.json @@ -16,7 +16,7 @@ "rule": "Schema-first: All types defined in schemas/*.ts using Zod" }, { - "scope": ["packages/**"], + "scope": ["packages/**", ".changeset/**"], "rule": "Unified versioning: All packages must maintain exact same version - never version packages independently" }, { From e14348dd7b6b6928d25bb8d4066a05025a8c009a Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Wed, 4 Mar 2026 13:23:08 -0600 Subject: [PATCH 8/8] Deprecate VerseSelectionContextData --- packages/hooks/src/context/VerseSelectionContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hooks/src/context/VerseSelectionContext.tsx b/packages/hooks/src/context/VerseSelectionContext.tsx index d77a6363..697583a4 100644 --- a/packages/hooks/src/context/VerseSelectionContext.tsx +++ b/packages/hooks/src/context/VerseSelectionContext.tsx @@ -1,7 +1,7 @@ import { createContext } from 'react'; /** - * @deprecated VerseSelectionContextData will be removed in the next major version. + * @deprecated No replacement needed. Remove usage. Will be removed in the next major version. */ export type VerseSelectionContextData = { selectedVerseUsfms: Set;