test(subscribe): cover popular and share workflows (#545)

This commit is contained in:
InfinityPacer
2026-07-17 19:54:24 +08:00
committed by GitHub
parent 2b310c9a97
commit 435e9ecfdd
16 changed files with 2122 additions and 123 deletions
+2 -1
View File
@@ -49,6 +49,7 @@ const posterUrl = computed(() => {
function getMediaId() {
if (props.media?.tmdbid) return `tmdb:${props.media?.tmdbid}`
else if (props.media?.doubanid) return `douban:${props.media?.doubanid}`
else if (props.media?.bangumiid) return `bangumi:${props.media?.bangumiid}`
}
// 查看媒体详情
@@ -152,7 +153,7 @@ function doDelete() {
</div>
</VCardText>
<VCardText class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
<VIcon icon="mdi-calcdar" class="me-1" />
<VIcon icon="mdi-calendar" class="me-1" />
{{ dateText }}
</VCardText>
</div>
@@ -0,0 +1,180 @@
import { formatDateDifference } from '@/@core/utils/formatters'
import type { SubscribeShare } from '@/api/types'
import SubscribeShareCard from '@/components/cards/SubscribeShareCard.vue'
import { fireEvent, screen, waitFor } from '@testing-library/vue'
import { createSubscribeShare } from '@tests/support/factories/subscribe'
import { renderWithProviders } from '@tests/support/render'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
openSharedDialog: vi.fn(),
routerPush: vi.fn(),
}))
vi.mock('@/composables/useSharedDialog', () => ({
openSharedDialog: (...args: unknown[]) => mocks.openSharedDialog(...args),
}))
vi.mock('@/router', () => ({
default: { push: (...args: unknown[]) => mocks.routerPush(...args) },
}))
vi.mock('@iconify/vue', () => ({
Icon: {
props: ['icon'],
template: '<span :data-icon="icon" />',
},
}))
type ShareOverrides = Partial<SubscribeShare>
function observeElementsImmediately() {
class ImmediateIntersectionObserver {
readonly root = null
readonly rootMargin = '0px'
readonly thresholds = [0]
constructor(private readonly callback: IntersectionObserverCallback) {}
disconnect() {}
observe(target: Element) {
this.callback([{ intersectionRatio: 1, isIntersecting: true, target } as IntersectionObserverEntry], this)
}
takeRecords(): IntersectionObserverEntry[] {
return []
}
unobserve() {}
}
vi.stubGlobal('IntersectionObserver', ImmediateIntersectionObserver)
}
async function renderCard(overrides: ShareOverrides = {}, globalImageCache = false) {
const media = createSubscribeShare(overrides)
const result = await renderWithProviders(SubscribeShareCard, {
initialState: {
globalSettings: {
data: { GLOBAL_IMAGE_CACHE: globalImageCache },
initialized: true,
loading: false,
},
},
props: { media },
})
return { ...result, media }
}
async function loadPoster(container: Element) {
const backdrop = container.querySelector<HTMLImageElement>('img')
expect(backdrop).not.toBeNull()
await fireEvent.load(backdrop as HTMLImageElement)
await waitFor(() => expect(container.querySelectorAll('img')).toHaveLength(2))
return container.querySelectorAll<HTMLImageElement>('img')[1]
}
function getDialogCall(index = 0) {
const [, props, events, options] = mocks.openSharedDialog.mock.calls[index] as [
unknown,
Record<string, unknown>,
Record<string, (...args: unknown[]) => void>,
Record<string, unknown>,
]
return { events, options, props }
}
describe('SubscribeShareCard', () => {
beforeEach(() => {
observeElementsImmediately()
mocks.openSharedDialog.mockReturnValue({ close: vi.fn(), id: 1, updateProps: vi.fn() })
})
it('renders sharing metadata and reveals the cached poster after the backdrop loads', async () => {
const { container, media } = await renderCard({}, true)
expect(screen.getByText(media.share_title!)).toBeInTheDocument()
expect(screen.getByText(media.share_comment!)).toBeInTheDocument()
expect(screen.getByText(media.share_user!)).toBeInTheDocument()
expect(screen.getByText(media.count!.toLocaleString())).toBeInTheDocument()
expect(screen.getByText(formatDateDifference(media.date!))).toBeInTheDocument()
const backdrop = container.querySelector<HTMLImageElement>('img')
expect(backdrop).not.toBeNull()
expect((backdrop as HTMLImageElement).src).toContain('system/cache/image?url=')
expect((backdrop as HTMLImageElement).src).toContain(encodeURIComponent(media.backdrop!))
const poster = await loadPoster(container)
expect(poster.src).toContain('system/cache/image?url=')
expect(poster.src).toContain(encodeURIComponent(media.poster!))
const dateMetadata = screen.getByText(formatDateDifference(media.date!)).closest('.v-card-text')
expect(dateMetadata?.querySelector('[data-icon="mdi-calendar"], [data-icon="mdi:calendar"]')).not.toBeNull()
})
it('falls back to the poster when the backdrop is missing and hides a zero reuse count', async () => {
const { container, media } = await renderCard({ backdrop: undefined, count: 0 })
const backdrop = container.querySelector<HTMLImageElement>('img')
expect(backdrop).not.toBeNull()
expect((backdrop as HTMLImageElement).src).toContain(media.poster!)
expect(screen.queryByText('0')).not.toBeInTheDocument()
})
it.each([
['TMDB before Douban', { doubanid: '2202', tmdbid: 1101 }, 'tmdb:1101'],
['Douban without TMDB', { doubanid: '2202', tmdbid: undefined }, 'douban:2202'],
['Bangumi without TMDB or Douban', { bangumiid: 3303, doubanid: undefined, tmdbid: undefined }, 'bangumi:3303'],
] as const)('routes media details with %s while keeping the fork dialog closed', async (_case, ids, mediaid) => {
const { container, media } = await renderCard(ids)
const poster = await loadPoster(container)
await fireEvent.click(poster)
expect(mocks.routerPush).toHaveBeenCalledWith({
path: '/media',
query: {
mediaid,
title: media.name,
type: media.type,
year: media.year,
},
})
expect(mocks.openSharedDialog).not.toHaveBeenCalled()
})
it('opens the fork dialog with the exact media and replaces it with editing after fork success', async () => {
const { container, media } = await renderCard()
const card = container.querySelector<HTMLElement>('.v-card')
expect(card).not.toBeNull()
await fireEvent.click(card as HTMLElement)
expect(mocks.openSharedDialog).toHaveBeenCalledOnce()
const forkDialog = getDialogCall()
expect(forkDialog.props).toEqual({ media })
expect(forkDialog.options).toEqual({ closeOn: ['close', 'fork', 'delete'] })
forkDialog.events.fork(4701)
expect(mocks.openSharedDialog).toHaveBeenCalledTimes(2)
const editDialog = getDialogCall(1)
expect(editDialog.props).toEqual({ subid: 4701 })
expect(editDialog.events).toEqual({})
expect(editDialog.options).toEqual({ closeOn: ['close', 'save', 'remove'] })
})
it('forwards deletion from the fork dialog without depending on its response payload', async () => {
const { container, emitted } = await renderCard()
const card = container.querySelector<HTMLElement>('.v-card')
expect(card).not.toBeNull()
await fireEvent.click(card as HTMLElement)
getDialogCall().events.delete({ id: 9999 })
expect(emitted('delete')).toHaveLength(1)
expect(emitted('delete')?.[0]).toEqual([])
})
})