mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fix(series): show mixed-media books in every group
Classify series entries from every available format so combined editions appear in each relevant section. Fixes #1566
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { BookCard } from '@bookorbit/types'
|
||||
import { getSeriesBookMediaGroupKey, groupSeriesBooksByMedia } from './useSeriesBookMediaGroups'
|
||||
import { getSeriesBookMediaGroupKeys, groupSeriesBooksByMedia } from './useSeriesBookMediaGroups'
|
||||
|
||||
function makeBook(format: string | null | undefined, overrides: Partial<BookCard> = {}): BookCard {
|
||||
return {
|
||||
@@ -40,18 +40,18 @@ function makeBook(format: string | null | undefined, overrides: Partial<BookCard
|
||||
|
||||
describe('series book media groups', () => {
|
||||
it.each(['epub', 'pdf', 'mobi', 'azw', 'azw3', 'fb2', 'unknown', null])('groups %s as Books', (format) => {
|
||||
expect(getSeriesBookMediaGroupKey(makeBook(format))).toBe('books')
|
||||
expect(getSeriesBookMediaGroupKeys(makeBook(format))).toEqual(['books'])
|
||||
})
|
||||
|
||||
it.each(['m4b', 'mp3', 'm4a', 'opus', 'ogg', 'flac'])('groups %s as Audiobooks', (format) => {
|
||||
expect(getSeriesBookMediaGroupKey(makeBook(format))).toBe('audiobooks')
|
||||
expect(getSeriesBookMediaGroupKeys(makeBook(format))).toEqual(['audiobooks'])
|
||||
})
|
||||
|
||||
it.each(['cbz', 'cbr', 'cb7'])('groups %s as Comics', (format) => {
|
||||
expect(getSeriesBookMediaGroupKey(makeBook(format))).toBe('comics')
|
||||
expect(getSeriesBookMediaGroupKeys(makeBook(format))).toEqual(['comics'])
|
||||
})
|
||||
|
||||
it('uses the primary file before secondary files', () => {
|
||||
it('groups a book by every media kind it contains regardless of its primary file', () => {
|
||||
const book = makeBook('epub', {
|
||||
files: [
|
||||
{ id: 1, format: 'mp3', role: 'secondary', sizeBytes: null },
|
||||
@@ -60,31 +60,54 @@ describe('series book media groups', () => {
|
||||
],
|
||||
})
|
||||
|
||||
expect(getSeriesBookMediaGroupKey(book)).toBe('books')
|
||||
expect(getSeriesBookMediaGroupKeys(book)).toEqual(['books', 'audiobooks', 'comics'])
|
||||
})
|
||||
|
||||
it('uses the first formatted file when no primary file exists', () => {
|
||||
it('does not add a book to the same group more than once', () => {
|
||||
const book = makeBook(undefined, {
|
||||
files: [
|
||||
{ id: 1, format: null, role: 'secondary', sizeBytes: null },
|
||||
{ id: 1, format: 'mp3', role: 'primary', sizeBytes: null },
|
||||
{ id: 2, format: 'm4b', role: 'secondary', sizeBytes: null },
|
||||
{ id: 3, format: ' M4A ', role: 'secondary', sizeBytes: null },
|
||||
],
|
||||
})
|
||||
|
||||
expect(getSeriesBookMediaGroupKey(book)).toBe('audiobooks')
|
||||
expect(getSeriesBookMediaGroupKeys(book)).toEqual(['audiobooks'])
|
||||
})
|
||||
|
||||
it('falls back to Books when files are missing', () => {
|
||||
expect(getSeriesBookMediaGroupKey(makeBook(undefined))).toBe('books')
|
||||
})
|
||||
it.each([{ files: [] }, { files: [{ id: 1, format: null, role: 'primary', sizeBytes: null }] }])(
|
||||
'falls back to Books when no file identifies a media kind',
|
||||
({ files }) => {
|
||||
expect(getSeriesBookMediaGroupKeys(makeBook(undefined, { files }))).toEqual(['books'])
|
||||
},
|
||||
)
|
||||
|
||||
it('returns ordered groups with matching books', () => {
|
||||
const books = [makeBook('mp3', { id: 1 }), makeBook('cbz', { id: 2 }), makeBook('pdf', { id: 3 }), makeBook('epub', { id: 4 })]
|
||||
it('returns ordered groups, includes mixed-media books in each match, and preserves input order', () => {
|
||||
const books = [
|
||||
makeBook('mp3', { id: 1 }),
|
||||
makeBook('cbz', { id: 2 }),
|
||||
makeBook('epub', {
|
||||
id: 3,
|
||||
files: [
|
||||
{ id: 30, format: 'epub', role: 'primary', sizeBytes: null },
|
||||
{ id: 31, format: 'm4b', role: 'secondary', sizeBytes: null },
|
||||
],
|
||||
}),
|
||||
makeBook('pdf', {
|
||||
id: 4,
|
||||
files: [
|
||||
{ id: 40, format: 'pdf', role: 'primary', sizeBytes: null },
|
||||
{ id: 41, format: 'flac', role: 'secondary', sizeBytes: null },
|
||||
{ id: 42, format: 'cbr', role: 'secondary', sizeBytes: null },
|
||||
],
|
||||
}),
|
||||
makeBook(undefined, { id: 5 }),
|
||||
]
|
||||
|
||||
expect(groupSeriesBooksByMedia(books).map((group) => [group.label, group.books.map((book) => book.id)])).toEqual([
|
||||
['Books', [3, 4]],
|
||||
['Audiobooks', [1]],
|
||||
['Comics', [2]],
|
||||
['Books', [3, 4, 5]],
|
||||
['Audiobooks', [1, 3, 4]],
|
||||
['Comics', [2, 4]],
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,18 +14,24 @@ export type SeriesBookMediaGroup = {
|
||||
books: BookCard[]
|
||||
}
|
||||
|
||||
export function getSeriesBookMediaGroupKey(book: BookCard): SeriesBookMediaGroupKey {
|
||||
const primaryMediaKind = getBookMediaProfile(book.files).primaryMediaKind
|
||||
if (primaryMediaKind === 'audiobook') return 'audiobooks'
|
||||
if (primaryMediaKind === 'comic') return 'comics'
|
||||
return 'books'
|
||||
export function getSeriesBookMediaGroupKeys(book: BookCard): SeriesBookMediaGroupKey[] {
|
||||
const profile = getBookMediaProfile(book.files)
|
||||
const keys: SeriesBookMediaGroupKey[] = []
|
||||
|
||||
if (profile.hasEbook) keys.push('books')
|
||||
if (profile.hasAudio) keys.push('audiobooks')
|
||||
if (profile.hasComic) keys.push('comics')
|
||||
|
||||
return keys.length > 0 ? keys : ['books']
|
||||
}
|
||||
|
||||
export function groupSeriesBooksByMedia(books: BookCard[]): SeriesBookMediaGroup[] {
|
||||
const grouped = new Map<SeriesBookMediaGroupKey, BookCard[]>(SERIES_BOOK_MEDIA_GROUP_DEFS.map((group) => [group.key, []]))
|
||||
|
||||
for (const book of books) {
|
||||
grouped.get(getSeriesBookMediaGroupKey(book))?.push(book)
|
||||
for (const key of getSeriesBookMediaGroupKeys(book)) {
|
||||
grouped.get(key)?.push(book)
|
||||
}
|
||||
}
|
||||
|
||||
return SERIES_BOOK_MEDIA_GROUP_DEFS.map((group) => ({
|
||||
|
||||
@@ -359,6 +359,25 @@ describe('SeriesDetailView', () => {
|
||||
expect(comicsGroup.get('[data-testid="virtual-book-grid"]').attributes('data-book-ids')).toBe('10')
|
||||
})
|
||||
|
||||
it('shows a mixed ebook and audiobook in both media sections', async () => {
|
||||
mocks.items = ref([
|
||||
makeBook({
|
||||
id: 7,
|
||||
files: [
|
||||
{ id: 1, format: 'epub', role: 'primary', sizeBytes: null },
|
||||
{ id: 2, format: 'm4b', role: 'content', sizeBytes: null },
|
||||
],
|
||||
}),
|
||||
])
|
||||
|
||||
const wrapper = mountView()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.get('[data-testid="series-media-group-books"] [data-testid="virtual-book-grid"]').attributes('data-book-ids')).toBe('7')
|
||||
expect(wrapper.get('[data-testid="series-media-group-audiobooks"] [data-testid="virtual-book-grid"]').attributes('data-book-ids')).toBe('7')
|
||||
expect(wrapper.find('[data-testid="series-media-group-comics"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('switches between grouped and plain grid rendering', async () => {
|
||||
mocks.items = ref([
|
||||
makeBook({ id: 7, files: [{ id: 1, format: 'epub', role: 'primary', sizeBytes: null }] }),
|
||||
|
||||
Reference in New Issue
Block a user