test(site): cover site maintenance dialogs (#562)

This commit is contained in:
InfinityPacer
2026-07-19 16:12:17 +08:00
committed by GitHub
parent 47f007480f
commit 0d094b01b5
6 changed files with 645 additions and 2 deletions

View File

@@ -0,0 +1,199 @@
import SiteAddEditDialog from '@/components/dialog/SiteAddEditDialog.vue'
import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue'
import { fireEvent, screen, waitFor } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import { createSite, createSiteDownloader } from '@tests/support/factories/site'
import {
addSiteHandler,
siteDetailsHandler,
siteDownloadersHandler,
updateSiteHandler,
} from '@tests/support/msw/handlers/site'
import { server } from '@tests/support/msw/server'
import { renderWithProviders } from '@tests/support/render'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
done: vi.fn(),
start: vi.fn(),
toastError: vi.fn(),
toastSuccess: vi.fn(),
}))
vi.mock('vue-toastification', () => ({
useToast: () => ({ error: mocks.toastError, success: mocks.toastSuccess }),
}))
vi.mock('@/api/nprogress', () => ({
configureNProgress: vi.fn(),
doneNProgress: mocks.done,
startNProgress: mocks.start,
}))
async function renderDialog(oper: 'add' | 'edit', siteid?: number) {
const events = { close: vi.fn(), save: vi.fn() }
const result = await renderWithProviders(SiteAddEditDialog, {
props: {
modelValue: true,
oper,
siteid,
onClose: events.close,
onSave: events.save,
},
global: { components: { VDialogCloseBtn: DialogCloseBtn } },
})
return { ...result, events }
}
describe('SiteAddEditDialog', () => {
beforeEach(() => {
server.use(siteDownloadersHandler())
})
it('creates a site with the entered form values and loaded downloader', async () => {
const saved = vi.fn()
server.use(
siteDownloadersHandler([createSiteDownloader({ name: '下载器 A' })]),
addSiteHandler({ success: true }, 200, saved),
)
const user = userEvent.setup()
const { events } = await renderDialog('add')
await user.type(screen.getByLabelText('站点地址'), 'https://new.example.com/')
await fireEvent.update(screen.getByLabelText('RSS地址'), 'https://new.example.com/rss')
await fireEvent.update(screen.getByLabelText('超时时间(秒)'), '30')
await fireEvent.update(screen.getByLabelText('站点Cookie'), 'session=test')
await fireEvent.update(screen.getByLabelText('站点User-Agent'), 'Goal5C-UA')
await user.click(screen.getByRole('tab', { name: 'API' }))
await fireEvent.update(screen.getByLabelText('请求头Authorization'), 'Bearer goal5c')
await fireEvent.update(screen.getByLabelText('令牌API Key'), 'goal5c-key')
await user.click(screen.getByLabelText('下载器'))
await user.click(await screen.findByText('下载器 A'))
await user.click(screen.getByLabelText('限制站点访问频率'))
await fireEvent.update(screen.getByLabelText('单位周期(秒)'), '60')
await fireEvent.update(screen.getByLabelText('周期内访问次数'), '5')
await fireEvent.update(screen.getByLabelText('访问间隔(秒)'), '2')
await user.click(screen.getByLabelText('使用代理访问'))
await user.click(screen.getByLabelText('浏览器仿真'))
await user.click(screen.getByRole('button', { name: '新增站点' }))
await waitFor(() => expect(saved).toHaveBeenCalledOnce())
expect(saved.mock.calls[0][0]).toMatchObject({
apikey: 'goal5c-key',
cookie: 'session=test',
downloader: '下载器 A',
limit_count: '5',
limit_interval: '60',
limit_seconds: '2',
proxy: true,
render: true,
rss: 'https://new.example.com/rss',
timeout: '30',
token: 'Bearer goal5c',
ua: 'Goal5C-UA',
url: 'https://new.example.com/',
})
expect(events.save).toHaveBeenCalledOnce()
expect(mocks.toastSuccess).toHaveBeenCalledWith('新增站点成功')
expect(mocks.start).toHaveBeenCalledOnce()
expect(mocks.done).toHaveBeenCalledOnce()
})
it('does not submit an add request without a URL', async () => {
const saved = vi.fn()
server.use(addSiteHandler({ success: true }, 200, saved))
const { events } = await renderDialog('add')
await fireEvent.click(screen.getByRole('button', { name: '新增站点' }))
expect(saved).not.toHaveBeenCalled()
expect(events.save).not.toHaveBeenCalled()
expect(mocks.start).not.toHaveBeenCalled()
})
it('shows a business failure when creation is rejected', async () => {
server.use(addSiteHandler({ message: '站点已存在', success: false }))
const user = userEvent.setup()
const { events } = await renderDialog('add')
await user.type(screen.getByLabelText('站点地址'), 'https://duplicate.example.com/')
await user.click(screen.getByRole('button', { name: '新增站点' }))
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('新增站点失败:站点已存在'))
expect(events.save).not.toHaveBeenCalled()
expect(mocks.done).toHaveBeenCalledOnce()
})
it('restores progress and keeps the dialog open after an HTTP creation failure', async () => {
server.use(addSiteHandler({ message: '服务异常', success: false }, 500))
const user = userEvent.setup()
const { events } = await renderDialog('add')
await user.type(screen.getByLabelText('站点地址'), 'https://failed.example.com/')
await user.click(screen.getByRole('button', { name: '新增站点' }))
await waitFor(() => expect(mocks.done).toHaveBeenCalledOnce())
expect(events.save).not.toHaveBeenCalled()
expect(mocks.toastSuccess).not.toHaveBeenCalled()
})
it('restores numeric flags and API mode when editing a rate-limited site', async () => {
const site = createSite({
apikey: 'api-key',
downloader: '下载器 A',
limit_count: 4,
limit_interval: 60,
limit_seconds: 2,
proxy: 1,
render: 0,
token: 'token',
})
server.use(siteDetailsHandler(site.id, site), siteDownloadersHandler([createSiteDownloader({ name: '下载器 A' })]))
await renderDialog('edit', site.id)
expect(await screen.findByText(site.name)).toBeInTheDocument()
expect(screen.getByLabelText('使用代理访问')).toBeChecked()
expect(screen.getByLabelText('浏览器仿真')).not.toBeChecked()
expect(screen.getByLabelText('限制站点访问频率')).toBeChecked()
expect(screen.getByDisplayValue('api-key')).toBeInTheDocument()
})
it('clears rate limits when disabled and emits save after an update', async () => {
const site = createSite({ limit_count: 4, limit_interval: 60, limit_seconds: 2, name: '限流站点' })
const saved = vi.fn()
server.use(siteDetailsHandler(site.id, site), updateSiteHandler({ success: true }, 200, saved))
const user = userEvent.setup()
const { events } = await renderDialog('edit', site.id)
await user.click(await screen.findByLabelText('限制站点访问频率'))
await user.click(screen.getByRole('button', { name: '保存' }))
await waitFor(() => expect(saved).toHaveBeenCalledOnce())
expect(saved.mock.calls[0][0]).toMatchObject({ limit_count: 0, limit_interval: 0, limit_seconds: 0 })
expect(events.save).toHaveBeenCalledOnce()
expect(mocks.toastSuccess).toHaveBeenCalledWith('限流站点 更新成功')
})
it.each([
['business', 200, { message: '不允许更新', success: false }, '限流站点 更新失败:不允许更新'],
['HTTP', 500, { message: '服务异常', success: false }, '限流站点 更新失败!'],
])('keeps the dialog open on %s update failure', async (_case, status, response, message) => {
const site = createSite({ name: '限流站点' })
server.use(siteDetailsHandler(site.id, site), updateSiteHandler(response, status))
const { events } = await renderDialog('edit', site.id)
await fireEvent.click(await screen.findByRole('button', { name: '保存' }))
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith(message))
expect(events.save).not.toHaveBeenCalled()
expect(mocks.done).toHaveBeenCalledOnce()
})
it('emits close from the dialog close button', async () => {
const { events } = await renderDialog('add')
await fireEvent.click(document.querySelector('.absolute.right-3.top-3')!)
expect(events.close).toHaveBeenCalledOnce()
})
})

