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. 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" }, { 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.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..2691c6d8 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 No replacement needed. Remove usage. Will be removed in the next major version. + */ export const ReaderContext = createContext(null); +/** + * @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.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..cdf5dbb9 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 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); const [currentBook, setCurrentBook] = useState(props.currentBook); diff --git a/packages/hooks/src/context/VerseSelectionContext.tsx b/packages/hooks/src/context/VerseSelectionContext.tsx index 86eacf38..697583a4 100644 --- a/packages/hooks/src/context/VerseSelectionContext.tsx +++ b/packages/hooks/src/context/VerseSelectionContext.tsx @@ -1,5 +1,8 @@ import { createContext } from 'react'; +/** + * @deprecated No replacement needed. Remove usage. Will be removed in the next major version. + */ export type VerseSelectionContextData = { selectedVerseUsfms: Set; toggleVerse: (usfm: string) => void; @@ -8,4 +11,7 @@ export type VerseSelectionContextData = { selectedCount: number; }; +/** + * @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.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..dc025b8b 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 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/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..cf3f37ed 100644 --- a/packages/hooks/src/useChapterNavigation.ts +++ b/packages/hooks/src/useChapterNavigation.ts @@ -16,6 +16,9 @@ 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/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/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..edf3fb87 100644 --- a/packages/hooks/src/useVerseSelection.ts +++ b/packages/hooks/src/useVerseSelection.ts @@ -4,6 +4,9 @@ import { type VerseSelectionContextData, } from './context/VerseSelectionContext'; +/** + * @deprecated No replacement needed. Remove usage. 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 89b8a63d..64a97bfb 100644 --- a/packages/hooks/vitest.config.ts +++ b/packages/hooks/vitest.config.ts @@ -16,6 +16,15 @@ export default defineConfig({ reportsDirectory: './coverage', all: true, include: ['src/**/*.{ts,tsx}'], + 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', + ], }, }, });