mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 15:36:49 +08:00
feat: propagate media source identities (#6129)
This commit is contained in:
@@ -166,12 +166,17 @@ function getExistsStatusKey() {
|
||||
}
|
||||
|
||||
function isSameSubscribeMedia(subscribe: Subscribe) {
|
||||
const mediaId = getMediaId()
|
||||
if (subscribe.media_source && subscribe.media_id) {
|
||||
const prefix = subscribe.media_source === 'themoviedb' ? 'tmdb' : subscribe.media_source
|
||||
return mediaId === `${prefix}:${subscribe.media_id}`
|
||||
}
|
||||
if (subscribe.mediaid) return mediaId === subscribe.mediaid
|
||||
if (props.media?.tmdb_id && subscribe.tmdbid) return props.media.tmdb_id === subscribe.tmdbid
|
||||
if (props.media?.douban_id && subscribe.doubanid) return props.media.douban_id === subscribe.doubanid
|
||||
if (props.media?.bangumi_id && subscribe.bangumiid) return props.media.bangumi_id === subscribe.bangumiid
|
||||
|
||||
const mediaId = props.media?.media_id ? `${props.media.mediaid_prefix}:${props.media.media_id}` : ''
|
||||
return Boolean(mediaId && subscribe.mediaid === mediaId)
|
||||
if (props.media?.anilist_id && subscribe.anilistid) return props.media.anilist_id === subscribe.anilistid
|
||||
return false
|
||||
}
|
||||
|
||||
// 角标颜色
|
||||
|
||||
@@ -264,9 +264,14 @@ async function editSubscribeDialog() {
|
||||
|
||||
// 获得mediaid
|
||||
function getMediaId() {
|
||||
if (props.media?.media_source && props.media?.media_id) {
|
||||
const prefix = props.media.media_source === 'themoviedb' ? 'tmdb' : props.media.media_source
|
||||
return `${prefix}:${props.media.media_id}`
|
||||
}
|
||||
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}`
|
||||
else if (props.media?.anilistid) return `anilist:${props.media?.anilistid}`
|
||||
else return props.media?.mediaid
|
||||
}
|
||||
|
||||
@@ -480,13 +485,7 @@ function handleCardClick() {
|
||||
|
||||
<template v-if="display.xs.value">
|
||||
<div class="subscribe-card-mobile-media">
|
||||
<VImg
|
||||
:src="backdropUrl || posterUrl"
|
||||
:aspect-ratio="2"
|
||||
cover
|
||||
position="top"
|
||||
@load="imageLoadHandler"
|
||||
>
|
||||
<VImg :src="backdropUrl || posterUrl" :aspect-ratio="2" cover position="top" @load="imageLoadHandler">
|
||||
<template #placeholder>
|
||||
<VSkeletonLoader class="h-full w-full" />
|
||||
</template>
|
||||
@@ -535,12 +534,7 @@ function handleCardClick() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<IconBtn
|
||||
v-if="!props.sortable"
|
||||
class="subscribe-card-mobile-menu"
|
||||
size="small"
|
||||
@click.stop
|
||||
>
|
||||
<IconBtn v-if="!props.sortable" class="subscribe-card-mobile-menu" size="small" @click.stop>
|
||||
<VIcon icon="mdi-dots-horizontal" size="20" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
@@ -576,112 +570,114 @@ function handleCardClick() {
|
||||
</template>
|
||||
|
||||
<div v-else>
|
||||
<VCardText class="flex items-center pt-3 pb-2">
|
||||
<div
|
||||
class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md relative"
|
||||
v-if="imageLoaded"
|
||||
:class="{ 'cursor-move': props.sortable && display.mdAndUp.value }"
|
||||
>
|
||||
<VImg :src="posterUrl" aspect-ratio="2/3" cover>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
|
||||
<div class="text-sm font-medium text-white sm:pt-1">{{ props.media?.year }}</div>
|
||||
<div class="mr-2 min-w-0 text-lg font-bold text-white text-ellipsis overflow-hidden line-clamp-2 ...">
|
||||
{{ props.media?.name }}
|
||||
{{ formatSeasonLabel(props.media?.season, t('media.specials')) }}
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText class="flex min-w-0 justify-space-between align-center flex-wrap px-3">
|
||||
<div class="flex min-w-0 max-w-full align-center">
|
||||
<VIcon
|
||||
v-if="props.media?.total_episode && props.sortable"
|
||||
icon="mdi-progress-download"
|
||||
size="small"
|
||||
color="white"
|
||||
class="me-1"
|
||||
/>
|
||||
<IconBtn
|
||||
v-else-if="props.media?.total_episode"
|
||||
size="small"
|
||||
v-bind="props"
|
||||
icon="mdi-progress-download"
|
||||
color="white"
|
||||
/>
|
||||
<!-- 守卫改用 total_episode:电视剧订阅可能不带 season 字段(旧数据或自定义来源),仍应展示集数进度 -->
|
||||
<div v-if="props.media?.total_episode" class="flex-shrink-0 text-subtitle-2 me-2 text-white">
|
||||
{{ subscribeProgressText }}
|
||||
<VTooltip v-if="subscribeProgressTooltip" activator="parent" location="top">
|
||||
{{ subscribeProgressTooltip }}
|
||||
</VTooltip>
|
||||
</div>
|
||||
<VIcon
|
||||
v-if="props.media?.username && props.sortable"
|
||||
icon="mdi-account"
|
||||
size="small"
|
||||
color="white"
|
||||
class="flex-shrink-0 me-1"
|
||||
/>
|
||||
<IconBtn
|
||||
v-else-if="props.media?.username"
|
||||
icon="mdi-account"
|
||||
size="small"
|
||||
color="white"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
<!-- 用户名过长时限制在卡片宽度内,并用省略号展示剩余内容 -->
|
||||
<span
|
||||
v-if="props.media?.username"
|
||||
class="min-w-0 truncate text-subtitle-2 text-white"
|
||||
:title="props.media?.username"
|
||||
<VCardText class="flex items-center pt-3 pb-2">
|
||||
<div
|
||||
class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md relative"
|
||||
v-if="imageLoaded"
|
||||
:class="{ 'cursor-move': props.sortable && display.mdAndUp.value }"
|
||||
>
|
||||
{{ props.media?.username }}
|
||||
</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
<!-- 右下角元数据:暂停 / 待定时替换"x 天前"为状态文案 -->
|
||||
<VCardText
|
||||
v-if="rightBottomStateDisplay"
|
||||
class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300 text-xs"
|
||||
>
|
||||
<VIcon :icon="rightBottomStateDisplay.icon" class="me-1" />
|
||||
{{ rightBottomStateDisplay.label }}
|
||||
</VCardText>
|
||||
<VCardText
|
||||
v-else-if="lastUpdateText"
|
||||
class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300 text-xs"
|
||||
>
|
||||
<VIcon icon="mdi-download" class="me-1" />
|
||||
{{ lastUpdateText }}
|
||||
</VCardText>
|
||||
<div class="w-full absolute bottom-0">
|
||||
<!--
|
||||
<VImg :src="posterUrl" aspect-ratio="2/3" cover>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
|
||||
<div class="text-sm font-medium text-white sm:pt-1">{{ props.media?.year }}</div>
|
||||
<div
|
||||
class="mr-2 min-w-0 text-lg font-bold text-white text-ellipsis overflow-hidden line-clamp-2 ..."
|
||||
>
|
||||
{{ props.media?.name }}
|
||||
{{ formatSeasonLabel(props.media?.season, t('media.specials')) }}
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText class="flex min-w-0 justify-space-between align-center flex-wrap px-3">
|
||||
<div class="flex min-w-0 max-w-full align-center">
|
||||
<VIcon
|
||||
v-if="props.media?.total_episode && props.sortable"
|
||||
icon="mdi-progress-download"
|
||||
size="small"
|
||||
color="white"
|
||||
class="me-1"
|
||||
/>
|
||||
<IconBtn
|
||||
v-else-if="props.media?.total_episode"
|
||||
size="small"
|
||||
v-bind="props"
|
||||
icon="mdi-progress-download"
|
||||
color="white"
|
||||
/>
|
||||
<!-- 守卫改用 total_episode:电视剧订阅可能不带 season 字段(旧数据或自定义来源),仍应展示集数进度 -->
|
||||
<div v-if="props.media?.total_episode" class="flex-shrink-0 text-subtitle-2 me-2 text-white">
|
||||
{{ subscribeProgressText }}
|
||||
<VTooltip v-if="subscribeProgressTooltip" activator="parent" location="top">
|
||||
{{ subscribeProgressTooltip }}
|
||||
</VTooltip>
|
||||
</div>
|
||||
<VIcon
|
||||
v-if="props.media?.username && props.sortable"
|
||||
icon="mdi-account"
|
||||
size="small"
|
||||
color="white"
|
||||
class="flex-shrink-0 me-1"
|
||||
/>
|
||||
<IconBtn
|
||||
v-else-if="props.media?.username"
|
||||
icon="mdi-account"
|
||||
size="small"
|
||||
color="white"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
<!-- 用户名过长时限制在卡片宽度内,并用省略号展示剩余内容 -->
|
||||
<span
|
||||
v-if="props.media?.username"
|
||||
class="min-w-0 truncate text-subtitle-2 text-white"
|
||||
:title="props.media?.username"
|
||||
>
|
||||
{{ props.media?.username }}
|
||||
</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
<!-- 右下角元数据:暂停 / 待定时替换"x 天前"为状态文案 -->
|
||||
<VCardText
|
||||
v-if="rightBottomStateDisplay"
|
||||
class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300 text-xs"
|
||||
>
|
||||
<VIcon :icon="rightBottomStateDisplay.icon" class="me-1" />
|
||||
{{ rightBottomStateDisplay.label }}
|
||||
</VCardText>
|
||||
<VCardText
|
||||
v-else-if="lastUpdateText"
|
||||
class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300 text-xs"
|
||||
>
|
||||
<VIcon icon="mdi-download" class="me-1" />
|
||||
{{ lastUpdateText }}
|
||||
</VCardText>
|
||||
<div class="w-full absolute bottom-0">
|
||||
<!--
|
||||
分集洗版模式:底色保持深绿、buffer 段显示"已下载未洗版"为浅绿、model 段显示"已洗版完成"为亮绿,
|
||||
形成两段语义;其余订阅维持原有单段进度条
|
||||
-->
|
||||
<VProgressLinear
|
||||
v-if="isBestVersion && getBufferPercentage() > 0"
|
||||
:model-value="getPercentage()"
|
||||
:buffer-value="getBufferPercentage()"
|
||||
bg-color="success"
|
||||
bg-opacity="0.25"
|
||||
color="success"
|
||||
buffer-color="success"
|
||||
buffer-opacity="0.55"
|
||||
/>
|
||||
<VProgressLinear
|
||||
v-else-if="getPercentage() > 0"
|
||||
:model-value="getPercentage()"
|
||||
bg-color="success"
|
||||
color="success"
|
||||
/>
|
||||
</div>
|
||||
<VProgressLinear
|
||||
v-if="isBestVersion && getBufferPercentage() > 0"
|
||||
:model-value="getPercentage()"
|
||||
:buffer-value="getBufferPercentage()"
|
||||
bg-color="success"
|
||||
bg-opacity="0.25"
|
||||
color="success"
|
||||
buffer-color="success"
|
||||
buffer-opacity="0.55"
|
||||
/>
|
||||
<VProgressLinear
|
||||
v-else-if="getPercentage() > 0"
|
||||
:model-value="getPercentage()"
|
||||
bg-color="success"
|
||||
color="success"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
|
||||
@@ -47,9 +47,14 @@ const posterUrl = computed(() => {
|
||||
|
||||
// 获得mediaid
|
||||
function getMediaId() {
|
||||
if (props.media?.media_source && props.media?.media_id) {
|
||||
const prefix = props.media.media_source === 'themoviedb' ? 'tmdb' : props.media.media_source
|
||||
return `${prefix}:${props.media.media_id}`
|
||||
}
|
||||
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}`
|
||||
else if (props.media?.anilistid) return `anilist:${props.media?.anilistid}`
|
||||
}
|
||||
|
||||
// 查看媒体详情
|
||||
@@ -102,61 +107,65 @@ function doDelete() {
|
||||
'app-hover-lift-card--hovering': hover.isHovering,
|
||||
}"
|
||||
>
|
||||
<VCard
|
||||
:key="props.media?.id"
|
||||
class="app-hover-lift-card flex flex-col h-full"
|
||||
min-height="150"
|
||||
@click="showForkSubscribe"
|
||||
>
|
||||
<template #image>
|
||||
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||
<VCard
|
||||
:key="props.media?.id"
|
||||
class="app-hover-lift-card flex flex-col h-full"
|
||||
min-height="150"
|
||||
@click="showForkSubscribe"
|
||||
>
|
||||
<template #image>
|
||||
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="absolute inset-0 subscribe-card-background"></div>
|
||||
</template>
|
||||
</VImg>
|
||||
</template>
|
||||
<div class="h-full flex flex-col">
|
||||
<VCardText class="flex items-center pa-3 pb-1 grow">
|
||||
<div class="h-auto w-16 flex-shrink-0 overflow-hidden rounded-md" v-if="imageLoaded">
|
||||
<VImg :src="posterUrl" aspect-ratio="2/3" cover @click.stop="viewMediaDetail">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="absolute inset-0 subscribe-card-background"></div>
|
||||
</template>
|
||||
</VImg>
|
||||
</template>
|
||||
<div class="h-full flex flex-col">
|
||||
<VCardText class="flex items-center pa-3 pb-1 grow">
|
||||
<div class="h-auto w-16 flex-shrink-0 overflow-hidden rounded-md" v-if="imageLoaded">
|
||||
<VImg :src="posterUrl" aspect-ratio="2/3" cover @click.stop="viewMediaDetail">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center pl-2 xl:pl-4">
|
||||
<div class="mr-2 min-w-0 text-lg font-bold text-white line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.share_title }}
|
||||
<div class="flex flex-col justify-center pl-2 xl:pl-4">
|
||||
<div
|
||||
class="mr-2 min-w-0 text-lg font-bold text-white line-clamp-2 overflow-hidden text-ellipsis ..."
|
||||
>
|
||||
{{ props.media?.share_title }}
|
||||
</div>
|
||||
<div
|
||||
class="text-sm font-medium text-gray-200 sm:pt-1 line-clamp-3 overflow-hidden text-ellipsis ..."
|
||||
>
|
||||
{{ props.media?.share_comment }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm font-medium text-gray-200 sm:pt-1 line-clamp-3 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.share_comment }}
|
||||
</VCardText>
|
||||
<VCardText class="flex justify-space-between align-center flex-wrap py-2">
|
||||
<div class="flex align-center">
|
||||
<IconBtn v-bind="props" icon="mdi-account" color="white" class="me-1" />
|
||||
<div class="text-subtitle-2 me-4 text-white">
|
||||
{{ props.media?.share_user }}
|
||||
</div>
|
||||
<IconBtn v-if="props.media?.count" icon="mdi-fire" color="white" class="me-1" />
|
||||
<span v-if="props.media?.count" class="text-subtitle-2 me-4 text-white">
|
||||
{{ props.media?.count.toLocaleString() }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText class="flex justify-space-between align-center flex-wrap py-2">
|
||||
<div class="flex align-center">
|
||||
<IconBtn v-bind="props" icon="mdi-account" color="white" class="me-1" />
|
||||
<div class="text-subtitle-2 me-4 text-white">
|
||||
{{ props.media?.share_user }}
|
||||
</div>
|
||||
<IconBtn v-if="props.media?.count" icon="mdi-fire" color="white" class="me-1" />
|
||||
<span v-if="props.media?.count" class="text-subtitle-2 me-4 text-white">
|
||||
{{ props.media?.count.toLocaleString() }}
|
||||
</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
|
||||
<VIcon icon="mdi-calendar" class="me-1" />
|
||||
{{ dateText }}
|
||||
</VCardText>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
|
||||
<VIcon icon="mdi-calendar" class="me-1" />
|
||||
{{ dateText }}
|
||||
</VCardText>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -164,7 +164,16 @@ describe('SubscribeCard display and progress', () => {
|
||||
['disabled flag', false, true, 3, '电视剧', 80, false, false],
|
||||
])(
|
||||
'normalizes %s for wash progress and badges',
|
||||
async (_case, bestVersion, bestVersionFull, completedEpisode, type, expectedProgress, expectedWash, expectedFull) => {
|
||||
async (
|
||||
_case,
|
||||
bestVersion,
|
||||
bestVersionFull,
|
||||
completedEpisode,
|
||||
type,
|
||||
expectedProgress,
|
||||
expectedWash,
|
||||
expectedFull,
|
||||
) => {
|
||||
const { container } = await renderCard({
|
||||
best_version: bestVersion,
|
||||
best_version_full: bestVersionFull,
|
||||
@@ -291,10 +300,20 @@ describe('SubscribeCard interaction boundaries', () => {
|
||||
})
|
||||
|
||||
it.each([
|
||||
['TMDB before all fallbacks', { bangumiid: '33', doubanid: '22', mediaid: 'custom:44', tmdbid: 11 }, 'tmdb:11'],
|
||||
['Douban before Bangumi', { bangumiid: '33', doubanid: '22', mediaid: 'custom:44', tmdbid: 0 }, 'douban:22'],
|
||||
['Bangumi before custom', { bangumiid: '33', doubanid: undefined, mediaid: 'custom:44', tmdbid: 0 }, 'bangumi:33'],
|
||||
['custom media ID last', { bangumiid: undefined, doubanid: undefined, mediaid: 'custom:44', tmdbid: 0 }, 'custom:44'],
|
||||
['TMDB before all fallbacks', { bangumiid: 33, doubanid: '22', mediaid: 'custom:44', tmdbid: 11 }, 'tmdb:11'],
|
||||
['Douban before Bangumi', { bangumiid: 33, doubanid: '22', mediaid: 'custom:44', tmdbid: 0 }, 'douban:22'],
|
||||
['Bangumi before custom', { bangumiid: 33, doubanid: undefined, mediaid: 'custom:44', tmdbid: 0 }, 'bangumi:33'],
|
||||
[
|
||||
'AniList before legacy custom',
|
||||
{ anilistid: 55, bangumiid: undefined, mediaid: 'custom:44', tmdbid: 0 },
|
||||
'anilist:55',
|
||||
],
|
||||
['selected primary identity', { media_id: '66', media_source: 'anilist', tmdbid: 11 }, 'anilist:66'],
|
||||
[
|
||||
'custom media ID last',
|
||||
{ bangumiid: undefined, doubanid: undefined, mediaid: 'custom:44', tmdbid: 0 },
|
||||
'custom:44',
|
||||
],
|
||||
])('routes media details with %s', async (_case, identifiers, expectedMediaId) => {
|
||||
const { container, media } = await renderCard(identifiers)
|
||||
|
||||
@@ -364,30 +383,34 @@ describe('SubscribeCard item operations', () => {
|
||||
['confirmation cancellation', false, 200, { success: true }, null],
|
||||
['business failure', true, 200, { message: 'rejected', success: false }, '暂停失败:rejected'],
|
||||
['HTTP failure', true, 500, { message: 'server down', success: false }, '请求失败,请稍后重试'],
|
||||
] as const)(
|
||||
'keeps status unchanged after %s',
|
||||
async (_case, confirmed, status, response, expectedError) => {
|
||||
const requested = vi.fn()
|
||||
mocks.confirm.mockResolvedValue(confirmed)
|
||||
const { container, emitted, media } = await renderCard({ state: 'R' })
|
||||
server.use(updateSubscribeStatusHandler(media.id, response, status, requested))
|
||||
] as const)('keeps status unchanged after %s', async (_case, confirmed, status, response, expectedError) => {
|
||||
const requested = vi.fn()
|
||||
mocks.confirm.mockResolvedValue(confirmed)
|
||||
const { container, emitted, media } = await renderCard({ state: 'R' })
|
||||
server.use(updateSubscribeStatusHandler(media.id, response, status, requested))
|
||||
|
||||
await chooseMenuItem(container, '暂停')
|
||||
await waitFor(() => expect(mocks.confirm).toHaveBeenCalledOnce())
|
||||
await chooseMenuItem(container, '暂停')
|
||||
await waitFor(() => expect(mocks.confirm).toHaveBeenCalledOnce())
|
||||
|
||||
if (confirmed) await waitFor(() => expect(requested).toHaveBeenCalledOnce())
|
||||
else expect(requested).not.toHaveBeenCalled()
|
||||
if (expectedError) await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith(expectedError))
|
||||
else expect(mocks.toastError).not.toHaveBeenCalled()
|
||||
expect(container.querySelector('.subscribe-card')).not.toHaveClass('subscribe-card-paused')
|
||||
expect(emitted('save') ?? []).toHaveLength(0)
|
||||
},
|
||||
)
|
||||
if (confirmed) await waitFor(() => expect(requested).toHaveBeenCalledOnce())
|
||||
else expect(requested).not.toHaveBeenCalled()
|
||||
if (expectedError) await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith(expectedError))
|
||||
else expect(mocks.toastError).not.toHaveBeenCalled()
|
||||
expect(container.querySelector('.subscribe-card')).not.toHaveClass('subscribe-card-paused')
|
||||
expect(emitted('save') ?? []).toHaveLength(0)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['success', true, 200, { success: true }, 'success', '卡片测试媒体 重置成功!'],
|
||||
['confirmation cancellation', false, 200, { success: true }, null, null],
|
||||
['business failure', true, 200, { message: 'rejected', success: false }, 'error', '卡片测试媒体 重置失败:rejected'],
|
||||
[
|
||||
'business failure',
|
||||
true,
|
||||
200,
|
||||
{ message: 'rejected', success: false },
|
||||
'error',
|
||||
'卡片测试媒体 重置失败:rejected',
|
||||
],
|
||||
['HTTP failure', true, 500, { message: 'server down', success: false }, 'error', '请求失败,请稍后重试'],
|
||||
] as const)(
|
||||
'handles reset %s without speculative state',
|
||||
@@ -423,16 +446,19 @@ describe('SubscribeCard item operations', () => {
|
||||
it.each([
|
||||
['success', 200, { success: true }, true, null],
|
||||
['HTTP failure', 500, { message: 'server down', success: false }, false, '请求失败,请稍后重试'],
|
||||
] as const)('handles delete %s without a synthetic business-failure branch', async (_case, status, response, removed, error) => {
|
||||
const requested = vi.fn()
|
||||
const { container, emitted, media } = await renderCard()
|
||||
server.use(deleteSubscribeByIdHandler(media.id, response, status, requested))
|
||||
] as const)(
|
||||
'handles delete %s without a synthetic business-failure branch',
|
||||
async (_case, status, response, removed, error) => {
|
||||
const requested = vi.fn()
|
||||
const { container, emitted, media } = await renderCard()
|
||||
server.use(deleteSubscribeByIdHandler(media.id, response, status, requested))
|
||||
|
||||
await chooseMenuItem(container, '取消订阅')
|
||||
await chooseMenuItem(container, '取消订阅')
|
||||
|
||||
await waitFor(() => expect(requested).toHaveBeenCalledOnce())
|
||||
expect(emitted('remove') ?? []).toHaveLength(removed ? 1 : 0)
|
||||
if (error) await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith(error))
|
||||
else expect(mocks.toastError).not.toHaveBeenCalled()
|
||||
})
|
||||
await waitFor(() => expect(requested).toHaveBeenCalledOnce())
|
||||
expect(emitted('remove') ?? []).toHaveLength(removed ? 1 : 0)
|
||||
if (error) await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith(error))
|
||||
else expect(mocks.toastError).not.toHaveBeenCalled()
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -127,6 +127,7 @@ describe('SubscribeShareCard', () => {
|
||||
['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'],
|
||||
['AniList without other IDs', { anilistid: 4404, bangumiid: undefined, tmdbid: undefined }, 'anilist:4404'],
|
||||
] 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)
|
||||
|
||||
Reference in New Issue
Block a user