View File

@@ -0,0 +1,128 @@
import SiteCookieUpdateDialog from '@/components/dialog/SiteCookieUpdateDialog.vue'
import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue'
import { fireEvent, screen, waitFor } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import { createSite } from '@tests/support/factories/site'
import { updateSiteCookieHandler } 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 { describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({ toastError: vi.fn(), toastSuccess: vi.fn() }))
vi.mock('vue-toastification', () => ({
useToast: () => ({ error: mocks.toastError, success: mocks.toastSuccess }),
}))
async function renderDialog() {
const site = createSite({ id: 701, name: 'Cookie 站点' })
const events = { close: vi.fn(), done: vi.fn() }
const result = await renderWithProviders(SiteCookieUpdateDialog, {
props: { modelValue: true, site, onClose: events.close, onDone: events.done },
global: {
components: { VDialogCloseBtn: DialogCloseBtn },
stubs: { ProgressDialog: { props: ['text'], template: '<div role="status">{{ text }}</div>' } },
},
})
return { ...result, events, site }
}
async function fillCredentials(user: ReturnType<typeof userEvent.setup>) {
await user.type(screen.getByLabelText('用户名'), 'tester')
await user.type(screen.getByLabelText('密码'), 'secret')
await user.type(screen.getByLabelText('验证码'), '654321')
}
describe('SiteCookieUpdateDialog', () => {
it('skips requests until username and password are both provided', async () => {
const requested = vi.fn()
server.use(updateSiteCookieHandler(701, { success: true }, 200, requested))
const { events } = await renderDialog()
await fireEvent.click(screen.getByRole('button', { name: '开始更新' }))
expect(requested).not.toHaveBeenCalled()
expect(events.done).not.toHaveBeenCalled()
})
it('submits username, password, and OTP and emits done on success', async () => {
const requested = vi.fn()
server.use(updateSiteCookieHandler(701, { success: true }, 200, requested))
const user = userEvent.setup()
const { events } = await renderDialog()
await fillCredentials(user)
await user.click(screen.getByRole('button', { name: '开始更新' }))
await waitFor(() =>
expect(requested).toHaveBeenCalledWith({ code: '654321', password: 'secret', username: 'tester' }),
)
expect(events.done).toHaveBeenCalledOnce()
expect(mocks.toastSuccess).toHaveBeenCalledWith('Cookie 站点 更新Cookie & UA成功')
expect(screen.getByRole('button', { name: '开始更新' })).toBeEnabled()
})
it('toggles password visibility', async () => {
const user = userEvent.setup()
await renderDialog()
const password = screen.getByLabelText('密码')
const toggle = password.closest('.v-input')!.querySelector('.v-field__append-inner [role="button"]')!
expect(password).toHaveAttribute('type', 'password')
await user.click(toggle)
expect(password).toHaveAttribute('type', 'text')
})
it('shows the backend message for a business failure', async () => {
server.use(updateSiteCookieHandler(701, { message: '验证码错误', success: false }))
const user = userEvent.setup()
const { events } = await renderDialog()
await fillCredentials(user)
await user.click(screen.getByRole('button', { name: '开始更新' }))
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('Cookie 站点 更新失败:验证码错误'))
expect(events.done).not.toHaveBeenCalled()
})
it.each([
['detail', { detail: '站点不存在' }, '站点不存在'],
['message', { message: '认证服务异常' }, '认证服务异常'],
])('shows an HTTP %s response and restores pending state', async (_case, body, message) => {
server.use(http.post('http://localhost/api/v1/site/cookie/701', () => HttpResponse.json(body, { status: 500 })))
const user = userEvent.setup()
await renderDialog()
await fillCredentials(user)
await user.click(screen.getByRole('button', { name: '开始更新' }))
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith(`Cookie 站点 更新失败:${message}`))
expect(screen.getByRole('button', { name: '开始更新' })).toBeEnabled()
expect(screen.queryByRole('status')).not.toBeInTheDocument()
})
it('disables the update action while the request is pending and restores it afterward', async () => {
let resolveRequest!: () => void
const pending = new Promise<void>(resolve => {
resolveRequest = resolve
})
server.use(updateSiteCookieHandler(701, { success: true }, 200, () => pending))
const user = userEvent.setup()
await renderDialog()
await fillCredentials(user)
await user.click(screen.getByRole('button', { name: '开始更新' }))
expect(await screen.findByRole('status')).toHaveTextContent('正在更新 Cookie 站点 Cookie & UA...')
expect(screen.getByRole('button', { name: '开始更新' })).toBeDisabled()
resolveRequest()
await waitFor(() => expect(screen.getByRole('button', { name: '开始更新' })).toBeEnabled())
})
it('emits close from the dialog close button', async () => {
const { events } = await renderDialog()
await fireEvent.click(document.querySelector('.absolute.right-3.top-3')!)
expect(events.close).toHaveBeenCalledOnce()
})
})

