test(resource): cover torrent card interactions (#608)

This commit is contained in:
InfinityPacer
2026-07-31 12:20:05 +08:00
committed by GitHub
parent a1802f7f93
commit 2c2f55b6d7
6 changed files with 723 additions and 38 deletions

View File

@@ -187,16 +187,6 @@
"count": 2
}
},
"src/components/cards/TorrentCard.vue": {
"@typescript-eslint/no-unused-vars": {
"count": 1
}
},
"src/components/cards/TorrentItem.vue": {
"@typescript-eslint/no-unused-vars": {
"count": 1
}
},
"src/components/cards/UserCard.vue": {
"@typescript-eslint/no-explicit-any": {
"count": 1

View File

@@ -76,9 +76,12 @@ async function handleAddDownload(item: Context | null = null) {
openSharedDialog(
AddDownloadDialog,
{
title: `${downloadItem.value?.media_info?.title_year || downloadItem.value?.meta_info?.name} ${
downloadItem.value?.meta_info?.season_episode
}`,
title: [
downloadItem.value?.media_info?.title_year || downloadItem.value?.meta_info?.name,
downloadItem.value?.meta_info?.season_episode,
]
.filter(Boolean)
.join(' '),
media: downloadItem.value?.media_info,
torrent: downloadItem.value?.torrent_info,
},
@@ -92,16 +95,10 @@ async function handleAddDownload(item: Context | null = null) {
// 打开种子详情页面
function openTorrentDetail(item: Context | null = null) {
if (item && !isNullOrEmptyObject(item) && !isNullOrEmptyObject(item.torrent_info)) {
window.open(item.torrent_info.page_url, '_blank')
return
const pageUrl = item && !isNullOrEmptyObject(item) ? item.torrent_info?.page_url : torrent.value?.page_url
if (pageUrl) {
window.open(pageUrl, '_blank')
}
window.open(torrent.value?.page_url, '_blank')
}
// 下载种子文件
async function downloadTorrentFile() {
window.open(torrent.value?.enclosure, '_blank')
}
// 获取优惠类型样式

View File

@@ -29,15 +29,16 @@ const siteIcon = ref('')
const isDownloaded = computed(() => Boolean(torrent.value?.enclosure && downloadedTorrentMap[torrent.value.enclosure]))
// 查询站点图标
async function getSiteIcon() {
if (!torrent?.value?.site) {
async function getSiteIcon(site: number | undefined) {
if (!site) {
siteIcon.value = ''
return
}
try {
siteIcon.value = await getCachedSiteIcon(torrent.value.site, async () => {
const icon = await getCachedSiteIcon(site, async () => {
try {
const response = await api.get(`site/icon/${torrent.value?.site}`)
const response = await api.get(`site/icon/${site}`)
return response?.data?.icon || ''
} catch (error) {
@@ -45,9 +46,15 @@ async function getSiteIcon() {
return ''
}
})
// 只提交当前站点的响应,避免 Context 快速切换时旧请求覆盖新图标。
if (torrent.value?.site === site) {
siteIcon.value = icon
}
} catch (error) {
console.error('Failed to load site icon:', error)
siteIcon.value = ''
if (torrent.value?.site === site) {
siteIcon.value = ''
}
}
}
@@ -60,15 +67,6 @@ function getPromotionClass(downloadVolumeFactor: number | undefined, uploadVolum
else return ''
}
// 获取优惠标签类
function getPromotionChipClass(downloadVolumeFactor: number | undefined, uploadVolumeFactor: number | undefined) {
if (!downloadVolumeFactor) return 'chip-free'
if (downloadVolumeFactor === 0) return 'chip-free'
else if (downloadVolumeFactor < 1) return 'chip-discount'
else if (uploadVolumeFactor !== undefined && uploadVolumeFactor > 1) return 'chip-bonus'
else return ''
}
// 询问并添加下载
async function handleAddDownload() {
// 打开下载对话框
@@ -99,7 +97,9 @@ function addDownloadError(error: string) {
// 打开种子详情页面
function openTorrentDetail() {
window.open(torrent.value?.page_url, '_blank')
if (torrent.value?.page_url) {
window.open(torrent.value.page_url, '_blank')
}
}
watch(
@@ -108,7 +108,7 @@ watch(
torrent.value = value?.torrent_info
media.value = value?.media_info
meta.value = value?.meta_info
getSiteIcon()
getSiteIcon(value?.torrent_info?.site)
},
{ immediate: true },
)

View File

@@ -0,0 +1,346 @@
import { formatDateDifference, formatFileSize } from '@/@core/utils/formatters'
import type { Context, MediaInfo, MetaInfo, TorrentInfo } from '@/api/types'
import TorrentCard from '@/components/cards/TorrentCard.vue'
import { downloadedTorrentMap } from '@/utils/torrentDownloadCache'
import { fireEvent, screen, waitFor } from '@testing-library/vue'
import { renderWithProviders } from '@tests/support/render'
import { defineComponent, h } from 'vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
getCachedSiteIcon: vi.fn(),
openSharedDialog: vi.fn(),
}))
vi.mock('@/composables/useSharedDialog', () => ({
openSharedDialog: (...args: unknown[]) => mocks.openSharedDialog(...args),
}))
vi.mock('@/utils/siteIconCache', () => ({
getCachedSiteIcon: (...args: unknown[]) => mocks.getCachedSiteIcon(...args),
}))
const ImageStub = defineComponent({
inheritAttrs: false,
props: {
alt: String,
src: String,
},
setup: props => () => h('img', { alt: props.alt, src: props.src }),
})
interface ContextOverrides {
media?: Partial<MediaInfo>
meta?: Partial<MetaInfo>
torrent?: Partial<TorrentInfo>
}
function createContext(overrides: ContextOverrides = {}): Context {
return {
media_info: {
episode_run_time: [],
origin_country: [],
title: '测试电影',
title_year: '测试电影 (2026)',
...overrides.media,
},
meta_info: {
apply_words: [],
audio_term: '',
edition: '',
episode: '',
episode_list: [],
episode_seq: '',
episode_seqs: '',
episodes: '',
isfile: false,
name: '测试电影',
release_group: '',
resource_term: '',
sea: '',
season: '',
season_episode: '',
season_list: [],
season_seq: '',
total_episode: 0,
total_season: 0,
type: '电影',
video_term: '',
web_source: '',
...overrides.meta,
},
torrent_info: {
category: '电影',
downloadvolumefactor: 1,
enclosure: 'https://downloads.example.com/test.torrent',
freedate: '',
freedate_diff: '',
grabs: 3,
hit_and_run: false,
imdbid: 'tt1000001',
labels: [],
page_url: 'https://tracker.example.com/details/1001',
peers: 2,
pri_order: 0,
seeders: 10,
site: 101,
site_name: '测试站',
site_order: 0,
site_proxy: false,
size: 1024,
title: 'Test.Movie.2026.1080p',
uploadvolumefactor: 1,
volume_factor: 'FREE',
...overrides.torrent,
},
}
}
async function renderCard(context: Context, more: Context[] = []) {
return renderWithProviders(TorrentCard, {
global: { stubs: { VImg: ImageStub } },
props: { more, torrent: context },
})
}
function getCard(container: Element) {
const card = container.querySelector<HTMLElement>('.torrent-card')
expect(card).not.toBeNull()
return card as HTMLElement
}
function getDetailButton(container: Element) {
const button = container.querySelector<HTMLButtonElement>('.v-card-actions .v-btn--icon')
expect(button).not.toBeNull()
return button as HTMLButtonElement
}
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('TorrentCard approved regressions', () => {
beforeEach(() => {
Object.keys(downloadedTorrentMap).forEach(url => delete downloadedTorrentMap[url])
mocks.getCachedSiteIcon.mockImplementation((site: number) =>
Promise.resolve(`https://images.example.com/site-${site}.png`),
)
mocks.openSharedDialog.mockReturnValue({ close: vi.fn(), id: 1, updateProps: vi.fn() })
vi.spyOn(console, 'error').mockImplementation(() => {})
})
it('omits a missing season and episode from the download title', async () => {
const context = createContext({ meta: { season_episode: undefined } })
const { container } = await renderCard(context)
await fireEvent.click(getCard(container))
await waitFor(() => expect(mocks.openSharedDialog).toHaveBeenCalledOnce())
expect(mocks.openSharedDialog.mock.calls[0]?.[1]).toMatchObject({
media: context.media_info,
title: '测试电影 (2026)',
torrent: context.torrent_info,
})
})
it('does not open a blank detail page or trigger download when the URL is missing', async () => {
const open = vi.spyOn(window, 'open').mockImplementation(() => null)
const context = createContext({ torrent: { page_url: undefined } })
const { container } = await renderCard(context)
await fireEvent.click(getDetailButton(container))
expect(open).not.toHaveBeenCalled()
expect(mocks.openSharedDialog).not.toHaveBeenCalled()
})
})
describe('TorrentCard display and interactions', () => {
beforeEach(() => {
Object.keys(downloadedTorrentMap).forEach(url => delete downloadedTorrentMap[url])
mocks.getCachedSiteIcon.mockImplementation((site: number) =>
Promise.resolve(`https://images.example.com/site-${site}.png`),
)
mocks.openSharedDialog.mockReturnValue({ close: vi.fn(), id: 1, updateProps: vi.fn() })
vi.spyOn(console, 'error').mockImplementation(() => {})
})
it('renders the current title, release metadata, tracker state, labels, size, and time', async () => {
const context = createContext({
media: { title: '可见媒体' },
meta: {
edition: 'IMAX',
resource_pix: '2160p',
resource_team: '测试组',
season_episode: 'S02E03',
subtitle: '国粤双语',
video_encode: 'HEVC',
web_source: 'Netflix',
},
torrent: {
description: '种子描述',
downloadvolumefactor: 0,
freedate_diff: '剩余 1 天',
hit_and_run: true,
labels: ['国语', '杜比视界'],
peers: 4,
pubdate: '2026-07-29 12:00:00',
seeders: 12,
size: 1536,
site_name: '高清测试站',
title: 'Visible.Media.S02E03.2160p',
uploadvolumefactor: 2,
},
})
const { container } = await renderCard(context)
for (const text of [
'可见媒体',
'S02E03',
'Visible.Media.S02E03.2160p',
'国粤双语',
'高清测试站',
'Netflix',
'IMAX',
'2160p',
'HEVC',
'测试组',
'国语',
'杜比视界',
'H&R',
'剩余 1 天',
formatFileSize(1536),
formatDateDifference('2026-07-29 12:00:00'),
]) {
expect(screen.getByText(text)).toBeInTheDocument()
}
expect(container.querySelector('.discount-banner')).toHaveClass('bg-success')
await waitFor(() => expect(container.querySelector<HTMLImageElement>('img')?.src).toContain('site-101.png'))
})
it('falls back to the recognized name and tracker initial when richer metadata and icon are unavailable', async () => {
mocks.getCachedSiteIcon.mockRejectedValue(new Error('icon unavailable'))
const context = createContext({
media: { title: undefined },
meta: { name: '识别名称', subtitle: undefined },
torrent: { description: undefined, site_name: '备用站' },
})
const { container } = await renderCard(context)
expect(screen.getByText('识别名称')).toBeInTheDocument()
expect(screen.getByText('备')).toBeInTheDocument()
expect(container.querySelector('img')).not.toBeInTheDocument()
})
it.each([
['discount', 0.5, 1, 'bg-orange'],
['upload bonus', 1, 2, 'bg-purple'],
['unclassified promotion', 2, 1, null],
] as const)('projects the %s promotion style', async (_case, downloadFactor, uploadFactor, expectedClass) => {
const { container } = await renderCard(
createContext({
torrent: {
downloadvolumefactor: downloadFactor,
uploadvolumefactor: uploadFactor,
volume_factor: _case,
},
}),
)
const banner = container.querySelector('.discount-banner')
expect(banner).toBeInTheDocument()
if (expectedClass) expect(banner).toHaveClass(expectedClass)
else expect(banner).not.toHaveClass('bg-success', 'bg-orange', 'bg-purple')
})
it('opens the download dialog with the current Context and shares successful download state across cards', async () => {
const context = createContext({ meta: { season_episode: 'S01E02' } })
const Harness = {
components: { TorrentCard },
data: () => ({ context }),
template: '<div><TorrentCard :torrent="context" /><TorrentCard :torrent="context" /></div>',
}
const { container } = await renderWithProviders(Harness, {
global: { stubs: { VImg: ImageStub } },
})
await fireEvent.click(container.querySelector('.torrent-card') as Element)
await waitFor(() => expect(mocks.openSharedDialog).toHaveBeenCalledOnce())
const { events, options, props } = getDialogCall()
expect(props).toEqual({
media: context.media_info,
title: '测试电影 (2026) S01E02',
torrent: context.torrent_info,
})
expect(options).toEqual({ closeOn: ['close', 'done', 'error'] })
events.done('')
expect(container.querySelectorAll('.torrent-card.border-success')).toHaveLength(0)
events.done(context.torrent_info.enclosure)
await waitFor(() => expect(container.querySelectorAll('.torrent-card.border-success')).toHaveLength(2))
events.error('下载失败')
expect(console.error).toHaveBeenCalledWith('下载失败')
})
it('opens more sources with candidate icons and keeps alternative download and detail Context intact', async () => {
const open = vi.spyOn(window, 'open').mockImplementation(() => null)
const primary = createContext()
const alternative = createContext({
media: { title: '备选媒体', title_year: '备选媒体 (2025)' },
meta: { name: '备选名称', season_episode: 'S03E04' },
torrent: {
enclosure: 'https://downloads.example.com/alternative.torrent',
page_url: 'https://tracker.example.com/details/202',
site: 202,
site_name: '备选站',
title: 'Alternative.Media.S03E04',
},
})
const { container } = await renderCard(primary, [alternative])
await fireEvent.click(screen.getByRole('button', { name: /更多来源/ }))
expect(mocks.openSharedDialog).toHaveBeenCalledOnce()
const moreDialog = getDialogCall()
expect(moreDialog.props.items).toEqual([alternative])
expect(moreDialog.options).toEqual({ closeOn: ['close', 'update:modelValue'] })
await waitFor(() =>
expect(moreDialog.props.siteIcons).toEqual(
expect.objectContaining({ 202: 'https://images.example.com/site-202.png' }),
),
)
moreDialog.events.download(alternative)
await waitFor(() => expect(mocks.openSharedDialog).toHaveBeenCalledTimes(2))
expect(getDialogCall(1).props).toEqual({
media: alternative.media_info,
title: '备选媒体 (2025) S03E04',
torrent: alternative.torrent_info,
})
moreDialog.events.detail(alternative)
expect(open).toHaveBeenCalledWith(alternative.torrent_info.page_url, '_blank')
expect(container.querySelector('.torrent-card')).toBeInTheDocument()
})
it('opens an available primary detail URL without starting a download', async () => {
const open = vi.spyOn(window, 'open').mockImplementation(() => null)
const context = createContext()
const { container } = await renderCard(context)
await fireEvent.click(getDetailButton(container))
expect(open).toHaveBeenCalledWith(context.torrent_info.page_url, '_blank')
expect(mocks.openSharedDialog).not.toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,331 @@
import { formatDateDifference, formatFileSize } from '@/@core/utils/formatters'
import type { Context, MediaInfo, MetaInfo, TorrentInfo } from '@/api/types'
import TorrentItem from '@/components/cards/TorrentItem.vue'
import { downloadedTorrentMap } from '@/utils/torrentDownloadCache'
import { fireEvent, screen, waitFor } from '@testing-library/vue'
import { renderWithProviders } from '@tests/support/render'
import { defineComponent, h } from 'vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
getCachedSiteIcon: vi.fn(),
openSharedDialog: vi.fn(),
}))
vi.mock('@/composables/useSharedDialog', () => ({
openSharedDialog: (...args: unknown[]) => mocks.openSharedDialog(...args),
}))
vi.mock('@/utils/siteIconCache', () => ({
getCachedSiteIcon: (...args: unknown[]) => mocks.getCachedSiteIcon(...args),
}))
const ImageStub = defineComponent({
inheritAttrs: false,
props: {
alt: String,
src: String,
},
setup: props => () => h('img', { alt: props.alt, src: props.src }),
})
interface ContextOverrides {
media?: Partial<MediaInfo>
meta?: Partial<MetaInfo>
torrent?: Partial<TorrentInfo>
}
interface Deferred<T> {
promise: Promise<T>
resolve: (value: T) => void
}
function createDeferred<T>(): Deferred<T> {
let resolve!: (value: T) => void
const promise = new Promise<T>(done => {
resolve = done
})
return { promise, resolve }
}
function createContext(overrides: ContextOverrides = {}): Context {
return {
media_info: {
episode_run_time: [],
origin_country: [],
title: '测试电影',
title_year: '测试电影 (2026)',
...overrides.media,
},
meta_info: {
apply_words: [],
audio_term: '',
edition: '',
episode: '',
episode_list: [],
episode_seq: '',
episode_seqs: '',
episodes: '',
isfile: false,
name: '测试电影',
release_group: '',
resource_term: '',
sea: '',
season: '',
season_episode: '',
season_list: [],
season_seq: '',
total_episode: 0,
total_season: 0,
type: '电影',
video_term: '',
web_source: '',
...overrides.meta,
},
torrent_info: {
category: '电影',
downloadvolumefactor: 1,
enclosure: 'https://downloads.example.com/test.torrent',
freedate: '',
freedate_diff: '',
grabs: 3,
hit_and_run: false,
imdbid: 'tt1000001',
labels: [],
page_url: 'https://tracker.example.com/details/1001',
peers: 2,
pri_order: 0,
seeders: 10,
site: 101,
site_name: '测试站',
site_order: 0,
site_proxy: false,
size: 1024,
title: 'Test.Movie.2026.1080p',
uploadvolumefactor: 1,
volume_factor: 'FREE',
...overrides.torrent,
},
}
}
async function renderItem(context: Context) {
return renderWithProviders(TorrentItem, {
global: { stubs: { VImg: ImageStub } },
props: { torrent: context },
})
}
function getItem(container: Element) {
const item = container.querySelector<HTMLElement>('.torrent-item')
expect(item).not.toBeNull()
return item as HTMLElement
}
function getDetailButton(container: Element) {
const button = container.querySelector<HTMLButtonElement>('.v-list-item__append .v-btn--icon')
expect(button).not.toBeNull()
return button as HTMLButtonElement
}
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('TorrentItem approved regressions', () => {
beforeEach(() => {
Object.keys(downloadedTorrentMap).forEach(url => delete downloadedTorrentMap[url])
mocks.getCachedSiteIcon.mockResolvedValue('https://images.example.com/site-101.png')
mocks.openSharedDialog.mockReturnValue({ close: vi.fn(), id: 1, updateProps: vi.fn() })
vi.spyOn(console, 'error').mockImplementation(() => {})
})
it('does not open a blank detail page or trigger download when the URL is missing', async () => {
const open = vi.spyOn(window, 'open').mockImplementation(() => null)
const context = createContext({ torrent: { page_url: undefined } })
const { container } = await renderItem(context)
await fireEvent.click(getDetailButton(container))
expect(open).not.toHaveBeenCalled()
expect(mocks.openSharedDialog).not.toHaveBeenCalled()
})
it('keeps the current site icon when an older request resolves last', async () => {
const oldIcon = createDeferred<string>()
const currentIcon = createDeferred<string>()
mocks.getCachedSiteIcon.mockImplementation((site: number) => (site === 101 ? oldIcon.promise : currentIcon.promise))
const oldContext = createContext({ torrent: { site: 101, site_name: '旧站' } })
const currentContext = createContext({ torrent: { site: 202, site_name: '当前站' } })
const { container, rerender } = await renderItem(oldContext)
await waitFor(() => expect(mocks.getCachedSiteIcon).toHaveBeenCalledWith(101, expect.any(Function)))
await rerender({ torrent: currentContext })
await waitFor(() => expect(mocks.getCachedSiteIcon).toHaveBeenCalledWith(202, expect.any(Function)))
currentIcon.resolve('https://images.example.com/site-202.png')
await waitFor(() => expect(container.querySelector<HTMLImageElement>('img')?.src).toContain('site-202.png'))
oldIcon.resolve('https://images.example.com/site-101.png')
await waitFor(() => expect(mocks.getCachedSiteIcon).toHaveBeenCalledTimes(2))
await Promise.resolve()
expect(container.querySelector<HTMLImageElement>('img')?.src).toContain('site-202.png')
})
})
describe('TorrentItem display and interactions', () => {
beforeEach(() => {
Object.keys(downloadedTorrentMap).forEach(url => delete downloadedTorrentMap[url])
mocks.getCachedSiteIcon.mockImplementation((site: number) =>
Promise.resolve(`https://images.example.com/site-${site}.png`),
)
mocks.openSharedDialog.mockReturnValue({ close: vi.fn(), id: 1, updateProps: vi.fn() })
vi.spyOn(console, 'error').mockImplementation(() => {})
})
it('renders the current title, release metadata, tracker state, labels, size, and time', async () => {
const context = createContext({
media: { title: '列表媒体' },
meta: {
edition: '导演剪辑版',
resource_pix: '1080p',
resource_team: '列表组',
season_episode: 'S01E08',
subtitle: '简繁字幕',
video_encode: 'AV1',
web_source: 'Apple TV+',
},
torrent: {
downloadvolumefactor: 0.5,
freedate_diff: '剩余 2 小时',
hit_and_run: true,
labels: ['国语', '高码率'],
peers: 6,
pubdate: '2026-07-29 18:00:00',
seeders: 24,
size: 2048,
site_name: '列表测试站',
title: 'List.Media.S01E08.1080p',
uploadvolumefactor: 1,
},
})
const { container } = await renderItem(context)
for (const text of [
'列表媒体',
'S01E08',
'List.Media.S01E08.1080p',
'简繁字幕',
'Apple TV+',
'导演剪辑版',
'1080p',
'AV1',
'列表组',
'国语',
'高码率',
'H&R',
'剩余 2 小时',
formatFileSize(2048),
formatDateDifference('2026-07-29 18:00:00'),
]) {
expect(screen.getByText(text)).toBeInTheDocument()
}
expect(container.querySelector('.discount-banner')).toHaveClass('bg-orange')
expect(container.querySelector('.torrent-item [title="列表测试站"]')).toBeInTheDocument()
await waitFor(() => expect(container.querySelector<HTMLImageElement>('img')?.src).toContain('site-101.png'))
})
it('falls back to the recognized name, placeholder description, and tracker initial', async () => {
mocks.getCachedSiteIcon.mockRejectedValue(new Error('icon unavailable'))
const context = createContext({
media: { title: undefined },
meta: { name: '列表识别名称', subtitle: undefined },
torrent: { description: undefined, site_name: '备用列表站' },
})
const { container } = await renderItem(context)
expect(screen.getByText('列表识别名称')).toBeInTheDocument()
expect(screen.getByText('暂无描述')).toBeInTheDocument()
expect(screen.getByText('备')).toBeInTheDocument()
expect(container.querySelector('img')).not.toBeInTheDocument()
})
it.each([
['free', 0, 1, 'bg-success'],
['upload bonus', 1, 2, 'bg-purple'],
['unclassified promotion', 2, 1, null],
] as const)('projects the %s promotion style', async (_case, downloadFactor, uploadFactor, expectedClass) => {
const { container } = await renderItem(
createContext({
torrent: {
downloadvolumefactor: downloadFactor,
uploadvolumefactor: uploadFactor,
volume_factor: _case,
},
}),
)
const banner = container.querySelector('.discount-banner')
expect(banner).toBeInTheDocument()
if (expectedClass) expect(banner).toHaveClass(expectedClass)
else expect(banner).not.toHaveClass('bg-success', 'bg-orange', 'bg-purple')
})
it('opens the download dialog with the current Context and marks the item after success', async () => {
const context = createContext({ meta: { season_episode: 'S04E05' } })
const { container } = await renderItem(context)
await fireEvent.click(getItem(container))
await waitFor(() => expect(mocks.openSharedDialog).toHaveBeenCalledOnce())
const { events, options, props } = getDialogCall()
expect(props).toEqual({
media: context.media_info,
title: '测试电影 (2026) S04E05',
torrent: context.torrent_info,
})
expect(options).toEqual({ closeOn: ['close', 'done', 'error'] })
events.done('')
expect(getItem(container)).not.toHaveClass('border-success')
events.done(context.torrent_info.enclosure)
await waitFor(() => expect(getItem(container)).toHaveClass('border-success'))
events.error('下载失败')
expect(console.error).toHaveBeenCalledWith('下载失败')
})
it('opens an available detail URL without starting a download', async () => {
const open = vi.spyOn(window, 'open').mockImplementation(() => null)
const context = createContext()
const { container } = await renderItem(context)
await fireEvent.click(getDetailButton(container))
expect(open).toHaveBeenCalledWith(context.torrent_info.page_url, '_blank')
expect(mocks.openSharedDialog).not.toHaveBeenCalled()
})
it('clears a resolved icon when the current Context has no site identity', async () => {
const current = createContext({ torrent: { site: 101, site_name: '当前站' } })
const withoutSite = createContext({ torrent: { site: undefined, site_name: '无站点' } })
const { container, rerender } = await renderItem(current)
await waitFor(() => expect(container.querySelector<HTMLImageElement>('img')?.src).toContain('site-101.png'))
await rerender({ torrent: withoutSite })
await waitFor(() => expect(container.querySelector('img')).not.toBeInTheDocument())
expect(screen.getByText('无')).toBeInTheDocument()
expect(mocks.getCachedSiteIcon).toHaveBeenCalledOnce()
})
})

View File

@@ -302,6 +302,9 @@ export default defineConfig(({ command, mode, isPreview }) => ({
'src/composables/useTorrentFilter.ts',
'src/components/cards/SubscribeCard.vue',
'src/components/filter/TorrentFilterBar.vue',
'src/components/cards/TorrentCard.vue',
'src/components/cards/TorrentItem.vue',
'src/utils/torrentDownloadCache.ts',
'src/components/dialog/SiteAddEditDialog.vue',
'src/components/dialog/SiteCookieUpdateDialog.vue',
'src/components/dialog/SiteImportDialog.vue',
@@ -345,6 +348,24 @@ export default defineConfig(({ command, mode, isPreview }) => ({
lines: 80,
statements: 80,
},
'src/components/cards/TorrentCard.vue': {
branches: 75,
functions: 80,
lines: 80,
statements: 80,
},
'src/components/cards/TorrentItem.vue': {
branches: 75,
functions: 80,
lines: 80,
statements: 80,
},
'src/utils/torrentDownloadCache.ts': {
branches: 85,
functions: 90,
lines: 90,
statements: 90,
},
'src/components/cards/SubscribeShareCard.vue': {
branches: 75,
functions: 80,