From 254233973386dc1291431dcd84cc5659eb0965ce Mon Sep 17 00:00:00 2001
From: InfinityPacer <160988576+InfinityPacer@users.noreply.github.com>
Date: Sun, 19 Jul 2026 17:05:56 +0800
Subject: [PATCH] test(site): cover site resource dialog (#563)
---
eslint-suppressions.json | 5 -
src/components/dialog/SiteResourceDialog.vue | 62 ++-
.../__tests__/SiteResourceDialog.spec.ts | 428 ++++++++++++++++++
src/locales/en-US.ts | 1 +
src/locales/zh-CN.ts | 1 +
src/locales/zh-TW.ts | 1 +
tests/support/factories/site.ts | 38 +-
tests/support/msw/handlers/site.ts | 36 +-
vite.config.ts | 7 +
9 files changed, 558 insertions(+), 21 deletions(-)
create mode 100644 src/components/dialog/__tests__/SiteResourceDialog.spec.ts
diff --git a/eslint-suppressions.json b/eslint-suppressions.json
index b298a520..ac4fc243 100644
--- a/eslint-suppressions.json
+++ b/eslint-suppressions.json
@@ -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
diff --git a/src/components/dialog/SiteResourceDialog.vue b/src/components/dialog/SiteResourceDialog.vue
index dde8cfb2..2cd7d27b 100644
--- a/src/components/dialog/SiteResourceDialog.vue
+++ b/src/components/dialog/SiteResourceDialog.vue
@@ -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(() => {
-
+
{{ resultSummaryText }}
@@ -350,7 +363,14 @@ onMounted(() => {
/>
-
+
{{ t('dialog.siteResource.search') }}
@@ -366,6 +386,20 @@ onMounted(() => {
+
+
+
+ {{ t('common.retry') }}
+
+
+
+
+ {{ torrent?.title }}
+
+
+
+
+ `,
+})
+
+const DialogStub = defineComponent({
+ name: 'VDialog',
+ template: '
',
+})
+
+const ProgressiveCardGridStub = defineComponent({
+ name: 'ProgressiveCardGrid',
+ props: {
+ getItemKey: { type: Function as PropType<(item: TorrentInfo, index: number) => string>, required: true },
+ items: { type: Array as PropType, 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, 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() {
+ let resolve!: (value: T) => void
+ const promise = new Promise(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 = {},
+) {
+ 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()
+ const latestResponse = createDeferred()
+ 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()
+ 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()
+ })
+})
diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts
index d394ee41..e82fa7cb 100644
--- a/src/locales/en-US.ts
+++ b/src/locales/en-US.ts
@@ -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',
diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts
index 9ae682e4..91d02024 100644
--- a/src/locales/zh-CN.ts
+++ b/src/locales/zh-CN.ts
@@ -3212,6 +3212,7 @@ export default {
itemsPerPage: '每页条数',
noData: '没有数据',
loading: '加载中...',
+ loadFailed: '资源加载失败,请重试',
titleColumn: '标题',
timeColumn: '时间',
sizeColumn: '大小',
diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts
index 6855ba14..96b75b35 100644
--- a/src/locales/zh-TW.ts
+++ b/src/locales/zh-TW.ts
@@ -3212,6 +3212,7 @@ export default {
pageText: '{0}-{1} 共 {2} 條',
noData: '沒有數據',
loading: '加載中...',
+ loadFailed: '資源加載失敗,請重試',
titleColumn: '標題',
timeColumn: '時間',
sizeColumn: '大小',
diff --git a/tests/support/factories/site.ts b/tests/support/factories/site.ts
index 50e958ee..e5327740 100644
--- a/tests/support/factories/site.ts
+++ b/tests/support/factories/site.ts
@@ -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 {
siteSeed += 1
@@ -49,3 +50,38 @@ export function createSiteDownloader(overrides: Partial = {}): D
...overrides,
}
}
+
+/** 构造站点资源分类测试记录。 */
+export function createSiteCategory(overrides: Partial = {}): SiteCategory {
+ return {
+ cat: 'movie',
+ desc: '电影',
+ id: 1,
+ ...overrides,
+ }
+}
+
+/** 构造满足资源列表展示与下载边界的种子记录。 */
+export function createTorrentInfo(overrides: Partial = {}): 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,
+ }
+}
diff --git a/tests/support/msw/handlers/site.ts b/tests/support/msw/handlers/site.ts
index ecc856c2..ae4e72ba 100644
--- a/tests/support/msw/handlers/site.ts
+++ b/tests/support/msw/handlers/site.ts
@@ -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 })
+ })
+}
diff --git a/vite.config.ts b/vite.config.ts
index 4b680117..e6fc2abe 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -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,