View File

@@ -0,0 +1,217 @@
import SiteImportDialog from '@/components/dialog/SiteImportDialog.vue'
import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue'
import { fireEvent, screen, waitFor } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import { createSite } from '@tests/support/factories/site'
import { addSiteHandler, siteApiUrls } 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 { describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
done: vi.fn(),
start: vi.fn(),
toastError: vi.fn(),
toastSuccess: vi.fn(),
toastWarning: vi.fn(),
}))
vi.mock('vue-toastification', () => ({
useToast: () => ({ error: mocks.toastError, success: mocks.toastSuccess, warning: mocks.toastWarning }),
}))
vi.mock('@/api/nprogress', () => ({
configureNProgress: vi.fn(),
doneNProgress: mocks.done,
startNProgress: mocks.start,
}))
async function renderDialog() {
const events = { importSuccess: vi.fn(), update: vi.fn() }
const result = await renderWithProviders(SiteImportDialog, {
props: {
modelValue: true,
'onImport-success': events.importSuccess,
'onUpdate:modelValue': events.update,
},
global: { components: { VDialogCloseBtn: DialogCloseBtn } },
})
return { ...result, events }
}
function jsonFile(data: unknown, name = 'sites.json') {
const file = new File([JSON.stringify(data)], name, { type: 'application/json' })
Object.defineProperty(file, 'text', { value: async () => JSON.stringify(data) })
return file
}
function brokenJsonFile() {
const file = new File(['{broken'], 'broken.json', { type: 'application/json' })
Object.defineProperty(file, 'text', { value: async () => '{broken' })
return file
}
async function chooseFile(file: File) {
await userEvent.setup().upload(screen.getByLabelText('选择文件'), file)
}
describe('SiteImportDialog', () => {
it('previews only the first five sites and marks invalid rows', async () => {
const sites = Array.from({ length: 6 }, (_, index) =>
createSite(index === 1 ? { name: '' } : { name: `导入站点 ${index + 1}` }),
)
await renderDialog()
await chooseFile(jsonFile(sites))
expect(await screen.findByText('预览数据 (6 个站点)')).toBeInTheDocument()
expect(screen.getByText('导入站点 1')).toBeInTheDocument()
expect(screen.getByText('未命名站点')).toBeInTheDocument()
expect(screen.getByText('导入站点 5')).toBeInTheDocument()
expect(screen.queryByText('导入站点 6')).not.toBeInTheDocument()
expect(screen.getByText('数据无效')).toBeInTheDocument()
})
it('rejects a non-JSON drop before parsing', async () => {
await renderDialog()
const file = new File(['text'], 'sites.txt', { type: 'text/plain' })
await fireEvent.drop(document.querySelector('.upload-zone')!, { dataTransfer: { files: [file] } })
expect(mocks.toastError).toHaveBeenCalledWith('不支持的文件类型请选择JSON文件')
expect(screen.getByLabelText('选择文件')).toBeInTheDocument()
})
it.each([
['invalid JSON', brokenJsonFile(), '文件解析失败,请检查文件格式'],
['non-array JSON', jsonFile({ name: 'single' }), '文件格式无效,请检查文件内容'],
])('rejects %s', async (_case, file, message) => {
await renderDialog()
await chooseFile(file)
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith(message))
expect(screen.getByLabelText('选择文件')).toBeInTheDocument()
})
it('tracks drag state and can reset a selected batch', async () => {
await renderDialog()
const zone = document.querySelector('.upload-zone')!
await fireEvent.dragOver(zone)
expect(zone).toHaveClass('dragging')
await fireEvent.dragLeave(zone)
expect(zone).not.toHaveClass('dragging')
await fireEvent.drop(zone, { dataTransfer: { files: [jsonFile([createSite({ name: '拖入站点' })])] } })
expect(await screen.findByText('拖入站点')).toBeInTheDocument()
await fireEvent.click(screen.getByRole('button', { name: '重置' }))
expect(screen.getByLabelText('选择文件')).toBeInTheDocument()
})
it('imports valid sites sequentially without their ids and closes on complete success', async () => {
const sites = [createSite({ id: 801, name: '第一站' }), createSite({ id: 802, name: '第二站' })]
const order: Array<Record<string, unknown>> = []
server.use(
addSiteHandler({ success: true }, 200, payload => {
order.push(payload as unknown as Record<string, unknown>)
}),
)
const user = userEvent.setup()
const { events } = await renderDialog()
await chooseFile(jsonFile(sites))
await user.click(await screen.findByRole('button', { name: '开始导入' }))
await waitFor(() => expect(order).toHaveLength(2))
expect(order.map(site => site.name)).toEqual(['第一站', '第二站'])
expect(order.every(site => !('id' in site))).toBe(true)
expect(mocks.toastSuccess).toHaveBeenCalledWith('成功导入 2 个站点')
expect(events.importSuccess).toHaveBeenCalledOnce()
expect(events.update).toHaveBeenCalledWith(false)
expect(mocks.start).toHaveBeenCalledOnce()
expect(mocks.done).toHaveBeenCalledOnce()
})
it('warns about invalid records and imports only valid records', async () => {
const valid = createSite({ name: '有效站点' })
const invalid = createSite({ domain: '', name: '无效站点' })
const requested = vi.fn()
server.use(addSiteHandler({ success: true }, 200, requested))
await renderDialog()
await chooseFile(jsonFile([valid, invalid]))
await fireEvent.click(await screen.findByRole('button', { name: '开始导入' }))
await waitFor(() => expect(requested).toHaveBeenCalledOnce())
expect(mocks.toastWarning).toHaveBeenCalledWith('部分数据无效,有效数据 1/2 个')
})
it('keeps all business failures visible with backend details', async () => {
const sites = [createSite({ name: '重复站点' }), createSite({ name: '未支持站点' })]
let requestIndex = 0
server.use(
addSiteHandler({ success: false }, 200, () => {
requestIndex += 1
}),
)
await renderDialog()
await chooseFile(jsonFile(sites))
await fireEvent.click(await screen.findByRole('button', { name: '开始导入' }))
expect(await screen.findByText('导入过程中出现 2 个错误')).toBeInTheDocument()
expect(requestIndex).toBe(2)
expect(mocks.toastError).toHaveBeenCalledWith('导入失败2 个站点全部导入失败')
expect(screen.getByText('重复站点 - 错误详情')).toBeInTheDocument()
expect(screen.getByText('未支持站点 - 错误详情')).toBeInTheDocument()
})
it('reports partial success and preserves the HTTP error message', async () => {
const sites = [createSite({ name: '成功站点' }), createSite({ name: '失败站点' })]
let requestIndex = 0
server.use(
http.post(siteApiUrls.list, () => {
requestIndex += 1
if (requestIndex === 1) return HttpResponse.json({ success: true })
return HttpResponse.json({ message: '第二站请求失败', success: false }, { status: 500 })
}),
)
await renderDialog()
await chooseFile(jsonFile(sites))
await fireEvent.click(await screen.findByRole('button', { name: '开始导入' }))
expect(await screen.findByText('导入过程中出现 1 个错误')).toBeInTheDocument()
expect(screen.getByText('成功导入 1 个站点')).toBeInTheDocument()
expect(mocks.toastError).toHaveBeenCalledWith('导入完成,成功 1 个,失败 1 个')
await fireEvent.click(screen.getByText('失败站点 - 错误详情'))
expect(await screen.findByText('Request failed with status code 500')).toBeInTheDocument()
})
it('does not start a request when every record is invalid', async () => {
const requested = vi.fn()
server.use(addSiteHandler({ success: true }, 200, requested))
await renderDialog()
await chooseFile(jsonFile([createSite({ domain: '', name: '', url: '' })]))
await fireEvent.click(await screen.findByRole('button', { name: '开始导入' }))
expect(mocks.toastError).toHaveBeenCalledWith('没有有效的数据')
expect(requested).not.toHaveBeenCalled()
expect(mocks.start).not.toHaveBeenCalled()
})
it('closes a failed result without a success event', async () => {
server.use(addSiteHandler({ success: false, message: '已存在' }))
const { events } = await renderDialog()
await chooseFile(jsonFile([createSite({ name: '失败站点' })]))
await fireEvent.click(await screen.findByRole('button', { name: '开始导入' }))
expect(await screen.findByText('导入过程中出现 1 个错误')).toBeInTheDocument()
await fireEvent.click(screen.getByRole('button', { name: '关闭' }))
expect(events.update).toHaveBeenCalledWith(false)
expect(events.importSuccess).not.toHaveBeenCalled()
})
})

