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
+23
View File
@@ -0,0 +1,23 @@
import { HttpResponse } from 'msw'
import { describe, expect, it } from 'vitest'
import { apiFailureJson, apiJson } from '../support/msw/response'
describe('API response fixtures', () => {
it('keeps raw MSW responses unchanged for HTTP errors and plugin protocols', async () => {
const response = HttpResponse.json([{ id: 1 }])
await expect(response.json()).resolves.toEqual([{ id: 1 }])
})
it('builds the complete main-program success envelope explicitly', async () => {
const response = apiJson({ id: 1 })
await expect(response.json()).resolves.toEqual({ data: { id: 1 }, message: '', success: true })
})
it('builds the complete main-program business-failure envelope explicitly', async () => {
const response = apiFailureJson('rejected', { id: 1 })
await expect(response.json()).resolves.toEqual({ data: { id: 1 }, message: 'rejected', success: false })
})
})
+17
View File
@@ -6,6 +6,7 @@ const workflowPath = resolve(process.cwd(), '.github/workflows/test.yml')
const releaseWorkflowPath = resolve(process.cwd(), '.github/workflows/build.yml')
const testingGuidePath = resolve(process.cwd(), 'docs/testing.md')
const codeQualityGuidePath = resolve(process.cwd(), 'docs/code-quality.md')
const prettierIgnorePath = resolve(process.cwd(), '.prettierignore')
describe('前端测试 workflow', () => {
it('在 PR 与 v3 push 上运行,并将变更文件格式检查限制为 PR', () => {
@@ -59,9 +60,25 @@ describe('前端测试 workflow', () => {
const workflow = readFileSync(releaseWorkflowPath, 'utf8')
expect(workflow).toContain('name: Build Moviepilot-Frontend v3')
expect(workflow).toContain('permissions:\n contents: write')
expect(workflow).toContain('push:\n branches:\n - v3')
expect(workflow).toContain(" - 'package.json'")
expect(workflow).toContain('timeout-minutes: 30')
expect(workflow).toContain('uses: actions/checkout@v7')
expect(workflow).toContain('uses: actions/setup-node@v7')
expect(workflow).toContain("node-version: '24'")
expect(workflow).toContain('cache-dependency-path: yarn.lock')
expect(workflow).toContain('corepack install --global yarn@1.22.22')
expect(workflow).toContain('run: yarn --frozen-lockfile')
expect(workflow).toContain('echo "frontend_version=v$frontend_version"')
expect(workflow).not.toContain('actions/checkout@v3')
expect(workflow).not.toContain('actions/setup-node@v3')
expect(workflow).not.toContain(' - v2')
})
it('全仓格式检查排除仓内 linked worktree', () => {
const prettierIgnore = readFileSync(prettierIgnorePath, 'utf8')
expect(prettierIgnore).toContain('/.worktrees/')
})
})
-56
View File
@@ -1,7 +1,6 @@
import '@testing-library/jest-dom/vitest'
import { abortAllRequests } from '@/utils/requestOptimizer'
import { cleanup } from '@testing-library/vue'
import { HttpResponse, type HttpResponseInit, type JsonBodyType } from 'msw'
import { afterAll, afterEach, beforeAll, vi } from 'vitest'
import { createDataApiMock as buildDataApiMock } from './support/apiMock'
import { server } from './support/msw/server'
@@ -13,61 +12,6 @@ declare global {
globalThis.createDataApiMock = buildDataApiMock
const originalJsonResponse = HttpResponse.json.bind(HttpResponse)
/** 判断测试夹具是否已经表达了业务响应状态。 */
function isLegacyApiEnvelope(body: unknown): body is Record<string, unknown> & { success: boolean } {
return (
body !== null &&
typeof body === 'object' &&
!Array.isArray(body) &&
typeof (body as { success?: unknown }).success === 'boolean'
)
}
/** 识别 MSW 夹具中历史遗留的 Axios `{ data }` 响应壳。 */
function isLegacyDataWrapper(body: unknown): body is { data: unknown } {
if (!body || typeof body !== 'object' || Array.isArray(body)) return false
return Object.keys(body).length === 1 && 'data' in body
}
/**
* 将业务测试中的裸成功数据适配为当前后端统一 envelope。
*
* 低层客户端协议测试使用 Axios adapter,不经过这里;HTTP 错误保持原载荷,
* 便于继续覆盖 detail、Blob 和非标准错误响应的归一化行为。
*/
function installApiEnvelopeFixtureAdapter() {
Object.defineProperty(HttpResponse, 'json', {
configurable: true,
value: <BodyType extends JsonBodyType>(body?: BodyType | null, init: HttpResponseInit = {}) => {
const status = init.status ?? 200
if (status >= 400) return originalJsonResponse(body, init)
if (isLegacyApiEnvelope(body)) {
const envelope = body as Record<string, unknown> & { success: boolean }
return originalJsonResponse(
{
...envelope,
message: typeof envelope.message === 'string' ? envelope.message : '',
data: Object.hasOwn(envelope, 'data') ? envelope.data : null,
},
init,
)
}
if (isLegacyDataWrapper(body)) {
return originalJsonResponse({ success: true, message: '', data: body.data }, init)
}
return originalJsonResponse({ success: true, message: '', data: body ?? null }, init)
},
writable: true,
})
}
installApiEnvelopeFixtureAdapter()
class ResizeObserverStub implements ResizeObserver {
disconnect() {}
observe() {}
+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)
}