fix(cover): prevent series badge clicks from opening reader

treat the series-position badge as a non-open interactive target and
exclude those clicks from card-open handling.

add regression tests for hover hit-testing and badge-click behavior.

type vitest mock functions in the series detail spec to satisfy
the repository pre-push lint checks.

Closes #99
This commit is contained in:
neon
2026-05-24 16:31:44 +00:00
parent 989c16313f
commit 7fe5b9133a
4 changed files with 36 additions and 8 deletions
@@ -475,6 +475,14 @@ describe('BookCoverCard', () => {
expect(wrapper.text()).toContain('#3')
})
it('marks badge as a card-click blocker and disables hit-testing on hover', () => {
mockCardOverlays.value = ['series-position']
const wrapper = mountCard({ book: makeBook({ seriesIndex: 3, seriesName: 'Dune' }) })
const badge = wrapper.find('[data-card-click-blocker]')
expect(badge.exists()).toBe(true)
expect(badge.classes()).toContain('group-hover:pointer-events-none')
})
it('shows #1.5 badge when seriesIndex is 1.5', () => {
mockCardOverlays.value = ['series-position']
const wrapper = mountCard({ book: makeBook({ seriesIndex: 1.5, seriesName: 'Dune' }) })
@@ -517,5 +525,15 @@ describe('BookCoverCard', () => {
const wrapper = mountCard({ book: makeBook({ seriesIndex: 3, seriesName: null }) })
expect(wrapper.text()).toContain('#3')
})
it('does not open the reader when the series badge is clicked', async () => {
setTouchMode(false)
mockCardOverlays.value = ['series-position']
const wrapper = mountCard({ book: makeBook({ seriesIndex: 3, seriesName: 'Dune' }) })
const badge = wrapper.find('[data-card-click-blocker]')
expect(badge.exists()).toBe(true)
await badge.trigger('click')
expect(mockRouterPush).not.toHaveBeenCalledWith(expect.objectContaining({ name: 'reader' }))
})
})
})
@@ -192,7 +192,8 @@ function openFile(file: BookFileRef) {
}
function handleCardClick(event: MouseEvent) {
if ((event.target as Element).closest('button')) return
const target = event.target
if (target instanceof Element && target.closest('button, [data-card-click-blocker]')) return
if (props.selectionMode) {
emit('select', event)
return
@@ -333,7 +334,8 @@ async function handleSetStatus(status: ReadStatus) {
<Tooltip v-if="showSeriesPositionBadge">
<TooltipTrigger as-child>
<div
class="pointer-events-auto flex items-center bg-black/60 rounded-full px-1.5 py-0.5 group-hover:opacity-0 transition-opacity duration-150 cursor-default"
data-card-click-blocker
class="pointer-events-auto group-hover:pointer-events-none flex items-center bg-black/60 rounded-full px-1.5 py-0.5 group-hover:opacity-0 transition-opacity duration-150 cursor-default"
>
<span class="text-[9px] font-bold text-white leading-none">{{ seriesPositionLabel }}</span>
</div>
@@ -290,6 +290,14 @@ describe('BookCoverCard — series position overlay', () => {
expect(wrapper.text()).toContain('#3')
})
it('marks the series badge as a card click blocker and disables hit-testing on hover', () => {
cardOverlays.value = ['series-position']
const wrapper = mountCard(bookWithSeries)
const badge = wrapper.find('[data-card-click-blocker]')
expect(badge.exists()).toBe(true)
expect(badge.classes()).toContain('group-hover:pointer-events-none')
})
it('does not render the badge when series-position is not in overlays', () => {
cardOverlays.value = []
const wrapper = mountCard(bookWithSeries)
@@ -7,10 +7,10 @@ import SeriesDetailView from './SeriesDetailView.vue'
type ViewMode = 'grid' | 'list' | 'table'
class MockIntersectionObserver {
observe = vi.fn()
unobserve = vi.fn()
disconnect = vi.fn()
takeRecords = vi.fn(() => [])
observe = vi.fn<(target: Element) => void>()
unobserve = vi.fn<(target: Element) => void>()
disconnect = vi.fn<() => void>()
takeRecords = vi.fn<() => IntersectionObserverEntry[]>(() => [])
}
const mocks = vi.hoisted(() => ({
@@ -30,8 +30,8 @@ const mocks = vi.hoisted(() => ({
order: null as unknown as { value: 'asc' | 'desc' },
libraryId: null as unknown as { value: number | null },
effectiveViewMode: null as unknown as { value: ViewMode },
fetchSeriesBooks: vi.fn(),
api: vi.fn(),
fetchSeriesBooks: vi.fn<(...args: unknown[]) => Promise<unknown>>(),
api: vi.fn<(...args: unknown[]) => Promise<unknown>>(),
}))
vi.mock('vue-router', async (importOriginal) => {