View File

@@ -1,4 +1,4 @@
import type { Site, SiteStatistic, SiteUserData } from '@/api/types'
import type { DownloaderConf, Site, SiteStatistic, SiteUserData } from '@/api/types'
let siteSeed = 0
@@ -37,3 +37,15 @@ export function createSiteUserData(overrides: Partial<SiteUserData> = {}): SiteU
...overrides,
}
}
/** 构造可用的下载器配置测试记录。 */
export function createSiteDownloader(overrides: Partial<DownloaderConf> = {}): DownloaderConf {
return {
config: {},
default: false,
enabled: true,
name: '测试下载器',
type: 'qbittorrent',
...overrides,
}
}

View File

@@ -1,10 +1,19 @@
import type { ApiResponse, Site, SiteStatistic, SiteUserData } from '@/api/types'
import type { ApiResponse, DownloaderConf, Site, SiteStatistic, SiteUserData } from '@/api/types'
import { HttpResponse, http, type JsonBodyType } from 'msw'
const API_BASE_URL = 'http://localhost/api/v1/'
interface SiteMutationResponse {
success: boolean
data?: Record<string, unknown>
message?: string
}
export const siteApiUrls = {
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,
downloaders: new URL('download/clients', API_BASE_URL).href,
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,
@@ -114,3 +123,60 @@ export function deleteSiteHandler(
return response(result, status)
})
}
export function siteDownloadersHandler(
response: DownloaderConf[] = [],
status = 200,
onRequest: () => void | Promise<void> = () => {},
) {
return http.get(siteApiUrls.downloaders, async () => {
await onRequest()
return HttpResponse.json(response as unknown as JsonBodyType, { status })
})
}
export function siteDetailsHandler(
id: number,
response: Site,
status = 200,
onRequest: () => void | Promise<void> = () => {},
) {
return http.get(siteApiUrls.details(id), async () => {
await onRequest()
return HttpResponse.json(response as unknown as JsonBodyType, { status })
})
}
export function addSiteHandler(
response: SiteMutationResponse = { success: true },
status = 200,
onRequest: (payload: Site) => void | Promise<void> = () => {},
) {
return http.post(siteApiUrls.list, async ({ request }) => {
await onRequest((await request.json()) as Site)
return HttpResponse.json(response, { status })
})
}
export function updateSiteHandler(
response: SiteMutationResponse = { success: true },
status = 200,
onRequest: (payload: Site) => void | Promise<void> = () => {},
) {
return http.put(siteApiUrls.list, async ({ request }) => {
await onRequest((await request.json()) as Site)
return HttpResponse.json(response, { status })
})
}
export function updateSiteCookieHandler(
id: number,
response: SiteMutationResponse = { success: true },
status = 200,
onRequest: (payload: { code: string; password: string; username: string }) => void | Promise<void> = () => {},
) {
return http.post(siteApiUrls.cookie(id), async ({ request }) => {
await onRequest((await request.json()) as { code: string; password: string; username: string })
return HttpResponse.json(response, { status })
})
}

View File

@@ -289,6 +289,9 @@ export default defineConfig(({ mode }) => ({
'src/views/subscribe/SubscribeShareView.vue',
'src/composables/useMediaSubscribe.ts',
'src/components/cards/SubscribeCard.vue',
'src/components/dialog/SiteAddEditDialog.vue',
'src/components/dialog/SiteCookieUpdateDialog.vue',
'src/components/dialog/SiteImportDialog.vue',
'src/components/cards/SubscribeShareCard.vue',
'src/components/dialog/ForkSubscribeDialog.vue',
'src/components/dialog/SubscribeEditDialog.vue',
@@ -338,6 +341,24 @@ export default defineConfig(({ mode }) => ({
lines: 80,
statements: 80,
},
'src/components/dialog/SiteAddEditDialog.vue': {
branches: 75,
functions: 80,
lines: 80,
statements: 80,
},
'src/components/dialog/SiteCookieUpdateDialog.vue': {
branches: 75,
functions: 80,
lines: 80,
statements: 80,
},
'src/components/dialog/SiteImportDialog.vue': {
branches: 85,
functions: 90,
lines: 90,
statements: 90,
},
'src/components/dialog/SubscribeEditDialog.vue': {
branches: 75,
functions: 80,