From 4e7a1189ff380a744eb96c4efcc2b77d5213a174 Mon Sep 17 00:00:00 2001 From: Bryson Hotopp <6694199+arinthros@users.noreply.github.com> Date: Thu, 29 Oct 2026 12:53:16 -0500 Subject: [PATCH 1/2] fix(core): navigate next into the next book's intro getAdjacentChapter skipped the next book's intro going forward but visited it going back, so previous(next(x)) did not return x and BibleReader's next button never reached an intro. Next from a book's last chapter now goes to the next book's intro when it has one, matching Swift's goToNextChapter. Reverses the two tests from #168 that asserted the skip and adds a round-trip check over every chapter and intro. YPE-6400 Co-Authored-By: Claude Opus 5.5 --- .changeset/adjacent-chapter-next-intro.md | 5 ++++ packages/core/src/getAdjacentChapter.test.ts | 30 ++++++++++++++++++-- packages/core/src/getAdjacentChapter.ts | 6 +++- 3 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 .changeset/adjacent-chapter-next-intro.md diff --git a/.changeset/adjacent-chapter-next-intro.md b/.changeset/adjacent-chapter-next-intro.md new file mode 100644 index 00000000..9f4f5082 --- /dev/null +++ b/.changeset/adjacent-chapter-next-intro.md @@ -0,0 +1,5 @@ +--- +'@youversion/platform-core': patch +--- + +`getAdjacentChapter` now goes from the last chapter of a book to the next book's intro when it has one, instead of skipping to chapter 1. Next and previous are now inverses, and `BibleReader`'s next-chapter button reaches book intros the way the Swift reader does. diff --git a/packages/core/src/getAdjacentChapter.test.ts b/packages/core/src/getAdjacentChapter.test.ts index ad1ca009..b836d558 100644 --- a/packages/core/src/getAdjacentChapter.test.ts +++ b/packages/core/src/getAdjacentChapter.test.ts @@ -49,9 +49,16 @@ describe('getAdjacentChapter', () => { }); }); - it('next chapter cross-book, skipping intro', () => { + it('next chapter cross-book → intro of next book', () => { expect(getAdjacentChapter(books, 'GEN', '50', 'next')).toEqual({ bookId: 'EXO', + chapterId: 'INTRO', + }); + }); + + it('next chapter cross-book when next book has no intro → chapter 1', () => { + expect(getAdjacentChapter(books, 'EXO', '40', 'next')).toEqual({ + bookId: 'LEV', chapterId: '1', }); }); @@ -134,14 +141,31 @@ describe('getAdjacentChapter', () => { }); }); - it('cross-book next skips intro in chapters array of next book', () => { + it('cross-book next → intro of next book when intro is also in its chapters array', () => { const booksWithIntroInArray = [ makeBook('GEN', 50), makeBook('EXO', 40, { hasIntro: true, introInChapters: true }), ]; expect(getAdjacentChapter(booksWithIntroInArray, 'GEN', '50', 'next')).toEqual({ bookId: 'EXO', - chapterId: '1', + chapterId: 'INTRO', + }); + }); + + it('next and previous are inverses across every chapter and intro', () => { + // Reading order: each book's intro (when present), then its numbered chapters. + const positions = books.flatMap((b) => [ + ...(b.intro ? [{ bookId: b.id, chapterId: b.intro.id }] : []), + ...(b.chapters ?? []).map((c) => ({ bookId: b.id, chapterId: c.id })), + ]); + + positions.forEach((position, i) => { + expect(getAdjacentChapter(books, position.bookId, position.chapterId, 'next')).toEqual( + positions[i + 1] ?? null, + ); + expect(getAdjacentChapter(books, position.bookId, position.chapterId, 'previous')).toEqual( + positions[i - 1] ?? null, + ); }); }); diff --git a/packages/core/src/getAdjacentChapter.ts b/packages/core/src/getAdjacentChapter.ts index f0fb6bc7..6d49dd79 100644 --- a/packages/core/src/getAdjacentChapter.ts +++ b/packages/core/src/getAdjacentChapter.ts @@ -61,9 +61,13 @@ function getNext( } } - // Cross-book: first non-intro chapter of next book + // Cross-book: next book's intro when it has one (mirrors previous from chapter 1, and + // matches Swift's goToNextChapter), else its first non-intro chapter const nextBook = books[bookIndex + 1]; if (!nextBook) return null; + if (nextBook.intro) { + return { bookId: nextBook.id, chapterId: nextBook.intro.id }; + } const nextChapters: BibleChapter[] = nextBook.chapters ?? []; const firstCanonical = nextChapters.find((c) => !isIntroChapter(c.id)); From 2142ef14fe029d85b2358683dcac3f821b6aa70c Mon Sep 17 00:00:00 2001 From: Bryson Hotopp <6694199+arinthros@users.noreply.github.com> Date: Wed, 4 Nov 2026 14:32:55 -0600 Subject: [PATCH 2/2] docs(core): document intro reading order on getAdjacentChapter Co-Authored-By: Claude Opus 5.5 --- packages/core/src/getAdjacentChapter.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/core/src/getAdjacentChapter.ts b/packages/core/src/getAdjacentChapter.ts index 6d49dd79..726fcbc4 100644 --- a/packages/core/src/getAdjacentChapter.ts +++ b/packages/core/src/getAdjacentChapter.ts @@ -10,6 +10,12 @@ function isIntroChapter(chapterId: string): boolean { /** * Computes the next or previous chapter across book boundaries. * + * Reading order is each book's intro (when `book.intro` is present), then its numbered + * chapters. `next` from the last chapter of a book returns the next book's intro when it has + * one, otherwise its first numbered chapter. `previous` from chapter 1 returns the book's + * intro when it has one, otherwise the last chapter of the previous book. `next` and + * `previous` are inverses of each other. + * * @param books - Ordered array of Bible books with populated chapters * @param currentBookId - USFM book ID (e.g., "JHN") * @param currentChapterId - Chapter ID string (e.g., "1", "INTRO")