test(download): cover downloading task workflows (#612)

This commit is contained in:
InfinityPacer
2026-07-31 15:25:31 +08:00
committed by GitHub
parent 38fe17ab99
commit 39395ca6b6
7 changed files with 589 additions and 73 deletions

View File

@@ -115,11 +115,6 @@
"count": 27
}
},
"src/components/cards/DownloadingCard.vue": {
"@typescript-eslint/no-explicit-any": {
"count": 1
}
},
"src/components/cards/FilterRuleCard.vue": {
"vue/no-mutating-props": {
"count": 1
@@ -995,11 +990,6 @@
"count": 4
}
},
"src/views/reorganize/DownloadingListView.vue": {
"@typescript-eslint/no-unused-vars": {
"count": 2
}
},
"src/views/reorganize/FileBrowserView.vue": {
"@typescript-eslint/no-explicit-any": {
"count": 2

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
import api from '@/api'
import type { DownloadingInfo } from '@/api/types'
import type { ApiResponse, DownloadingInfo } from '@/api/types'
import { formatFileSize } from '@/@core/utils/formatters'
// 输入参数
@@ -35,19 +35,11 @@ watch(
},
)
// 图片是否加载完成
const imageLoaded = ref(false)
// 图片加载完成响应
function imageLoadHandler() {
imageLoaded.value = true
}
// 下载状态控制
async function toggleDownload() {
const operation = isDownloading.value ? 'stop' : 'start'
try {
const result: { [key: string]: any } = await api.get(`download/${operation}/${props.info?.hash}`, {
const result: ApiResponse<unknown> = await api.get(`download/${operation}/${props.info?.hash}`, {
params: {
name: props.downloaderName,
},
@@ -59,11 +51,13 @@ async function toggleDownload() {
}
}
// 删除下
// 删除下载任务
async function deleteDownload() {
try {
await api.delete(`download/${props.info?.hash}`, { params: { name: props.downloaderName } })
cardState.value = false
const result: ApiResponse<unknown> = await api.delete(`download/${props.info?.hash}`, {
params: { name: props.downloaderName },
})
if (result.success) cardState.value = false
} catch (error) {
console.error(error)
}
@@ -83,53 +77,46 @@ async function deleteDownload() {
}"
min-height="150"
>
<template #image>
<VImg
:src="props.info?.media.image"
class="downloading-card-image"
aspect-ratio="2/3"
cover
@load="imageLoadHandler"
position="top"
>
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
</div>
</template>
<template #default>
<div class="absolute inset-0 outline-none downloading-card-background"></div>
</template>
</VImg>
</template>
<template #image>
<VImg :src="props.info?.media.image" class="downloading-card-image" aspect-ratio="2/3" cover position="top">
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
</div>
</template>
<template #default>
<div class="absolute inset-0 outline-none downloading-card-background"></div>
</template>
</VImg>
</template>
<div>
<VCardTitle class="break-words whitespace-normal text-white">
{{ props.info?.media.title || props.info?.name }}
{{
props.info?.media.episode
? `${props.info?.media.season} ${props.info?.media.episode}`
: props.info?.season_episode
}}
</VCardTitle>
<div>
<VCardTitle class="break-words whitespace-normal text-white">
{{ props.info?.media.title || props.info?.name }}
{{
props.info?.media.episode
? `${props.info?.media.season} ${props.info?.media.episode}`
: props.info?.season_episode
}}
</VCardTitle>
<VCardSubtitle class="break-words whitespace-normal text-white">
{{ props.info?.title }}
</VCardSubtitle>
<VCardSubtitle class="break-words whitespace-normal text-white">
{{ props.info?.title }}
</VCardSubtitle>
<VCardText class="text-subtitle-1 pt-3 pb-1 text-white">
{{ getSpeedText() }}
</VCardText>
<VCardText class="text-subtitle-1 pt-3 pb-1 text-white">
{{ getSpeedText() }}
</VCardText>
<VCardText v-if="getPercentage() > 0" class="text-white">
<VProgressLinear :model-value="getPercentage()" bg-color="success" color="success" />
</VCardText>
<VCardText v-if="getPercentage() > 0" class="text-white">
<VProgressLinear :model-value="getPercentage()" bg-color="success" color="success" />
</VCardText>
<VCardActions class="justify-space-between">
<VBtn :icon="`${isDownloading ? 'mdi-pause' : 'mdi-play'}`" @click="toggleDownload" />
<VBtn color="error" icon="mdi-trash-can-outline" @click="deleteDownload" />
</VCardActions>
</div>
<VCardActions class="justify-space-between">
<VBtn :icon="`${isDownloading ? 'mdi-pause' : 'mdi-play'}`" @click="toggleDownload" />
<VBtn color="error" icon="mdi-trash-can-outline" @click="deleteDownload" />
</VCardActions>
</div>
</VCard>
</div>
</template>

View File

@@ -0,0 +1,139 @@
import type { DownloadingInfo } from '@/api/types'
import DownloadingCard from '@/components/cards/DownloadingCard.vue'
import { fireEvent, screen, waitFor } from '@testing-library/vue'
import { renderWithProviders } from '@tests/support/render'
import { deleteDownloadHandler, downloadActionHandler } from '@tests/support/msw/handlers/download'
import { server } from '@tests/support/msw/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'
function downloading(overrides: Partial<DownloadingInfo> = {}): DownloadingInfo {
return {
dlspeed: '2 MiB',
hash: 'hash-1',
left_time: '1 小时',
media: {
episode: 'E02',
image: 'https://images.example.com/poster.jpg',
season: 'S01',
title: '测试媒体',
},
name: 'fallback-name',
progress: 40,
season_episode: 'S01E02',
size: 1024,
state: 'downloading',
title: '下载任务标题',
upspeed: '1 MiB',
...overrides,
}
}
async function renderCard(info = downloading(), downloaderName = 'qb-main') {
return renderWithProviders(DownloadingCard, {
props: { downloaderName, info },
})
}
function actionButtons(container: Element) {
const buttons = [...container.querySelectorAll<HTMLButtonElement>('.v-card-actions button')]
expect(buttons).toHaveLength(2)
return { deleteButton: buttons[1]!, toggleButton: buttons[0]! }
}
beforeEach(() => {
vi.spyOn(console, 'error').mockImplementation(() => {})
})
describe('DownloadingCard display and pause state', () => {
it('renders task metadata, progress, speed and the current download state', async () => {
const { container } = await renderCard()
expect(screen.getByText(/测试媒体/)).toBeInTheDocument()
expect(screen.getByText(/S01 E02/)).toBeInTheDocument()
expect(screen.getByText('下载任务标题')).toBeInTheDocument()
expect(screen.getByText(/1 小时/)).toBeInTheDocument()
expect(container.querySelector('.v-card-text .v-progress-linear')).toBeInTheDocument()
})
it('falls back to the task name and season string when media recognition is incomplete', async () => {
const { container } = await renderCard(
downloading({
media: {},
name: '未识别任务',
progress: 0,
season_episode: 'S03E04',
state: 'stopped',
}),
)
expect(screen.getByText(/未识别任务/)).toBeInTheDocument()
expect(screen.getByText(/S03E04/)).toBeInTheDocument()
expect(container.querySelector('.v-card-text .v-progress-linear')).not.toBeInTheDocument()
})
it('uses the current operation and downloader name, changing state only on business success', async () => {
const stopRequested = vi.fn()
const startRequested = vi.fn()
server.use(
downloadActionHandler('stop', 'hash-1', { success: false }, 200, stopRequested),
downloadActionHandler('start', 'hash-1', { success: true }, 200, startRequested),
)
const { container, rerender } = await renderCard()
const { toggleButton } = actionButtons(container)
await fireEvent.click(toggleButton)
await waitFor(() => expect(stopRequested).toHaveBeenCalledOnce())
expect(stopRequested.mock.calls[0][0].searchParams.get('name')).toBe('qb-main')
await fireEvent.click(toggleButton)
await waitFor(() => expect(stopRequested).toHaveBeenCalledTimes(2))
await rerender({ downloaderName: 'transmission', info: downloading({ state: 'stopped' }) })
await fireEvent.click(toggleButton)
await waitFor(() => expect(startRequested).toHaveBeenCalledOnce())
expect(startRequested.mock.calls[0][0].searchParams.get('name')).toBe('transmission')
await fireEvent.click(toggleButton)
await waitFor(() => expect(stopRequested).toHaveBeenCalledTimes(3))
})
it('keeps the current state when the pause request fails at the HTTP boundary', async () => {
server.use(downloadActionHandler('stop', 'hash-1', { success: false }, 503))
const { container } = await renderCard()
await fireEvent.click(actionButtons(container).toggleButton)
await waitFor(() => expect(console.error).toHaveBeenCalled())
})
})
describe('DownloadingCard deletion', () => {
it('keeps the card visible when HTTP 200 reports business failure', async () => {
const requested = vi.fn()
server.use(deleteDownloadHandler('hash-1', { success: false, message: '任务仍在运行' }, 200, requested))
const { container } = await renderCard()
await fireEvent.click(actionButtons(container).deleteButton)
await waitFor(() => expect(requested).toHaveBeenCalledOnce())
expect(requested.mock.calls[0][0].searchParams.get('name')).toBe('qb-main')
expect(container.querySelector('.downloading-card')).toBeInTheDocument()
})
it('hides the card only after business success', async () => {
server.use(deleteDownloadHandler('hash-1', { success: true }))
const { container } = await renderCard()
await fireEvent.click(actionButtons(container).deleteButton)
await waitFor(() => expect(container.querySelector('.downloading-card')).not.toBeInTheDocument())
})
it('keeps the card visible when deletion fails at the HTTP boundary', async () => {
server.use(deleteDownloadHandler('hash-1', { success: false }, 503))
const { container } = await renderCard()
await fireEvent.click(actionButtons(container).deleteButton)
await waitFor(() => expect(console.error).toHaveBeenCalled())
expect(container.querySelector('.downloading-card')).toBeInTheDocument()
})
})

View File

@@ -7,11 +7,11 @@ import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue'
import { useUserStore } from '@/stores'
import { useI18n } from 'vue-i18n'
import { useBackground } from '@/composables/useBackground'
import { useKeepAliveRefresh, type KeepAliveRefreshContext } from '@/composables/useKeepAliveRefresh'
import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh'
// 国际化
const { t } = useI18n()
const { useDataRefresh } = useBackground()
const { useConditionalDataRefresh } = useBackground()
// 定义输入参数
const props = defineProps<{
@@ -28,8 +28,8 @@ const dataList = ref<DownloadingInfo[]>([])
// 是否刷新过
const isRefreshed = ref(false)
// 获取订阅列表数据
async function fetchData(_context: KeepAliveRefreshContext = {}) {
// 获取当前下载器的任务快照
async function fetchData() {
try {
dataList.value = await api.get('download/', { params: { name: props.name } })
isRefreshed.value = true
@@ -38,7 +38,7 @@ async function fetchData(_context: KeepAliveRefreshContext = {}) {
}
}
// 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅
// 管理员显示全部下载任务,普通用户仅显示本人任务
const filteredDataList = computed(() => {
// 从 Store 中获取用户信息
const superUser = userStore.superUser
@@ -47,10 +47,11 @@ const filteredDataList = computed(() => {
else return dataList.value.filter(data => data.userid === userName || data.username === userName)
})
// 使用数据刷新定时器
const { loading: dataLoading } = useDataRefresh(
'downloading-list',
// 每个下载器独立持有刷新身份,非活动标签不占用轮询资源。
useConditionalDataRefresh(
`downloading-list-${props.name}`,
fetchData,
computed(() => props.active !== false),
3000, // 3秒间隔
false, // 初始加载交给 keep-alive 页面自身,避免同时发起两次请求
)

View File

@@ -0,0 +1,323 @@
import type { DownloadingInfo } from '@/api/types'
import DownloadingListView from '@/views/reorganize/DownloadingListView.vue'
import { fireEvent, screen, waitFor } from '@testing-library/vue'
import { renderWithProviders } from '@tests/support/render'
import { downloadingListHandler } from '@tests/support/msw/handlers/download'
import { server } from '@tests/support/msw/server'
import { defineComponent, ref, type PropType, type Ref } from 'vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const refreshMocks = vi.hoisted(() => ({
registrations: [] as Array<{
callback: () => Promise<void> | void
condition: Ref<boolean>
id: string
immediate: boolean
interval: number
kind: 'conditional' | 'unconditional'
}>,
}))
vi.mock('@/composables/useBackground', async () => {
const { computed, onMounted, ref, watch } = await import('vue')
function register(
kind: 'conditional' | 'unconditional',
id: string,
callback: () => Promise<void> | void,
condition: Ref<boolean>,
interval: number,
immediate: boolean,
) {
refreshMocks.registrations.push({ callback, condition, id, immediate, interval, kind })
if (kind === 'conditional') {
onMounted(() => {
if (condition.value && immediate) void callback()
})
watch(condition, active => {
if (active && immediate) void callback()
})
}
return {
isActive: ref(condition.value),
loading: ref(false),
refresh: callback,
start: vi.fn(),
stop: vi.fn(),
}
}
return {
useBackground: () => ({
useConditionalDataRefresh: (
id: string,
callback: () => Promise<void> | void,
condition: Ref<boolean>,
interval: number,
immediate = true,
) => register('conditional', id, callback, condition, interval, immediate),
useDataRefresh: (id: string, callback: () => Promise<void> | void, interval: number) =>
register(
'unconditional',
id,
callback,
computed(() => true),
interval,
true,
),
}),
}
})
const LoadingBannerStub = defineComponent({
template: '<div data-testid="loading-banner">loading</div>',
})
const NoDataFoundStub = defineComponent({
template: '<div data-testid="no-data">empty</div>',
})
const DownloadingCardStub = defineComponent({
props: {
downloaderName: String,
info: Object,
},
template: '<article :data-testid="`download-${info.hash}`">{{ info.title }}|{{ downloaderName }}</article>',
})
const ProgressiveCardGridStub = defineComponent({
props: {
getItemKey: {
type: Function as PropType<(item: DownloadingInfo) => string | undefined>,
required: true,
},
items: {
type: Array as PropType<DownloadingInfo[]>,
default: () => [],
},
},
template: `
<div data-testid="grid">
<div v-for="item in items" :key="getItemKey(item)" :data-item-key="getItemKey(item)">
<slot :item="item" />
</div>
</div>
`,
})
function downloading(hash: string, title: string, overrides: Partial<DownloadingInfo> = {}): DownloadingInfo {
return {
dlspeed: '2 MiB',
hash,
left_time: '1 小时',
media: { title },
name: title,
progress: 20,
size: 1024,
state: 'downloading',
title,
upspeed: '1 MiB',
userid: 'tester',
username: 'tester',
...overrides,
}
}
async function renderList(
props: { active?: boolean; name?: string } = {},
options: {
onRequest?: (url: URL) => void
response?: DownloadingInfo[] | ((url: URL) => DownloadingInfo[] | Promise<DownloadingInfo[]>)
status?: number
superUser?: boolean
userName?: string
} = {},
) {
server.use(downloadingListHandler(options.response ?? [], options.status ?? 200, options.onRequest))
return renderWithProviders(DownloadingListView, {
props: {
active: props.active ?? true,
name: props.name ?? 'primary',
},
initialState: {
user: {
superUser: options.superUser ?? false,
userName: options.userName ?? 'tester',
},
},
global: {
stubs: {
DownloadingCard: DownloadingCardStub,
LoadingBanner: LoadingBannerStub,
NoDataFound: NoDataFoundStub,
ProgressiveCardGrid: ProgressiveCardGridStub,
},
},
})
}
async function runRegisteredRefreshes() {
const jobsById = new Map(refreshMocks.registrations.map(registration => [registration.id, registration]))
await Promise.all(
[...jobsById.values()]
.filter(registration => registration.condition.value)
.map(registration => registration.callback()),
)
}
beforeEach(() => {
refreshMocks.registrations.length = 0
vi.spyOn(console, 'error').mockImplementation(() => {})
})
describe('DownloadingListView loading and ownership', () => {
it('queries the selected downloader and filters a normal user by either owner field', async () => {
const requested = vi.fn()
await renderList(
{ name: 'qb-main' },
{
onRequest: requested,
response: [
downloading('own-id', 'Own by id', { userid: 'tester', username: 'other' }),
downloading('', 'Own by name', { userid: 'other', username: 'tester' }),
downloading('other', 'Other task', { userid: 'other', username: 'other' }),
],
},
)
expect(await screen.findByText('Own by id|qb-main')).toBeInTheDocument()
expect(screen.getByText('Own by name|qb-main')).toBeInTheDocument()
expect(screen.getByText('Own by id|qb-main').parentElement).toHaveAttribute('data-item-key', 'own-id')
expect(screen.getByText('Own by name|qb-main').parentElement).toHaveAttribute('data-item-key', 'Own by name')
expect(screen.queryByText('Other task|qb-main')).not.toBeInTheDocument()
expect(requested).toHaveBeenCalledOnce()
expect(requested.mock.calls[0][0].searchParams.get('name')).toBe('qb-main')
})
it('lets a superuser see every task', async () => {
await renderList(
{ name: 'transmission' },
{
response: [
downloading('own', 'Own task'),
downloading('other', 'Other task', { userid: 'other', username: 'other' }),
],
superUser: true,
},
)
expect(await screen.findByText('Own task|transmission')).toBeInTheDocument()
expect(screen.getByText('Other task|transmission')).toBeInTheDocument()
})
it('replaces the loading state with the successful empty state', async () => {
let resolveResponse: ((value: DownloadingInfo[]) => void) | undefined
await renderList(
{},
{
response: () =>
new Promise<DownloadingInfo[]>(resolve => {
resolveResponse = resolve
}),
},
)
expect(screen.getByTestId('loading-banner')).toBeInTheDocument()
await waitFor(() => expect(resolveResponse).toBeTypeOf('function'))
resolveResponse?.([])
expect(await screen.findByTestId('no-data')).toBeInTheDocument()
expect(screen.queryByTestId('loading-banner')).not.toBeInTheDocument()
})
it('does not misrepresent an HTTP failure as a successful empty snapshot', async () => {
await renderList({}, { status: 503 })
await waitFor(() => expect(console.error).toHaveBeenCalled())
expect(screen.queryByTestId('no-data')).not.toBeInTheDocument()
})
})
describe('DownloadingListView refresh ownership', () => {
it('uses downloader-scoped identities and refreshes only the active downloader snapshot', async () => {
const requested = vi.fn()
const snapshots: Record<string, DownloadingInfo[]> = {
alpha: [downloading('alpha-old', 'Alpha old')],
beta: [downloading('beta-old', 'Beta old')],
}
server.use(downloadingListHandler(url => snapshots[url.searchParams.get('name') || ''] ?? [], 200, requested))
const Host = defineComponent({
components: { DownloadingListView },
setup() {
const activeName = ref('alpha')
return { activeName }
},
template: `
<button type="button" @click="activeName = 'beta'">activate beta</button>
<DownloadingListView name="alpha" :active="activeName === 'alpha'" />
<DownloadingListView name="beta" :active="activeName === 'beta'" />
`,
})
await renderWithProviders(Host, {
initialState: { user: { superUser: true, userName: 'admin' } },
global: {
stubs: {
DownloadingCard: DownloadingCardStub,
LoadingBanner: LoadingBannerStub,
NoDataFound: NoDataFoundStub,
ProgressiveCardGrid: ProgressiveCardGridStub,
},
},
})
expect(await screen.findByText('Alpha old|alpha')).toBeInTheDocument()
expect(await screen.findByText('Beta old|beta')).toBeInTheDocument()
expect(requested).toHaveBeenCalledTimes(2)
expect(requested.mock.calls.map(call => call[0].searchParams.get('name')).sort()).toEqual(['alpha', 'beta'])
snapshots.alpha = [downloading('alpha-new', 'Alpha new')]
snapshots.beta = [downloading('beta-new', 'Beta new')]
await runRegisteredRefreshes()
await waitFor(() => expect(screen.getByText('Alpha new|alpha')).toBeInTheDocument())
expect(screen.queryByText('Alpha old|alpha')).not.toBeInTheDocument()
expect(screen.getByText('Beta old|beta')).toBeInTheDocument()
expect(screen.queryByText('Beta new|beta')).not.toBeInTheDocument()
expect(requested).toHaveBeenCalledTimes(3)
expect(requested.mock.calls.filter(call => call[0].searchParams.get('name') === 'alpha')).toHaveLength(2)
expect(requested.mock.calls.filter(call => call[0].searchParams.get('name') === 'beta')).toHaveLength(1)
snapshots.alpha = [downloading('alpha-later', 'Alpha later')]
snapshots.beta = [downloading('beta-activated', 'Beta activated')]
await fireEvent.click(screen.getByRole('button', { name: 'activate beta' }))
await waitFor(() => expect(screen.getByText('Beta activated|beta')).toBeInTheDocument())
expect(requested).toHaveBeenCalledTimes(4)
expect(requested.mock.calls.filter(call => call[0].searchParams.get('name') === 'alpha')).toHaveLength(2)
expect(requested.mock.calls.filter(call => call[0].searchParams.get('name') === 'beta')).toHaveLength(2)
snapshots.beta = [downloading('beta-new', 'Beta new')]
await runRegisteredRefreshes()
await waitFor(() => expect(screen.getByText('Beta new|beta')).toBeInTheDocument())
expect(screen.getByText('Alpha new|alpha')).toBeInTheDocument()
expect(screen.queryByText('Alpha later|alpha')).not.toBeInTheDocument()
expect(requested).toHaveBeenCalledTimes(5)
expect(requested.mock.calls.filter(call => call[0].searchParams.get('name') === 'alpha')).toHaveLength(2)
expect(requested.mock.calls.filter(call => call[0].searchParams.get('name') === 'beta')).toHaveLength(3)
expect(refreshMocks.registrations).toEqual([
expect.objectContaining({
id: 'downloading-list-alpha',
immediate: false,
interval: 3000,
kind: 'conditional',
}),
expect.objectContaining({
id: 'downloading-list-beta',
immediate: false,
interval: 3000,
kind: 'conditional',
}),
])
})
})

View File

@@ -0,0 +1,62 @@
import type { DownloadingInfo } from '@/api/types'
import { HttpResponse, http, type JsonBodyType } from 'msw'
const API_BASE_URL = 'http://localhost/api/v1/'
export interface DownloadMutationResponse {
success: boolean
message?: string
}
export const downloadApiUrls = {
action: (operation: 'start' | 'stop', hash: string) => new URL(`download/${operation}/${hash}`, API_BASE_URL).href,
delete: (hash: string) => new URL(`download/${hash}`, API_BASE_URL).href,
list: new URL('download/', API_BASE_URL).href,
}
function jsonResponse(body: JsonBodyType, status: number) {
return HttpResponse.json(body, { status })
}
/** 拦截下载任务快照查询,并保留下载器查询参数供断言。 */
export function downloadingListHandler(
response: DownloadingInfo[] | ((url: URL) => DownloadingInfo[] | Promise<DownloadingInfo[]>) = [],
status = 200,
onRequest: (url: URL) => void | Promise<void> = () => {},
) {
return http.get(downloadApiUrls.list, async ({ request }) => {
const url = new URL(request.url)
await onRequest(url)
const body = typeof response === 'function' ? await response(url) : response
return jsonResponse(body as unknown as JsonBodyType, status)
})
}
/** 拦截暂停或继续下载请求,并保留下载器查询参数供断言。 */
export function downloadActionHandler(
operation: 'start' | 'stop',
hash: string,
response: DownloadMutationResponse = { success: true },
status = 200,
onRequest: (url: URL) => void | Promise<void> = () => {},
) {
return http.get(downloadApiUrls.action(operation, hash), async ({ request }) => {
const url = new URL(request.url)
await onRequest(url)
return jsonResponse(response, status)
})
}
/** 拦截删除下载任务请求,并保留下载器查询参数供断言。 */
export function deleteDownloadHandler(
hash: string,
response: DownloadMutationResponse = { success: true },
status = 200,
onRequest: (url: URL) => void | Promise<void> = () => {},
) {
return http.delete(downloadApiUrls.delete(hash), async ({ request }) => {
const url = new URL(request.url)
await onRequest(url)
return jsonResponse(response, status)
})
}

View File

@@ -331,11 +331,13 @@ export default defineConfig(({ command, mode, isPreview }) => ({
'src/views/discover/MediaDetailView.vue',
'src/components/cards/MediaCard.vue',
'src/components/cards/SiteCard.vue',
'src/components/cards/DownloadingCard.vue',
'src/components/slide/VirtualSlideView.vue',
'src/views/discover/PersonCardSlideView.vue',
'src/utils/mediaStatusCache.ts',
'src/utils/searchStream.ts',
'src/views/site/SiteCardListView.vue',
'src/views/reorganize/DownloadingListView.vue',
'src/utils/siteIconCache.ts',
],
provider: 'v8',
@@ -622,6 +624,12 @@ export default defineConfig(({ command, mode, isPreview }) => ({
lines: 90,
statements: 90,
},
'src/components/cards/DownloadingCard.vue': {
branches: 85,
functions: 90,
lines: 90,
statements: 90,
},
'src/utils/siteIconCache.ts': {
branches: 85,
functions: 90,
@@ -652,6 +660,12 @@ export default defineConfig(({ command, mode, isPreview }) => ({
lines: 90,
statements: 90,
},
'src/views/reorganize/DownloadingListView.vue': {
branches: 85,
functions: 90,
lines: 90,
statements: 90,
},
'src/views/subscribe/FullCalendarView.vue': {
branches: 85,
functions: 90,