test: 统一前端测试夹具与发布工具链 (#704)

This commit is contained in:
InfinityPacer
2026-08-24 06:43:36 +08:00
committed by GitHub
parent 5847f9e936
commit 058b8f2299
36 changed files with 470 additions and 317 deletions
+7 -3
View File
@@ -1,5 +1,6 @@
import type { DiscoverSource } from '@/api/types'
import { HttpResponse, http, type JsonBodyType } from 'msw'
import { apiJson } from '../response'
const API_BASE_URL = 'http://localhost/api/v1/'
@@ -21,7 +22,8 @@ export function discoverSourcesHandler(
) {
return http.get(discoverApiUrls.sources, async () => {
await onRequest()
return HttpResponse.json(sources as unknown as JsonBodyType, { status })
if (status >= 400) return HttpResponse.json(sources as unknown as JsonBodyType, { status })
return apiJson(sources, { status })
})
}
@@ -32,7 +34,8 @@ export function discoverOrderConfigHandler(
) {
return http.get(discoverApiUrls.orderConfig, async () => {
await onRequest()
return HttpResponse.json({ data: { value: order }, success: status < 400 }, { status })
if (status >= 400) return HttpResponse.json({ detail: 'failed' }, { status })
return apiJson({ value: order }, { status })
})
}
@@ -43,6 +46,7 @@ export function saveDiscoverOrderHandler(
return http.post(discoverApiUrls.orderConfig, async ({ request }) => {
const order = (await request.json()) as DiscoverTabConfigItem[]
await onSave(order)
return HttpResponse.json({ success: status < 400 }, { status })
if (status >= 400) return HttpResponse.json({ detail: 'failed' }, { status })
return apiJson(null, { status })
})
}
+15 -7
View File
@@ -1,5 +1,6 @@
import type { DownloadHistory, DownloadingInfo } from '@/api/types'
import { HttpResponse, http, type JsonBodyType } from 'msw'
import { apiFailureJson, apiJson } from '../response'
const API_BASE_URL = 'http://localhost/api/v1/'
@@ -15,8 +16,15 @@ export const downloadApiUrls = {
history: new URL('history/download', API_BASE_URL).href,
}
function jsonResponse(body: JsonBodyType, status: number) {
return HttpResponse.json(body, { status })
function dataResponse(body: JsonBodyType, status: number) {
if (status >= 400) return HttpResponse.json(body, { status })
return apiJson(body, { status })
}
function mutationResponse(response: DownloadMutationResponse, status: number) {
if (status >= 400) return HttpResponse.json(response, { status })
if (!response.success) return apiFailureJson(response.message ?? '', null, { status })
return apiJson(null, { status })
}
/** 拦截下载任务快照查询,并保留下载器查询参数供断言。 */
@@ -29,7 +37,7 @@ export function downloadingListHandler(
const url = new URL(request.url)
await onRequest(url)
const body = typeof response === 'function' ? await response(url) : response
return jsonResponse(body as unknown as JsonBodyType, status)
return dataResponse(body as unknown as JsonBodyType, status)
})
}
@@ -44,7 +52,7 @@ export function downloadActionHandler(
return http.get(downloadApiUrls.action(operation, hash), async ({ request }) => {
const url = new URL(request.url)
await onRequest(url)
return jsonResponse(response, status)
return mutationResponse(response, status)
})
}
@@ -58,7 +66,7 @@ export function deleteDownloadHandler(
return http.delete(downloadApiUrls.delete(hash), async ({ request }) => {
const url = new URL(request.url)
await onRequest(url)
return jsonResponse(response, status)
return mutationResponse(response, status)
})
}
@@ -72,7 +80,7 @@ export function downloadHistoryHandler(
const url = new URL(request.url)
await onRequest(url)
const body = typeof response === 'function' ? await response(url) : response
return jsonResponse(body as unknown as JsonBodyType, status)
return dataResponse(body as unknown as JsonBodyType, status)
})
}
@@ -84,6 +92,6 @@ export function deleteDownloadHistoryHandler(
) {
return http.delete(downloadApiUrls.history, async ({ request }) => {
await onRequest((await request.json()) as DownloadHistory)
return jsonResponse(response, status)
return mutationResponse(response, status)
})
}
+17 -9
View File
@@ -1,5 +1,6 @@
import type { MediaInfo, MediaSeason, NotExistMediaInfo, TmdbEpisode } from '@/api/types'
import { HttpResponse, http, type JsonBodyType } from 'msw'
import { apiFailureJson, apiJson } from '../response'
const API_BASE_URL = 'http://localhost/api/v1/'
@@ -14,6 +15,11 @@ export const mediaApiUrls = {
seasons: new URL('media/seasons', API_BASE_URL).href,
}
function dataResponse(body: JsonBodyType, status: number) {
if (status >= 400) return HttpResponse.json(body, { status })
return apiJson(body, { status })
}
export function mediaExistsHandler(
response: { data?: Record<string, unknown>; message?: string; success: boolean },
status = 200,
@@ -21,7 +27,8 @@ export function mediaExistsHandler(
) {
return http.get(mediaApiUrls.exists, async ({ request }) => {
await onRequest(new URL(request.url))
return HttpResponse.json({ message: '', ...response } as JsonBodyType, { status })
if (!response.success) return apiFailureJson(response.message ?? '', response.data ?? null, { status })
return apiJson(response.data ?? null, { status })
})
}
@@ -33,7 +40,7 @@ export function mediaDetailsHandler(
) {
return http.get(mediaApiUrls.details(String(mediaId)), ({ request }) => {
onRequest(new URL(request.url))
return HttpResponse.json(response as unknown as JsonBodyType, { status })
return dataResponse(response as unknown as JsonBodyType, status)
})
}
@@ -44,7 +51,7 @@ export function mediaRemoteExistsHandler(
) {
return http.post(mediaApiUrls.existsRemote, async ({ request }) => {
await onRequest((await request.json()) as Record<string, unknown>)
return HttpResponse.json(response as JsonBodyType, { status })
return dataResponse(response as JsonBodyType, status)
})
}
@@ -56,7 +63,8 @@ export function mediaPlayHandler(
) {
return http.get(mediaApiUrls.play(itemId), () => {
onRequest()
return HttpResponse.json(response as JsonBodyType, { status })
if (!response.success) return apiFailureJson(response.message ?? '', response.data ?? null, { status })
return apiJson(response.data ?? null, { status })
})
}
@@ -69,7 +77,7 @@ export function tmdbSeasonEpisodesHandler(
) {
return http.get(new URL(`tmdb/${tmdbId}/${season}`, API_BASE_URL).href, ({ request }) => {
onRequest(new URL(request.url))
return HttpResponse.json(response as unknown as JsonBodyType, { status })
return dataResponse(response as unknown as JsonBodyType, status)
})
}
@@ -80,7 +88,7 @@ export function mediaSeasonsHandler(
) {
return http.get(mediaApiUrls.seasons, async ({ request }) => {
await onRequest(new URL(request.url))
return HttpResponse.json(response as unknown as JsonBodyType, { status })
return dataResponse(response as unknown as JsonBodyType, status)
})
}
@@ -92,7 +100,7 @@ export function mediaEpisodeGroupsHandler(
) {
return http.get(mediaApiUrls.episodeGroups(tmdbId), async ({ request }) => {
await onRequest(new URL(request.url))
return HttpResponse.json(response as JsonBodyType, { status })
return dataResponse(response as JsonBodyType, status)
})
}
@@ -104,7 +112,7 @@ export function mediaGroupSeasonsHandler(
) {
return http.get(mediaApiUrls.groupSeasons(episodeGroup), async ({ request }) => {
await onRequest(new URL(request.url))
return HttpResponse.json(response as unknown as JsonBodyType, { status })
return dataResponse(response as unknown as JsonBodyType, status)
})
}
@@ -116,6 +124,6 @@ export function mediaNotExistsHandler(
return http.post(mediaApiUrls.notExists, async ({ request }) => {
const payload = (await request.json()) as Record<string, unknown>
await onRequest(payload, new URL(request.url))
return HttpResponse.json(response as unknown as JsonBodyType, { status })
return dataResponse(response as unknown as JsonBodyType, status)
})
}
+12 -18
View File
@@ -1,5 +1,6 @@
import type { RecommendSource } from '@/api/types'
import { HttpResponse, http, type JsonBodyType } from 'msw'
import { apiJson } from '../response'
const API_BASE_URL = 'http://localhost/api/v1/'
@@ -9,36 +10,28 @@ export const recommendApiUrls = {
sources: new URL('recommend/source', API_BASE_URL).href,
}
export function recommendSourcesHandler(
sources: RecommendSource[],
status = 200,
onRequest: () => void = () => {},
) {
export function recommendSourcesHandler(sources: RecommendSource[], status = 200, onRequest: () => void = () => {}) {
return http.get(recommendApiUrls.sources, () => {
onRequest()
return HttpResponse.json(sources, { status })
if (status >= 400) return HttpResponse.json(sources, { status })
return apiJson(sources, { status })
})
}
export function recommendConfigHandler(
config: JsonBodyType,
status = 200,
onRequest: () => void = () => {},
) {
export function recommendConfigHandler(config: JsonBodyType, status = 200, onRequest: () => void = () => {}) {
return http.get(recommendApiUrls.config, () => {
onRequest()
return HttpResponse.json({ data: { value: config } }, { status })
if (status >= 400) return HttpResponse.json({ detail: 'failed' }, { status })
return apiJson({ value: config }, { status })
})
}
export function saveRecommendConfigHandler(
onSave: (config: Record<string, boolean>) => void = () => {},
status = 200,
) {
export function saveRecommendConfigHandler(onSave: (config: Record<string, boolean>) => void = () => {}, status = 200) {
return http.post(recommendApiUrls.config, async ({ request }) => {
const config = (await request.json()) as Record<string, boolean>
onSave(config)
return HttpResponse.json({ success: status < 400 }, { status })
if (status >= 400) return HttpResponse.json({ detail: 'failed' }, { status })
return apiJson(null, { status })
})
}
@@ -50,6 +43,7 @@ export function recommendMediaHandler(
) {
return http.get(recommendApiUrls.media(sourcePath), () => {
onRequest()
return HttpResponse.json(response as JsonBodyType, { status })
if (status >= 400) return HttpResponse.json(response as JsonBodyType, { status })
return apiJson(response, { status })
})
}
+44 -33
View File
@@ -9,6 +9,7 @@ import type {
TransferDirectoryConf,
} from '@/api/types'
import { HttpResponse, http, type JsonBodyType, type RequestHandler } from 'msw'
import { apiFailureJson, apiJson } from '../response'
const API_BASE_URL = 'http://localhost/api/v1/'
@@ -65,8 +66,15 @@ export const subscribeApiUrls = {
update: new URL('subscribe/', API_BASE_URL).href,
}
function jsonResponse(body: JsonBodyType, status: number) {
return HttpResponse.json(body, { status })
function dataResponse(body: JsonBodyType, status: number) {
if (status >= 400) return HttpResponse.json(body, { status })
return apiJson(body, { status })
}
function mutationResponse(response: SubscribeMutationResponse, status: number) {
if (status >= 400) return HttpResponse.json(response, { status })
if (!response.success) return apiFailureJson(response.message ?? '', response.data ?? null, { status })
return apiJson(response.data ?? null, { status })
}
export function subscribeListHandler(
@@ -76,7 +84,7 @@ export function subscribeListHandler(
) {
return http.get(subscribeApiUrls.list, ({ request }) => {
onRequest(new URL(request.url))
return jsonResponse(response, status)
return dataResponse(response, status)
})
}
@@ -87,7 +95,7 @@ export function popularSubscribesHandler(
) {
return http.get(subscribeApiUrls.popular, async ({ request }) => {
await onRequest(new URL(request.url))
return jsonResponse(response as unknown as JsonBodyType, status)
return dataResponse(response as unknown as JsonBodyType, status)
})
}
@@ -98,7 +106,7 @@ export function subscribeSharesHandler(
) {
return http.get(subscribeApiUrls.shares, async ({ request }) => {
await onRequest(new URL(request.url))
return jsonResponse(response as unknown as JsonBodyType, status)
return dataResponse(response as unknown as JsonBodyType, status)
})
}
@@ -110,7 +118,7 @@ export function shareSubscribeHandler(
return http.post(subscribeApiUrls.share, async ({ request }) => {
const payload = (await request.json()) as SubscribeShare
await onShare(payload)
return jsonResponse(response, status)
return mutationResponse(response, status)
})
}
@@ -122,7 +130,7 @@ export function forkSubscribeHandler(
return http.post(subscribeApiUrls.fork, async ({ request }) => {
const payload = (await request.json()) as SubscribeShare
await onFork(payload)
return jsonResponse(response, status)
return mutationResponse(response, status)
})
}
@@ -133,7 +141,8 @@ export function followSubscribersSettingHandler(
) {
return http.get(subscribeApiUrls.followSubscribers, async ({ request }) => {
await onRequest(new URL(request.url))
return jsonResponse({ data: { value: users }, success: status < 400 }, status)
if (status >= 400) return HttpResponse.json({ detail: 'failed' }, { status })
return apiJson({ value: users }, { status })
})
}
@@ -144,7 +153,7 @@ export function followSubscriberHandler(
) {
return http.post(subscribeApiUrls.follow, async ({ request }) => {
await onRequest(new URL(request.url))
return jsonResponse(response, status)
return mutationResponse(response, status)
})
}
@@ -155,7 +164,7 @@ export function unfollowSubscriberHandler(
) {
return http.delete(subscribeApiUrls.follow, async ({ request }) => {
await onRequest(new URL(request.url))
return jsonResponse(response, status)
return mutationResponse(response, status)
})
}
@@ -167,7 +176,7 @@ export function deleteSubscribeShareHandler(
) {
return http.delete(subscribeApiUrls.shareById(id), async ({ request }) => {
await onRequest(new URL(request.url))
return jsonResponse(response, status)
return mutationResponse(response, status)
})
}
@@ -178,7 +187,7 @@ export function subscribeShareStatisticsHandler(
) {
return http.get(subscribeApiUrls.shareStatistics, async ({ request }) => {
await onRequest(new URL(request.url))
return jsonResponse(response as unknown as JsonBodyType, status)
return dataResponse(response as unknown as JsonBodyType, status)
})
}
@@ -190,7 +199,7 @@ export function subscribeFilesHandler(
) {
return http.get(subscribeApiUrls.filesById(id), async ({ request }) => {
await onRequest(new URL(request.url))
return jsonResponse(response, status)
return dataResponse(response, status)
})
}
@@ -202,7 +211,7 @@ export function subscribeHistoryHandler(
) {
return http.get(subscribeApiUrls.historyByType(type), async ({ request }) => {
await onRequest(new URL(request.url))
return jsonResponse(response as unknown as JsonBodyType, status)
return dataResponse(response as unknown as JsonBodyType, status)
})
}
@@ -214,7 +223,7 @@ export function deleteSubscribeHistoryHandler(
) {
return http.delete(subscribeApiUrls.historyById(id), async ({ request }) => {
await onRequest(new URL(request.url))
return jsonResponse(response, status)
return mutationResponse(response, status)
})
}
@@ -226,7 +235,8 @@ export function subscribeOrderConfigHandler(
) {
return http.get(subscribeApiUrls.orderConfig(type), ({ request }) => {
onRequest(new URL(request.url))
return jsonResponse({ data: { value }, success: status < 400 }, status)
if (status >= 400) return HttpResponse.json({ detail: 'failed' }, { status })
return apiJson({ value }, { status })
})
}
@@ -239,7 +249,7 @@ export function saveSubscribeOrderConfigHandler(
return http.post(subscribeApiUrls.orderConfig(type), async ({ request }) => {
const payload = (await request.json()) as { id: number }[]
await onSave(payload, new URL(request.url))
return jsonResponse(response, status)
return mutationResponse(response, status)
})
}
@@ -251,7 +261,7 @@ export function updateSubscribeStatusHandler(
) {
return http.put(subscribeApiUrls.statusById(id), async ({ request }) => {
await onRequest(new URL(request.url))
return jsonResponse(response, status)
return mutationResponse(response, status)
})
}
@@ -263,7 +273,7 @@ export function searchSubscribeByIdHandler(
) {
return http.get(subscribeApiUrls.searchById(id), ({ request }) => {
onRequest(new URL(request.url))
return jsonResponse(response, status)
return mutationResponse(response, status)
})
}
@@ -275,7 +285,7 @@ export function resetSubscribeByIdHandler(
) {
return http.get(subscribeApiUrls.resetById(id), ({ request }) => {
onRequest(new URL(request.url))
return jsonResponse(response, status)
return mutationResponse(response, status)
})
}
@@ -287,7 +297,7 @@ export function createSubscribeHandler(
return http.post(subscribeApiUrls.create, async ({ request }) => {
const payload = (await request.json()) as Record<string, unknown>
onCreate(payload)
return jsonResponse(response, status)
return mutationResponse(response, status)
})
}
@@ -299,7 +309,7 @@ export function updateSubscribeHandler(
return http.put(subscribeApiUrls.update, async ({ request }) => {
const payload = (await request.json()) as Record<string, unknown>
onUpdate(payload)
return jsonResponse(response, status)
return mutationResponse(response, status)
})
}
@@ -311,7 +321,7 @@ export function querySubscribeByMediaHandler(
) {
return http.get(subscribeApiUrls.queryByMedia(mediaId), ({ request }) => {
onRequest(new URL(request.url))
return jsonResponse(subscribe as JsonBodyType, status)
return dataResponse(subscribe as JsonBodyType, status)
})
}
@@ -323,7 +333,7 @@ export function deleteSubscribeByMediaHandler(
) {
return http.delete(subscribeApiUrls.deleteByMedia(mediaId), ({ request }) => {
onRequest(new URL(request.url))
return jsonResponse(response, status)
return mutationResponse(response, status)
})
}
@@ -335,7 +345,7 @@ export function subscribeDetailsHandler(
) {
return http.get(subscribeApiUrls.details(id), () => {
onRequest()
return jsonResponse(subscribe as unknown as JsonBodyType, status)
return dataResponse(subscribe as unknown as JsonBodyType, status)
})
}
@@ -347,7 +357,7 @@ export function deleteSubscribeByIdHandler(
) {
return http.delete(subscribeApiUrls.deleteById(id), () => {
onRequest()
return jsonResponse(response, status)
return mutationResponse(response, status)
})
}
@@ -359,7 +369,8 @@ export function defaultSubscribeConfigHandler(
) {
return http.get(subscribeApiUrls.defaultConfig(type), () => {
onRequest()
return jsonResponse({ data: { value: config }, success: status < 400 }, status)
if (status >= 400) return HttpResponse.json({ detail: 'failed' }, { status })
return apiJson({ value: config }, { status })
})
}
@@ -372,7 +383,7 @@ export function saveDefaultSubscribeConfigHandler(
return http.post(subscribeApiUrls.defaultConfig(type, true), async ({ request }) => {
const payload = (await request.json()) as Record<string, unknown>
onSave(payload)
return jsonResponse(response, status)
return mutationResponse(response, status)
})
}
@@ -409,23 +420,23 @@ export function subscribeDialogOptionHandlers(options: SubscribeDialogOptions =
return [
http.get(subscribeApiUrls.sites, () => {
onSites()
return jsonResponse(sites as unknown as JsonBodyType, 200)
return dataResponse(sites as unknown as JsonBodyType, 200)
}),
http.get(subscribeApiUrls.downloaders, () => {
onDownloaders()
return jsonResponse(downloaders as unknown as JsonBodyType, 200)
return dataResponse(downloaders as unknown as JsonBodyType, 200)
}),
http.get(subscribeApiUrls.directories, () => {
onDirectories()
return jsonResponse({ data: { value: directories }, success: true }, 200)
return apiJson({ value: directories })
}),
http.get(subscribeApiUrls.filterRuleGroups, () => {
onFilterRuleGroups()
return jsonResponse({ data: { value: filterRuleGroups }, success: true }, 200)
return apiJson({ value: filterRuleGroups })
}),
http.get(subscribeApiUrls.episodeGroups(tmdbId), () => {
onEpisodeGroups()
return jsonResponse(episodeGroups as unknown as JsonBodyType, 200)
return dataResponse(episodeGroups as unknown as JsonBodyType, 200)
}),
]
}
+18
View File
@@ -0,0 +1,18 @@
import type { ApiResponse } from '@/api/types'
import { HttpResponse, type HttpResponseInit, type JsonBodyType } from 'msw'
/** 构造主程序普通 API 的固定三段式响应。 */
export function apiEnvelope<T>(data: T | null, message = ''): ApiResponse<T> {
return { data, message, success: true }
}
/** 返回主程序普通 API 的成功响应;调用点必须显式选择该协议。 */
export function apiJson<T>(data: T | null, init: HttpResponseInit = {}) {
return HttpResponse.json(apiEnvelope(data) as unknown as JsonBodyType, init)
}
/** 返回主程序普通 API 的业务失败响应。 */
export function apiFailureJson<T = null>(message: string, data: T | null = null, init: HttpResponseInit = {}) {
const body: ApiResponse<T> = { data, message, success: false }
return HttpResponse.json(body as unknown as JsonBodyType, init)
}