mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-04 23:18:44 +08:00
test(site): cover site resource dialog (#563)
This commit is contained in:
@@ -380,11 +380,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/dialog/SiteResourceDialog.vue": {
|
||||
"@typescript-eslint/no-unused-vars": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/components/dialog/SiteStatisticsDialog.vue": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
|
||||
@@ -45,6 +45,11 @@ const resourcePage = ref(1)
|
||||
// 加载状态
|
||||
const resourceLoading = ref(false)
|
||||
|
||||
const resourceError = ref(false)
|
||||
|
||||
// 只有最后发起的搜索可以更新资源数据、请求状态和移动端搜索面板。
|
||||
let resourceRequestId = 0
|
||||
|
||||
// 移动端搜索栏是否展开
|
||||
const mobileSearchExpanded = ref(false)
|
||||
|
||||
@@ -134,35 +139,46 @@ async function addDownload(_torrent: TorrentInfo) {
|
||||
}
|
||||
|
||||
// 添加下载成功
|
||||
function addDownloadSuccess(_url: string) {
|
||||
function addDownloadSuccess() {
|
||||
addDownloadDialog.value = false
|
||||
}
|
||||
|
||||
// 添加下载失败
|
||||
function addDownloadError(_error: string) {
|
||||
function addDownloadError() {
|
||||
addDownloadDialog.value = false
|
||||
}
|
||||
|
||||
// 调用API,查询站点资源
|
||||
async function getResourceList() {
|
||||
const requestId = ++resourceRequestId
|
||||
resourceLoading.value = true
|
||||
resourceError.value = false
|
||||
resourcePage.value = 1
|
||||
|
||||
try {
|
||||
resourceDataList.value = await api.get(`site/resource/${props.site?.id}`, {
|
||||
const resources: TorrentInfo[] = await api.get(`site/resource/${props.site?.id}`, {
|
||||
params: {
|
||||
keyword: keyword.value,
|
||||
cat: selectCategory.value?.join(','),
|
||||
},
|
||||
})
|
||||
|
||||
if (requestId === resourceRequestId) {
|
||||
resourceDataList.value = resources
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
if (requestId === resourceRequestId) {
|
||||
console.error(error)
|
||||
resourceError.value = true
|
||||
}
|
||||
} finally {
|
||||
if (requestId === resourceRequestId) {
|
||||
resourceLoading.value = false
|
||||
|
||||
resourceLoading.value = false
|
||||
|
||||
if (isMobileLayout.value) {
|
||||
mobileSearchExpanded.value = false
|
||||
if (isMobileLayout.value) {
|
||||
mobileSearchExpanded.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,10 +296,7 @@ onMounted(() => {
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<div
|
||||
v-if="resourceTotalItems > 0"
|
||||
class="d-flex justify-space-between align-center flex-wrap gap-2 mt-3"
|
||||
>
|
||||
<div v-if="resourceTotalItems > 0" class="d-flex justify-space-between align-center flex-wrap gap-2 mt-3">
|
||||
<div class="text-body-2 text-medium-emphasis">
|
||||
{{ resultSummaryText }}
|
||||
</div>
|
||||
@@ -350,7 +363,14 @@ onMounted(() => {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" class="d-flex gap-2">
|
||||
<VBtn color="primary" variant="flat" block rounded="lg" class="site-resource-search-btn" @click="getResourceList">
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="flat"
|
||||
block
|
||||
rounded="lg"
|
||||
class="site-resource-search-btn"
|
||||
@click="getResourceList"
|
||||
>
|
||||
{{ t('dialog.siteResource.search') }}
|
||||
</VBtn>
|
||||
<VBtn variant="text" rounded="lg" @click="closeMobileSearch">
|
||||
@@ -366,6 +386,20 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<VCardText class="site-resource-content px-0 py-0 my-0">
|
||||
<VAlert
|
||||
v-if="resourceError && !resourceLoading"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mx-3 mb-3"
|
||||
:text="t('dialog.siteResource.loadFailed')"
|
||||
>
|
||||
<template #append>
|
||||
<VBtn variant="text" color="error" @click="getResourceList">
|
||||
{{ t('common.retry') }}
|
||||
</VBtn>
|
||||
</template>
|
||||
</VAlert>
|
||||
|
||||
<VDataTable
|
||||
v-if="display.mdAndUp.value"
|
||||
v-model:page="resourcePage"
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
import type { TorrentInfo } from '@/api/types'
|
||||
import SiteResourceDialog from '@/components/dialog/SiteResourceDialog.vue'
|
||||
import i18n from '@/plugins/i18n'
|
||||
import { getActiveRequestsCount } from '@/utils/requestOptimizer'
|
||||
import { screen, waitFor } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { createSite, createSiteCategory, createTorrentInfo } from '@tests/support/factories/site'
|
||||
import { siteApiUrls, siteCategoriesHandler, siteResourcesHandler } from '@tests/support/msw/handlers/site'
|
||||
import { server } from '@tests/support/msw/server'
|
||||
import { renderWithProviders } from '@tests/support/render'
|
||||
import { HttpResponse, http } from 'msw'
|
||||
import { defineComponent, h, type Component, type PropType } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const AddDownloadDialogStub = defineComponent({
|
||||
name: 'AddDownloadDialog',
|
||||
props: {
|
||||
torrent: Object,
|
||||
},
|
||||
emits: ['close', 'done', 'error', 'update:modelValue'],
|
||||
template: `
|
||||
<section data-testid="add-download-dialog">
|
||||
{{ torrent?.title }}
|
||||
<button type="button" @click="$emit('done', 'url')">done</button>
|
||||
<button type="button" @click="$emit('error', 'error')">error</button>
|
||||
<button type="button" @click="$emit('close')">close-download</button>
|
||||
</section>
|
||||
`,
|
||||
})
|
||||
|
||||
const DialogStub = defineComponent({
|
||||
name: 'VDialog',
|
||||
template: '<div role="dialog"><slot /></div>',
|
||||
})
|
||||
|
||||
const ProgressiveCardGridStub = defineComponent({
|
||||
name: 'ProgressiveCardGrid',
|
||||
props: {
|
||||
getItemKey: { type: Function as PropType<(item: TorrentInfo, index: number) => string>, required: true },
|
||||
items: { type: Array as PropType<TorrentInfo[]>, required: true },
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
return () =>
|
||||
h(
|
||||
'section',
|
||||
{ 'data-testid': 'progressive-grid' },
|
||||
props.items.map((item, index) =>
|
||||
h('article', { 'data-resource-key': props.getItemKey(item, index) }, slots.default?.({ item })),
|
||||
),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const DataTableStub = defineComponent({
|
||||
name: 'VDataTable',
|
||||
props: {
|
||||
items: { type: Array as PropType<TorrentInfo[]>, required: true },
|
||||
itemsPerPage: { type: Number, required: true },
|
||||
loading: Boolean,
|
||||
page: { type: Number, required: true },
|
||||
},
|
||||
emits: ['update:itemsPerPage', 'update:page'],
|
||||
setup(props, { emit, slots }) {
|
||||
return () =>
|
||||
h('section', { 'data-loading': String(props.loading), 'data-testid': 'resource-table' }, [
|
||||
h('span', { 'data-testid': 'resource-page' }, String(props.page)),
|
||||
h('button', { onClick: () => emit('update:page', 4), type: 'button' }, 'page-4'),
|
||||
h('button', { onClick: () => emit('update:itemsPerPage', 100), type: 'button' }, 'per-page-100'),
|
||||
...props.items.flatMap(item => slots['item.title']?.({ item }) ?? []),
|
||||
props.items.length === 0 ? slots['no-data']?.({}) : null,
|
||||
])
|
||||
},
|
||||
})
|
||||
|
||||
function createDeferred<T>() {
|
||||
let resolve!: (value: T) => void
|
||||
const promise = new Promise<T>(done => {
|
||||
resolve = done
|
||||
})
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
function setViewport(width: number) {
|
||||
Object.defineProperty(window, 'innerWidth', { configurable: true, value: width, writable: true })
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
}
|
||||
|
||||
async function renderDialog(
|
||||
site = createSite({ id: 501, name: '资源测试站' }),
|
||||
stubs: Record<string, boolean | Component> = {},
|
||||
) {
|
||||
const close = vi.fn()
|
||||
const result = await renderWithProviders(SiteResourceDialog, {
|
||||
props: { site, onClose: close },
|
||||
global: {
|
||||
stubs: {
|
||||
AddDownloadDialog: AddDownloadDialogStub,
|
||||
ProgressiveCardGrid: ProgressiveCardGridStub,
|
||||
VDialog: DialogStub,
|
||||
...stubs,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return { ...result, close, site }
|
||||
}
|
||||
|
||||
describe('SiteResourceDialog', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
setViewport(1280)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['success', 200],
|
||||
['HTTP failure', 500],
|
||||
] as const)('keeps the newest mobile search state when an older request ends with %s', async (_case, oldStatus) => {
|
||||
setViewport(390)
|
||||
const oldResponse = createDeferred<void>()
|
||||
const latestResponse = createDeferred<TorrentInfo[]>()
|
||||
const oldRequested = vi.fn()
|
||||
const latestRequested = vi.fn()
|
||||
const latestResource = createTorrentInfo({ title: '新条件结果' })
|
||||
server.use(
|
||||
siteCategoriesHandler(501, [createSiteCategory()]),
|
||||
http.get(siteApiUrls.resources(501), async ({ request }) => {
|
||||
const keyword = new URL(request.url).searchParams.get('keyword')
|
||||
if (keyword) {
|
||||
latestRequested()
|
||||
return HttpResponse.json(await latestResponse.promise)
|
||||
}
|
||||
|
||||
oldRequested()
|
||||
await oldResponse.promise
|
||||
return oldStatus === 200
|
||||
? HttpResponse.json([createTorrentInfo({ title: '旧条件结果' })])
|
||||
: HttpResponse.json({ detail: 'stale failure' }, { status: oldStatus })
|
||||
}),
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
|
||||
const { container } = await renderDialog()
|
||||
await waitFor(() => expect(oldRequested).toHaveBeenCalledOnce())
|
||||
await user.click(container.querySelector('.site-resource-mobile-search__toggle') as HTMLElement)
|
||||
const keywordFields = screen.getAllByLabelText('搜索关键字')
|
||||
const keyword = keywordFields[keywordFields.length - 1]
|
||||
await user.type(keyword, 'new')
|
||||
const searchButtons = screen.getAllByRole('button', { name: /搜索/ })
|
||||
await user.click(searchButtons[searchButtons.length - 1])
|
||||
await waitFor(() => expect(latestRequested).toHaveBeenCalledOnce())
|
||||
await waitFor(() => expect(getActiveRequestsCount()).toBe(2))
|
||||
|
||||
oldResponse.resolve()
|
||||
await waitFor(() => expect(getActiveRequestsCount()).toBe(1))
|
||||
expect(screen.getByText('加载中...')).toBeInTheDocument()
|
||||
expect(container.querySelector('.site-resource-filter-panel')).toBeInTheDocument()
|
||||
expect(screen.queryByText('资源加载失败,请重试')).not.toBeInTheDocument()
|
||||
if (oldStatus === 500) expect(console.error).not.toHaveBeenCalled()
|
||||
|
||||
latestResponse.resolve([latestResource])
|
||||
expect(await screen.findByText('新条件结果')).toBeInTheDocument()
|
||||
await waitFor(() => expect(getActiveRequestsCount()).toBe(0))
|
||||
|
||||
expect(screen.queryByText('旧条件结果')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('加载中...')).not.toBeInTheDocument()
|
||||
expect(screen.queryByLabelText('搜索关键字')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('资源加载失败,请重试')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows a retry action after resource loading fails and recovers on the same query', async () => {
|
||||
let attempts = 0
|
||||
server.use(
|
||||
siteCategoriesHandler(501, []),
|
||||
http.get(siteApiUrls.resources(501), () => {
|
||||
attempts += 1
|
||||
if (attempts === 1) return HttpResponse.json({ detail: 'temporary failure' }, { status: 500 })
|
||||
|
||||
return HttpResponse.json([createTorrentInfo({ title: '重试恢复结果' })])
|
||||
}),
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
|
||||
await renderDialog()
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: '重试' }))
|
||||
expect(await screen.findByText('重试恢复结果')).toBeInTheDocument()
|
||||
expect(attempts).toBe(2)
|
||||
})
|
||||
|
||||
it('loads categories and resources from their direct-array endpoints', async () => {
|
||||
const categoryRequests: URL[] = []
|
||||
const resourceRequests: URL[] = []
|
||||
server.use(
|
||||
siteCategoriesHandler(501, [createSiteCategory({ desc: '高清电影', id: 11 })], 200, url =>
|
||||
categoryRequests.push(url),
|
||||
),
|
||||
siteResourcesHandler(501, [createTorrentInfo({ title: '首载资源' })], 200, url => resourceRequests.push(url)),
|
||||
)
|
||||
|
||||
await renderDialog()
|
||||
|
||||
expect(await screen.findByText('首载资源')).toBeInTheDocument()
|
||||
expect(categoryRequests).toHaveLength(1)
|
||||
expect(resourceRequests).toHaveLength(1)
|
||||
expect(resourceRequests[0].searchParams.get('keyword')).toBeNull()
|
||||
expect(resourceRequests[0].searchParams.get('cat')).toBe('')
|
||||
})
|
||||
|
||||
it('sends the exact keyword and selected category ids on every search', async () => {
|
||||
const requests: URL[] = []
|
||||
server.use(
|
||||
siteCategoriesHandler(501, [
|
||||
createSiteCategory({ desc: '电影', id: 11 }),
|
||||
createSiteCategory({ cat: 'tv', desc: '剧集', id: 22 }),
|
||||
]),
|
||||
siteResourcesHandler(501, [], 200, url => requests.push(url)),
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
|
||||
await renderDialog()
|
||||
await waitFor(() => expect(requests).toHaveLength(1))
|
||||
await user.type(screen.getByLabelText('搜索关键字'), '2160p')
|
||||
await user.click(screen.getByLabelText('资源分类'))
|
||||
await user.click(await screen.findByRole('option', { name: '电影' }))
|
||||
await user.click(await screen.findByRole('option', { name: '剧集' }))
|
||||
await user.click(screen.getByRole('button', { name: /搜索/ }))
|
||||
|
||||
await waitFor(() => expect(requests).toHaveLength(2))
|
||||
expect(requests[1].searchParams.get('keyword')).toBe('2160p')
|
||||
expect(requests[1].searchParams.get('cat')).toBe('11,22')
|
||||
})
|
||||
|
||||
it('distinguishes a valid empty resource list from a category request failure', async () => {
|
||||
server.use(siteCategoriesHandler(501, [], 500), siteResourcesHandler(501, []))
|
||||
|
||||
await renderDialog()
|
||||
|
||||
expect(await screen.findByText('没有数据')).toBeInTheDocument()
|
||||
expect(screen.queryByText('资源加载失败,请重试')).not.toBeInTheDocument()
|
||||
await waitFor(() => expect(console.error).toHaveBeenCalledOnce())
|
||||
})
|
||||
|
||||
it('exposes loading during a repeated search and clears it when the latest request completes', async () => {
|
||||
const nextResponse = createDeferred<TorrentInfo[]>()
|
||||
let requestCount = 0
|
||||
server.use(
|
||||
siteCategoriesHandler(501, []),
|
||||
http.get(siteApiUrls.resources(501), async () => {
|
||||
requestCount += 1
|
||||
if (requestCount === 1) return HttpResponse.json([])
|
||||
return HttpResponse.json(await nextResponse.promise)
|
||||
}),
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
|
||||
await renderDialog(undefined, { VDataTable: DataTableStub })
|
||||
await waitFor(() => expect(screen.getByTestId('resource-table')).toHaveAttribute('data-loading', 'false'))
|
||||
await user.click(screen.getByRole('button', { name: /搜索/ }))
|
||||
await waitFor(() => expect(screen.getByTestId('resource-table')).toHaveAttribute('data-loading', 'true'))
|
||||
nextResponse.resolve([createTorrentInfo({ title: '重复搜索结果' })])
|
||||
|
||||
expect(await screen.findByText('重复搜索结果')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('resource-table')).toHaveAttribute('data-loading', 'false')
|
||||
})
|
||||
|
||||
it('renders resource metadata and every promotion style branch', async () => {
|
||||
server.use(
|
||||
siteCategoriesHandler(501, []),
|
||||
siteResourcesHandler(501, [
|
||||
createTorrentInfo({
|
||||
date_elapsed: '2 小时前',
|
||||
description: '完整资源说明',
|
||||
downloadvolumefactor: 0,
|
||||
freedate_diff: '剩余 1 天',
|
||||
hit_and_run: true,
|
||||
labels: ['原盘'],
|
||||
pubdate: '2026-07-19',
|
||||
title: '免费资源',
|
||||
volume_factor: 'FREE',
|
||||
}),
|
||||
createTorrentInfo({ downloadvolumefactor: 0.5, title: '半价资源', volume_factor: '50%' }),
|
||||
createTorrentInfo({ title: '双倍上传', uploadvolumefactor: 2, volume_factor: '2X' }),
|
||||
createTorrentInfo({ title: '普通资源' }),
|
||||
]),
|
||||
)
|
||||
|
||||
await renderDialog()
|
||||
|
||||
expect(await screen.findByText('免费资源')).toBeInTheDocument()
|
||||
expect(screen.getByText('完整资源说明')).toBeInTheDocument()
|
||||
expect(screen.getByText('H&R')).toBeInTheDocument()
|
||||
expect(screen.getByText('剩余 1 天')).toBeInTheDocument()
|
||||
expect(screen.getByText('原盘')).toBeInTheDocument()
|
||||
expect(screen.getByText('FREE').closest('.v-chip')).toHaveClass('bg-lime-500')
|
||||
expect(screen.getByText('50%').closest('.v-chip')).toHaveClass('bg-green-500')
|
||||
expect(screen.getByText('2X').closest('.v-chip')).toHaveClass('bg-sky-500')
|
||||
expect(screen.queryByText('1x')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('opens and closes the add-download boundary through all child outcomes', async () => {
|
||||
server.use(siteCategoriesHandler(501, []), siteResourcesHandler(501, [createTorrentInfo({ title: '待下载资源' })]))
|
||||
const user = userEvent.setup()
|
||||
|
||||
await renderDialog()
|
||||
const title = await screen.findByRole('button', { name: /待下载资源/ })
|
||||
|
||||
await user.click(title)
|
||||
expect(screen.getByTestId('add-download-dialog')).toHaveTextContent('待下载资源')
|
||||
await user.click(screen.getByRole('button', { name: 'done' }))
|
||||
expect(screen.queryByTestId('add-download-dialog')).not.toBeInTheDocument()
|
||||
|
||||
await user.click(title)
|
||||
await user.click(screen.getByRole('button', { name: 'error' }))
|
||||
expect(screen.queryByTestId('add-download-dialog')).not.toBeInTheDocument()
|
||||
|
||||
await user.click(title)
|
||||
await user.click(screen.getByRole('button', { name: 'close-download' }))
|
||||
expect(screen.queryByTestId('add-download-dialog')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('corrects an out-of-range desktop page after the page size changes', async () => {
|
||||
server.use(
|
||||
siteCategoriesHandler(501, []),
|
||||
siteResourcesHandler(
|
||||
501,
|
||||
Array.from({ length: 30 }, (_, index) => createTorrentInfo({ title: `分页资源 ${index + 1}` })),
|
||||
),
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
|
||||
await renderDialog(undefined, { VDataTable: DataTableStub })
|
||||
await screen.findByText('分页资源 1')
|
||||
await user.click(screen.getByRole('button', { name: 'page-4' }))
|
||||
expect(screen.getByTestId('resource-page')).toHaveTextContent('4')
|
||||
await user.click(screen.getByRole('button', { name: 'per-page-100' }))
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('resource-page')).toHaveTextContent('1'))
|
||||
})
|
||||
|
||||
it('uses stable mobile keys and guards detail and torrent external links', async () => {
|
||||
setViewport(390)
|
||||
const open = vi.spyOn(window, 'open').mockImplementation(() => null)
|
||||
server.use(
|
||||
siteCategoriesHandler(501, []),
|
||||
siteResourcesHandler(501, [
|
||||
createTorrentInfo({
|
||||
description: '移动端资源说明',
|
||||
downloadvolumefactor: 0,
|
||||
enclosure: 'https://tracker.example.com/download/one',
|
||||
freedate_diff: '剩余 2 小时',
|
||||
hit_and_run: true,
|
||||
labels: ['移动标签'],
|
||||
page_url: 'https://tracker.example.com/details/one',
|
||||
title: '详情资源',
|
||||
volume_factor: 'FREE',
|
||||
}),
|
||||
createTorrentInfo({ enclosure: 'magnet:?xt=urn:btih:test', page_url: '', title: '无外链资源' }),
|
||||
createTorrentInfo({ enclosure: '', page_url: '', pubdate: '2026-07-19', title: '回退键资源' }),
|
||||
]),
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
|
||||
await renderDialog()
|
||||
expect(await screen.findByText('详情资源')).toBeInTheDocument()
|
||||
expect(screen.getByText('移动端资源说明')).toBeInTheDocument()
|
||||
expect(screen.getByText('移动标签')).toBeInTheDocument()
|
||||
const cards = screen.getByTestId('progressive-grid').querySelectorAll('[data-resource-key]')
|
||||
expect(Array.from(cards, card => card.getAttribute('data-resource-key'))).toEqual([
|
||||
'https://tracker.example.com/details/one',
|
||||
'magnet:?xt=urn:btih:test',
|
||||
'回退键资源-2026-07-19-2',
|
||||
])
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /详情资源/ }))
|
||||
await user.click(screen.getByRole('button', { name: 'close-download' }))
|
||||
await user.click(screen.getAllByRole('button', { name: '添加下载' })[0])
|
||||
await user.click(screen.getByRole('button', { name: 'close-download' }))
|
||||
|
||||
const detailButtons = screen.getAllByLabelText('查看详情')
|
||||
await user.click(detailButtons[0])
|
||||
await user.click(detailButtons[1])
|
||||
await user.click(screen.getAllByLabelText('下载种子文件')[0])
|
||||
expect(open).toHaveBeenNthCalledWith(1, 'https://tracker.example.com/details/one', '_blank')
|
||||
expect(open).toHaveBeenNthCalledWith(2, 'https://tracker.example.com/download/one', '_blank')
|
||||
expect(open).toHaveBeenCalledTimes(2)
|
||||
expect(screen.getAllByLabelText('下载种子文件')[1]).toBeDisabled()
|
||||
})
|
||||
|
||||
it('expands and closes mobile search without retaining it after a desktop switch', async () => {
|
||||
setViewport(390)
|
||||
server.use(siteCategoriesHandler(501, []), siteResourcesHandler(501, []))
|
||||
const user = userEvent.setup()
|
||||
|
||||
const { container } = await renderDialog()
|
||||
await screen.findByText('没有数据')
|
||||
const toggle = container.querySelector('.site-resource-mobile-search__toggle')
|
||||
expect(toggle).not.toBeNull()
|
||||
await user.click(toggle as HTMLElement)
|
||||
await user.type(screen.getByLabelText('搜索关键字'), 'mobile')
|
||||
await user.click(screen.getByRole('button', { name: '取消' }))
|
||||
expect(screen.queryByLabelText('搜索关键字')).not.toBeInTheDocument()
|
||||
|
||||
await user.click(toggle as HTMLElement)
|
||||
setViewport(1280)
|
||||
await waitFor(() => expect(container.querySelector('.site-resource-mobile-search')).toBeNull())
|
||||
setViewport(390)
|
||||
await waitFor(() => expect(screen.queryByLabelText('搜索关键字')).not.toBeInTheDocument())
|
||||
})
|
||||
|
||||
it('emits close and formats the result summary in English', async () => {
|
||||
server.use(
|
||||
siteCategoriesHandler(501, []),
|
||||
siteResourcesHandler(501, [createTorrentInfo({ title: 'Language resource' })]),
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
|
||||
const { close, container } = await renderDialog()
|
||||
await screen.findByText('Language resource')
|
||||
i18n.global.locale.value = 'en-US'
|
||||
expect(await screen.findByText('1 results')).toBeInTheDocument()
|
||||
|
||||
const closeButton = container.querySelector('.v-toolbar-items .v-btn')
|
||||
expect(closeButton).not.toBeNull()
|
||||
await user.click(closeButton as HTMLElement)
|
||||
expect(close).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
@@ -3264,6 +3264,7 @@ export default {
|
||||
itemsPerPage: 'Items Per Page',
|
||||
noData: 'No Data',
|
||||
loading: 'Loading...',
|
||||
loadFailed: 'Failed to load resources. Please retry.',
|
||||
titleColumn: 'Title',
|
||||
timeColumn: 'Time',
|
||||
sizeColumn: 'Size',
|
||||
|
||||
@@ -3212,6 +3212,7 @@ export default {
|
||||
itemsPerPage: '每页条数',
|
||||
noData: '没有数据',
|
||||
loading: '加载中...',
|
||||
loadFailed: '资源加载失败,请重试',
|
||||
titleColumn: '标题',
|
||||
timeColumn: '时间',
|
||||
sizeColumn: '大小',
|
||||
|
||||
@@ -3212,6 +3212,7 @@ export default {
|
||||
pageText: '{0}-{1} 共 {2} 條',
|
||||
noData: '沒有數據',
|
||||
loading: '加載中...',
|
||||
loadFailed: '資源加載失敗,請重試',
|
||||
titleColumn: '標題',
|
||||
timeColumn: '時間',
|
||||
sizeColumn: '大小',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { DownloaderConf, Site, SiteStatistic, SiteUserData } from '@/api/types'
|
||||
import type { DownloaderConf, Site, SiteCategory, SiteStatistic, SiteUserData, TorrentInfo } from '@/api/types'
|
||||
|
||||
let siteSeed = 0
|
||||
let torrentSeed = 0
|
||||
|
||||
export function createSite(overrides: Partial<Site> = {}): Site {
|
||||
siteSeed += 1
|
||||
@@ -49,3 +50,38 @@ export function createSiteDownloader(overrides: Partial<DownloaderConf> = {}): D
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
/** 构造站点资源分类测试记录。 */
|
||||
export function createSiteCategory(overrides: Partial<SiteCategory> = {}): SiteCategory {
|
||||
return {
|
||||
cat: 'movie',
|
||||
desc: '电影',
|
||||
id: 1,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
/** 构造满足资源列表展示与下载边界的种子记录。 */
|
||||
export function createTorrentInfo(overrides: Partial<TorrentInfo> = {}): TorrentInfo {
|
||||
torrentSeed += 1
|
||||
return {
|
||||
category: '电影',
|
||||
downloadvolumefactor: 1,
|
||||
freedate: '',
|
||||
freedate_diff: '',
|
||||
grabs: 10,
|
||||
hit_and_run: false,
|
||||
imdbid: '',
|
||||
labels: [],
|
||||
peers: 3,
|
||||
pri_order: 0,
|
||||
seeders: 12,
|
||||
site_order: 0,
|
||||
site_proxy: false,
|
||||
size: 1_073_741_824,
|
||||
title: `测试资源 ${torrentSeed}`,
|
||||
uploadvolumefactor: 1,
|
||||
volume_factor: '1x',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import type { ApiResponse, DownloaderConf, Site, SiteStatistic, SiteUserData } from '@/api/types'
|
||||
import type {
|
||||
ApiResponse,
|
||||
DownloaderConf,
|
||||
Site,
|
||||
SiteCategory,
|
||||
SiteStatistic,
|
||||
SiteUserData,
|
||||
TorrentInfo,
|
||||
} from '@/api/types'
|
||||
import { HttpResponse, http, type JsonBodyType } from 'msw'
|
||||
|
||||
const API_BASE_URL = 'http://localhost/api/v1/'
|
||||
@@ -10,6 +18,7 @@ interface SiteMutationResponse {
|
||||
}
|
||||
|
||||
export const siteApiUrls = {
|
||||
categories: (siteId: number) => new URL(`site/category/${siteId}`, API_BASE_URL).href,
|
||||
cookie: (id: number) => new URL(`site/cookie/${id}`, API_BASE_URL).href,
|
||||
delete: (id: number) => new URL(`site/${id}`, API_BASE_URL).href,
|
||||
details: (id: number) => new URL(`site/${id}`, API_BASE_URL).href,
|
||||
@@ -17,6 +26,7 @@ export const siteApiUrls = {
|
||||
icon: (id: number) => new URL(`site/icon/${id}`, API_BASE_URL).href,
|
||||
list: new URL('site/', API_BASE_URL).href,
|
||||
priorities: new URL('site/priorities', API_BASE_URL).href,
|
||||
resources: (siteId: number) => new URL(`site/resource/${siteId}`, API_BASE_URL).href,
|
||||
statistic: (domain: string) => new URL(`site/statistic/${domain}`, API_BASE_URL).href,
|
||||
statistics: new URL('site/statistic', API_BASE_URL).href,
|
||||
test: (id: number) => new URL(`site/test/${id}`, API_BASE_URL).href,
|
||||
@@ -180,3 +190,27 @@ export function updateSiteCookieHandler(
|
||||
return HttpResponse.json(response, { status })
|
||||
})
|
||||
}
|
||||
|
||||
export function siteCategoriesHandler(
|
||||
siteId: number,
|
||||
response: SiteCategory[],
|
||||
status = 200,
|
||||
onRequest: (url: URL) => void = () => {},
|
||||
) {
|
||||
return http.get(siteApiUrls.categories(siteId), ({ request }) => {
|
||||
onRequest(new URL(request.url))
|
||||
return HttpResponse.json(response, { status })
|
||||
})
|
||||
}
|
||||
|
||||
export function siteResourcesHandler(
|
||||
siteId: number,
|
||||
response: TorrentInfo[],
|
||||
status = 200,
|
||||
onRequest: (url: URL) => void = () => {},
|
||||
) {
|
||||
return http.get(siteApiUrls.resources(siteId), ({ request }) => {
|
||||
onRequest(new URL(request.url))
|
||||
return HttpResponse.json(response, { status })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -301,6 +301,7 @@ export default defineConfig(({ mode }) => ({
|
||||
'src/components/dialog/SubscribeShareDialog.vue',
|
||||
'src/components/dialog/SubscribeShareStatisticsDialog.vue',
|
||||
'src/components/dialog/DiscoverTabOrderDialog.vue',
|
||||
'src/components/dialog/SiteResourceDialog.vue',
|
||||
'src/views/discover/TheMovieDbView.vue',
|
||||
'src/views/discover/DoubanView.vue',
|
||||
'src/views/discover/BangumiView.vue',
|
||||
@@ -485,6 +486,12 @@ export default defineConfig(({ mode }) => ({
|
||||
lines: 80,
|
||||
statements: 80,
|
||||
},
|
||||
'src/components/dialog/SiteResourceDialog.vue': {
|
||||
branches: 85,
|
||||
functions: 90,
|
||||
lines: 90,
|
||||
statements: 90,
|
||||
},
|
||||
'src/views/discover/TheMovieDbView.vue': {
|
||||
branches: 75,
|
||||
functions: 80,
|
||||
|
||||
Reference in New Issue
Block a user