mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-04 23:18:44 +08:00
test(media): cover cards and virtual slides (#558)
This commit is contained in:
@@ -0,0 +1,539 @@
|
||||
import type { MediaInfo } from '@/api/types'
|
||||
import MediaCard from '@/components/cards/MediaCard.vue'
|
||||
import { clearCachedMediaSubscribeStatuses } from '@/utils/mediaStatusCache'
|
||||
import { fireEvent, waitFor } from '@testing-library/vue'
|
||||
import { createMediaInfo } from '@tests/support/factories/media'
|
||||
import { mediaExistsHandler } from '@tests/support/msw/handlers/media'
|
||||
import { querySubscribeByMediaHandler, subscribeListHandler } from '@tests/support/msw/handlers/subscribe'
|
||||
import { server } from '@tests/support/msw/server'
|
||||
import { renderWithProviders } from '@tests/support/render'
|
||||
import { HttpResponse, http } from 'msw'
|
||||
import { defineComponent, h } from 'vue'
|
||||
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),
|
||||
},
|
||||
}))
|
||||
|
||||
const API_BASE_URL = 'http://localhost/api/v1/'
|
||||
const siteListUrl = new URL('site/', API_BASE_URL).href
|
||||
const selectedSitesUrl = new URL('system/setting/public/IndexerSites', API_BASE_URL).href
|
||||
|
||||
let intersectionObservers: IntersectionObserverMock[] = []
|
||||
|
||||
class IntersectionObserverMock implements IntersectionObserver {
|
||||
readonly root: Element | Document | null
|
||||
readonly rootMargin: string
|
||||
readonly thresholds: readonly number[]
|
||||
readonly disconnect = vi.fn()
|
||||
readonly observe = vi.fn((target: Element) => {
|
||||
this.target = target
|
||||
})
|
||||
readonly unobserve = vi.fn()
|
||||
private target: Element = document.body
|
||||
|
||||
constructor(
|
||||
private readonly callback: IntersectionObserverCallback,
|
||||
options: IntersectionObserverInit = {},
|
||||
) {
|
||||
this.root = options.root ?? null
|
||||
this.rootMargin = options.rootMargin ?? '0px'
|
||||
this.thresholds = Array.isArray(options.threshold) ? options.threshold : [options.threshold ?? 0]
|
||||
intersectionObservers.push(this)
|
||||
}
|
||||
|
||||
takeRecords(): IntersectionObserverEntry[] {
|
||||
return []
|
||||
}
|
||||
|
||||
trigger(isIntersecting = true) {
|
||||
const bounds = this.target.getBoundingClientRect()
|
||||
this.callback(
|
||||
[
|
||||
{
|
||||
boundingClientRect: bounds,
|
||||
intersectionRatio: isIntersecting ? 1 : 0,
|
||||
intersectionRect: bounds,
|
||||
isIntersecting,
|
||||
rootBounds: null,
|
||||
target: this.target,
|
||||
time: 0,
|
||||
},
|
||||
],
|
||||
this,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface RenderCardOptions {
|
||||
permissions?: Record<string, boolean>
|
||||
superUser?: boolean
|
||||
}
|
||||
|
||||
async function renderCard(media: MediaInfo, options: RenderCardOptions = {}) {
|
||||
return renderWithProviders(MediaCard, {
|
||||
props: {
|
||||
media,
|
||||
width: '9rem',
|
||||
},
|
||||
initialState: {
|
||||
user: {
|
||||
permissions: options.permissions ?? {
|
||||
discovery: true,
|
||||
manage: false,
|
||||
search: true,
|
||||
subscribe: true,
|
||||
},
|
||||
superUser: options.superUser ?? true,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function getCard(container: Element) {
|
||||
const card = container.querySelector<HTMLElement>('.media-card')
|
||||
expect(card).not.toBeNull()
|
||||
return card as HTMLElement
|
||||
}
|
||||
|
||||
function getHoverArea(container: Element) {
|
||||
const area = container.querySelector<HTMLElement>('.media-card-hover-area')
|
||||
expect(area).not.toBeNull()
|
||||
return area as HTMLElement
|
||||
}
|
||||
|
||||
function getActionButtons(container: Element) {
|
||||
return [...container.querySelectorAll<HTMLButtonElement>('.media-card .v-card-text button')]
|
||||
}
|
||||
|
||||
function getSearchButton(container: Element) {
|
||||
const button = getActionButtons(container)[0]
|
||||
expect(button).toBeDefined()
|
||||
return button
|
||||
}
|
||||
|
||||
function getStatusObservers() {
|
||||
return intersectionObservers.filter(observer => observer.thresholds.includes(0.1))
|
||||
}
|
||||
|
||||
function installSearchHandlers(sites: Record<string, unknown>[], selected: number[]) {
|
||||
server.use(
|
||||
http.get(siteListUrl, () => HttpResponse.json(sites)),
|
||||
http.get(selectedSitesUrl, () => HttpResponse.json({ data: { value: selected }, success: true })),
|
||||
)
|
||||
}
|
||||
|
||||
describe('MediaCard', () => {
|
||||
beforeEach(() => {
|
||||
intersectionObservers = []
|
||||
clearCachedMediaSubscribeStatuses()
|
||||
vi.stubGlobal('IntersectionObserver', IntersectionObserverMock)
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
mocks.openSharedDialog.mockReturnValue({ close: vi.fn(), id: 1, updateProps: vi.fn() })
|
||||
})
|
||||
|
||||
it('loads exact subscribe and exists status only after entering the viewport', async () => {
|
||||
const media = createMediaInfo({ season: 2, title: '视口状态剧集', tmdb_id: 9101, type: '电视剧' })
|
||||
const subscribeRequest = vi.fn<(url: URL) => void>()
|
||||
const existsRequest = vi.fn<(url: URL) => void>()
|
||||
server.use(
|
||||
querySubscribeByMediaHandler('tmdb:9101', { id: 71, season: 2 }, 200, subscribeRequest),
|
||||
mediaExistsHandler({ data: { item: { id: 'library-item' } }, success: true }, 200, existsRequest),
|
||||
)
|
||||
|
||||
const { container } = await renderCard(media)
|
||||
expect(subscribeRequest).not.toHaveBeenCalled()
|
||||
expect(existsRequest).not.toHaveBeenCalled()
|
||||
|
||||
getStatusObservers()[0]?.trigger()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(subscribeRequest).toHaveBeenCalledOnce()
|
||||
expect(existsRequest).toHaveBeenCalledOnce()
|
||||
})
|
||||
expect(subscribeRequest.mock.calls[0][0].searchParams.get('season')).toBe('2')
|
||||
expect(subscribeRequest.mock.calls[0][0].searchParams.get('title')).toBe('视口状态剧集')
|
||||
expect(Object.fromEntries(existsRequest.mock.calls[0][0].searchParams)).toEqual({
|
||||
mtype: '电视剧',
|
||||
season: '2',
|
||||
title: '视口状态剧集',
|
||||
tmdbid: '9101',
|
||||
year: '2026',
|
||||
})
|
||||
await waitFor(() => expect(getActionButtons(container).at(-1)).toHaveClass('text-error'))
|
||||
expect(getStatusObservers()[0]?.disconnect).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('coalesces status requests for duplicate cards while updating both card states', async () => {
|
||||
const media = createMediaInfo({ title: '重复媒体', tmdb_id: 9102 })
|
||||
const subscribeRequest = vi.fn<(url: URL) => void>()
|
||||
const existsRequest = vi.fn<(url: URL) => void>()
|
||||
server.use(
|
||||
querySubscribeByMediaHandler('tmdb:9102', { id: 72 }, 200, subscribeRequest),
|
||||
mediaExistsHandler({ data: { item: {} }, success: false }, 200, existsRequest),
|
||||
)
|
||||
|
||||
const Harness = {
|
||||
components: { MediaCard },
|
||||
data: () => ({ media }),
|
||||
template: '<div><MediaCard :media="media" width="9rem" /><MediaCard :media="media" width="9rem" /></div>',
|
||||
}
|
||||
const { container } = await renderWithProviders(Harness, {
|
||||
initialState: { user: { superUser: true } },
|
||||
})
|
||||
|
||||
expect(getStatusObservers()).toHaveLength(2)
|
||||
getStatusObservers().forEach(observer => observer.trigger())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(subscribeRequest).toHaveBeenCalledOnce()
|
||||
expect(existsRequest).toHaveBeenCalledOnce()
|
||||
expect(container.querySelectorAll('.media-card .v-card-text button.text-error')).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
['TMDB', createMediaInfo({ season: 3, tmdb_id: 9201, type: '电视剧' }), 'tmdb:9201', '3'],
|
||||
[
|
||||
'Douban',
|
||||
createMediaInfo({ douban_id: 'db-9202', season: undefined, tmdb_id: undefined }),
|
||||
'douban:db-9202',
|
||||
null,
|
||||
],
|
||||
[
|
||||
'Bangumi',
|
||||
createMediaInfo({ bangumi_id: '9203', season: 1, tmdb_id: undefined, type: '电视剧' }),
|
||||
'bangumi:9203',
|
||||
'1',
|
||||
],
|
||||
[
|
||||
'extension',
|
||||
createMediaInfo({ media_id: 'item-9204', mediaid_prefix: 'custom', tmdb_id: undefined }),
|
||||
'custom:item-9204',
|
||||
null,
|
||||
],
|
||||
])('queries the current %s media identifier and season', async (_source, media, mediaId, season) => {
|
||||
const subscribeRequest = vi.fn<(url: URL) => void>()
|
||||
server.use(
|
||||
querySubscribeByMediaHandler(mediaId, {}, 200, subscribeRequest),
|
||||
mediaExistsHandler({ data: { item: {} }, success: false }),
|
||||
)
|
||||
|
||||
await renderCard(media)
|
||||
getStatusObservers()[0]?.trigger()
|
||||
|
||||
await waitFor(() => expect(subscribeRequest).toHaveBeenCalledOnce())
|
||||
expect(subscribeRequest.mock.calls[0][0].searchParams.get('season')).toBe(season)
|
||||
})
|
||||
|
||||
it('skips status requests for collections and releases observer and touch listeners on unmount', async () => {
|
||||
vi.spyOn(window, 'matchMedia').mockReturnValue({
|
||||
...window.matchMedia(''),
|
||||
matches: true,
|
||||
})
|
||||
const subscribeRequest = vi.fn<(url: URL) => void>()
|
||||
const existsRequest = vi.fn<(url: URL) => void>()
|
||||
const addListener = vi.spyOn(document, 'addEventListener')
|
||||
const removeListener = vi.spyOn(document, 'removeEventListener')
|
||||
server.use(
|
||||
querySubscribeByMediaHandler('tmdb:9301', {}, 200, subscribeRequest),
|
||||
mediaExistsHandler({ data: { item: {} }, success: false }, 200, existsRequest),
|
||||
)
|
||||
|
||||
const { unmount } = await renderCard(createMediaInfo({ collection_id: 44, tmdb_id: 9301 }))
|
||||
getStatusObservers()[0]?.trigger()
|
||||
await Promise.resolve()
|
||||
|
||||
expect(subscribeRequest).not.toHaveBeenCalled()
|
||||
expect(existsRequest).not.toHaveBeenCalled()
|
||||
expect(addListener).toHaveBeenCalledWith('pointerdown', expect.any(Function))
|
||||
|
||||
unmount()
|
||||
|
||||
expect(getStatusObservers()[0]?.disconnect).toHaveBeenCalledOnce()
|
||||
expect(removeListener).toHaveBeenCalledWith('pointerdown', expect.any(Function))
|
||||
})
|
||||
|
||||
it.each([
|
||||
[
|
||||
'media details',
|
||||
createMediaInfo({ title: '详情电影', tmdb_id: 9401 }),
|
||||
'/media',
|
||||
{ mediaid: 'tmdb:9401', title: '详情电影', type: '电影', year: '2026' },
|
||||
],
|
||||
[
|
||||
'collection browse',
|
||||
createMediaInfo({ collection_id: 88, title: '合集入口', tmdb_id: 9402 }),
|
||||
'/browse/tmdb/collection/88',
|
||||
{ title: '合集入口' },
|
||||
],
|
||||
])('opens %s from the desktop hover state', async (_case, media, path, query) => {
|
||||
const { container } = await renderCard(media)
|
||||
|
||||
await fireEvent.mouseEnter(getHoverArea(container))
|
||||
await waitFor(() => expect(getCard(container)).toHaveClass('app-hover-lift-card--hovering'))
|
||||
await fireEvent.click(getCard(container))
|
||||
|
||||
await waitFor(() => expect(mocks.routerPush).toHaveBeenCalledWith({ path, query }))
|
||||
})
|
||||
|
||||
it('routes directly to resource search when no active sites are available', async () => {
|
||||
installSearchHandlers([], [3, 5])
|
||||
const media = createMediaInfo({ season: 4, title: '直接搜索剧集', tmdb_id: 9501, type: '电视剧' })
|
||||
const { container } = await renderCard(media)
|
||||
|
||||
await fireEvent.mouseEnter(getHoverArea(container))
|
||||
await fireEvent.click(getSearchButton(container))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mocks.routerPush).toHaveBeenCalledWith({
|
||||
path: '/resource',
|
||||
query: {
|
||||
area: 'title',
|
||||
keyword: 'tmdb:9501',
|
||||
season: 4,
|
||||
sites: '3,5',
|
||||
title: '直接搜索剧集',
|
||||
type: '电视剧',
|
||||
year: '2026',
|
||||
},
|
||||
}),
|
||||
)
|
||||
expect(mocks.openSharedDialog).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('falls back to global search when site settings cannot provide active selections', async () => {
|
||||
server.use(
|
||||
http.get(siteListUrl, () => HttpResponse.json({ message: 'temporary failure' }, { status: 500 })),
|
||||
http.get(selectedSitesUrl, () => HttpResponse.json({ success: true })),
|
||||
)
|
||||
const media = createMediaInfo({ title: '站点失败搜索', tmdb_id: 9503 })
|
||||
const { container } = await renderCard(media)
|
||||
|
||||
await fireEvent.mouseEnter(getHoverArea(container))
|
||||
await fireEvent.click(getSearchButton(container))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mocks.routerPush).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ path: '/resource', query: expect.objectContaining({ sites: '' }) }),
|
||||
),
|
||||
)
|
||||
expect(mocks.openSharedDialog).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('opens site selection and routes with the selected sites when active sites exist', async () => {
|
||||
installSearchHandlers(
|
||||
[
|
||||
{
|
||||
domain: 'tracker.example',
|
||||
downloader: 'default',
|
||||
id: 7,
|
||||
is_active: true,
|
||||
name: '测试站点',
|
||||
url: 'https://tracker.example',
|
||||
},
|
||||
],
|
||||
[7],
|
||||
)
|
||||
const media = createMediaInfo({ title: '多站搜索电影', tmdb_id: 9502 })
|
||||
const { container } = await renderCard(media)
|
||||
|
||||
await fireEvent.mouseEnter(getHoverArea(container))
|
||||
await fireEvent.click(getSearchButton(container))
|
||||
await waitFor(() => expect(mocks.openSharedDialog).toHaveBeenCalledOnce())
|
||||
|
||||
const [, dialogProps, dialogEvents] = mocks.openSharedDialog.mock.calls[0] as [
|
||||
unknown,
|
||||
{ selected: number[]; sites: Array<{ id: number }> },
|
||||
{ search: (sites: number[]) => void },
|
||||
]
|
||||
expect(dialogProps.selected).toEqual([7])
|
||||
expect(dialogProps.sites.map(site => site.id)).toEqual([7])
|
||||
dialogEvents.search([7, 9])
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mocks.routerPush).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
path: '/resource',
|
||||
query: expect.objectContaining({ sites: '7,9' }),
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
it('opens active sites with an empty selection when the saved setting fails', async () => {
|
||||
server.use(
|
||||
http.get(siteListUrl, () =>
|
||||
HttpResponse.json([
|
||||
{
|
||||
domain: 'fallback.example',
|
||||
downloader: 'default',
|
||||
id: 8,
|
||||
is_active: true,
|
||||
name: '备用站点',
|
||||
url: 'https://fallback.example',
|
||||
},
|
||||
]),
|
||||
),
|
||||
http.get(selectedSitesUrl, () => HttpResponse.json({ message: 'temporary failure' }, { status: 500 })),
|
||||
)
|
||||
const { container } = await renderCard(createMediaInfo({ tmdb_id: 9504 }))
|
||||
|
||||
await fireEvent.mouseEnter(getHoverArea(container))
|
||||
await fireEvent.click(getSearchButton(container))
|
||||
|
||||
await waitFor(() => expect(mocks.openSharedDialog).toHaveBeenCalledOnce())
|
||||
const [, dialogProps] = mocks.openSharedDialog.mock.calls[0] as [unknown, { selected: number[] }]
|
||||
expect(dialogProps.selected).toEqual([])
|
||||
})
|
||||
|
||||
it('loads matching TV seasons before opening the subscription dialog', async () => {
|
||||
const media = createMediaInfo({ season: 2, title: '多季剧集', tmdb_id: 9551, type: '电视剧' })
|
||||
server.use(
|
||||
querySubscribeByMediaHandler('tmdb:9551', { id: 81, season: 2 }),
|
||||
mediaExistsHandler({ data: { item: {} }, success: false }),
|
||||
subscribeListHandler([
|
||||
{ best_version: 0, id: 81, season: 3, tmdbid: 9551, type: '电视剧' },
|
||||
{ best_version: 1, best_version_full: 1, id: 82, season: 1, tmdbid: 9551, type: '电视剧' },
|
||||
{ id: 83, season: 4, tmdbid: 9999, type: '电视剧' },
|
||||
{ id: 84, tmdbid: 9551, type: '电影' },
|
||||
]),
|
||||
http.get(new URL('system/setting/public/DefaultTvSubscribeConfig', API_BASE_URL).href, () =>
|
||||
HttpResponse.json({ data: { value: { best_version: 0 } }, success: true }),
|
||||
),
|
||||
)
|
||||
const { container } = await renderCard(media)
|
||||
getStatusObservers()[0]?.trigger()
|
||||
await waitFor(() => expect(getActionButtons(container).at(-1)).toHaveClass('text-error'))
|
||||
|
||||
await fireEvent.mouseEnter(getHoverArea(container))
|
||||
await fireEvent.click(getActionButtons(container).at(-1) as HTMLButtonElement)
|
||||
|
||||
await waitFor(() => expect(mocks.openSharedDialog).toHaveBeenCalledOnce())
|
||||
const [, dialogProps] = mocks.openSharedDialog.mock.calls[0] as [unknown, Record<string, unknown>]
|
||||
expect(dialogProps).toMatchObject({
|
||||
selectedSeason: undefined,
|
||||
subscribedSeasonModes: { 1: 'best_version_full', 3: 'normal' },
|
||||
subscribedSeasons: [1, 3],
|
||||
})
|
||||
})
|
||||
|
||||
it('matches custom media IDs when collecting subscribed TV seasons', async () => {
|
||||
const media = createMediaInfo({
|
||||
media_id: 'series-9553',
|
||||
mediaid_prefix: 'custom',
|
||||
season: 2,
|
||||
tmdb_id: undefined,
|
||||
type: '电视剧',
|
||||
})
|
||||
server.use(
|
||||
querySubscribeByMediaHandler('custom:series-9553', { id: 91, season: 2 }),
|
||||
mediaExistsHandler({ data: { item: {} }, success: false }),
|
||||
subscribeListHandler([
|
||||
{ id: 91, mediaid: 'custom:series-9553', season: 2, type: '电视剧' },
|
||||
{ id: 92, mediaid: 'custom:other', season: 5, type: '电视剧' },
|
||||
]),
|
||||
http.get(new URL('system/setting/public/DefaultTvSubscribeConfig', API_BASE_URL).href, () =>
|
||||
HttpResponse.json({ data: { value: {} }, success: true }),
|
||||
),
|
||||
)
|
||||
const { container } = await renderCard(media)
|
||||
getStatusObservers()[0]?.trigger()
|
||||
await waitFor(() => expect(getActionButtons(container).at(-1)).toHaveClass('text-error'))
|
||||
|
||||
await fireEvent.mouseEnter(getHoverArea(container))
|
||||
await fireEvent.click(getActionButtons(container).at(-1) as HTMLButtonElement)
|
||||
|
||||
await waitFor(() => expect(mocks.openSharedDialog).toHaveBeenCalledOnce())
|
||||
const [, dialogProps] = mocks.openSharedDialog.mock.calls[0] as [unknown, Record<string, unknown>]
|
||||
expect(dialogProps).toMatchObject({ subscribedSeasons: [2] })
|
||||
})
|
||||
|
||||
it('updates image badges on load and falls back after an image error', async () => {
|
||||
const media = createMediaInfo({
|
||||
poster_path: '/original/poster.jpg',
|
||||
source: 'themoviedb',
|
||||
tmdb_id: 9552,
|
||||
type: '电视剧',
|
||||
vote_average: 8.6,
|
||||
})
|
||||
const VImgStub = defineComponent({
|
||||
name: 'VImg',
|
||||
emits: ['error', 'load'],
|
||||
props: { src: String },
|
||||
setup(props, { emit, slots }) {
|
||||
return () =>
|
||||
h('div', { 'data-src': props.src }, [
|
||||
h('button', { 'aria-label': '图片加载成功', onClick: () => emit('load') }),
|
||||
h('button', { 'aria-label': '图片加载失败', onClick: () => emit('error') }),
|
||||
slots.default?.(),
|
||||
])
|
||||
},
|
||||
})
|
||||
const { container } = await renderWithProviders(MediaCard, {
|
||||
props: { media, width: '9rem' },
|
||||
initialState: { user: { superUser: true } },
|
||||
global: { stubs: { VImg: VImgStub } },
|
||||
})
|
||||
|
||||
await fireEvent.click(container.querySelector('[aria-label="图片加载成功"]') as HTMLElement)
|
||||
await waitFor(() => expect(container.querySelector('.media-card')).toHaveClass('ring-1'))
|
||||
expect(container).toHaveTextContent('TV')
|
||||
expect(container).toHaveTextContent('8.6')
|
||||
|
||||
await fireEvent.click(container.querySelector('[aria-label="图片加载失败"]') as HTMLElement)
|
||||
await waitFor(() =>
|
||||
expect(container.querySelector('.media-card-title')?.parentElement).not.toHaveStyle({ display: 'none' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('hides search and subscribe actions when the user lacks both permissions', async () => {
|
||||
const { container } = await renderCard(createMediaInfo({ tmdb_id: 9601 }), {
|
||||
permissions: {
|
||||
discovery: true,
|
||||
manage: false,
|
||||
search: false,
|
||||
subscribe: false,
|
||||
},
|
||||
superUser: false,
|
||||
})
|
||||
|
||||
await fireEvent.mouseEnter(getHoverArea(container))
|
||||
|
||||
expect(getActionButtons(container)).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('uses first tap to reveal details, second tap to route, and outside pointerdown to collapse', async () => {
|
||||
vi.spyOn(window, 'matchMedia').mockReturnValue({
|
||||
...window.matchMedia(''),
|
||||
matches: true,
|
||||
})
|
||||
const media = createMediaInfo({ title: '触摸卡片', tmdb_id: 9701 })
|
||||
const { container } = await renderCard(media)
|
||||
const detail = container.querySelector<HTMLElement>('.media-card-title')?.parentElement
|
||||
expect(detail).not.toBeNull()
|
||||
|
||||
await fireEvent.click(getCard(container))
|
||||
expect(detail).not.toHaveStyle({ display: 'none' })
|
||||
expect(mocks.routerPush).not.toHaveBeenCalled()
|
||||
|
||||
await fireEvent.pointerDown(document.body)
|
||||
expect(detail).toHaveStyle({ display: 'none' })
|
||||
|
||||
await fireEvent.click(getCard(container))
|
||||
await fireEvent.click(getCard(container))
|
||||
await waitFor(() => expect(mocks.routerPush).toHaveBeenCalledWith(expect.objectContaining({ path: '/media' })))
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user