mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-08 09:16:58 +08:00
test(site): cover site user data dialog (#564)
This commit is contained in:
@@ -385,11 +385,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/dialog/SiteUserDataDialog.vue": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"src/components/dialog/SmbConfigDialog.vue": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 2
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Site, SiteUserData } from '@/api/types'
|
||||
import type { ApiResponse, Site, SiteUserData } from '@/api/types'
|
||||
import api from '@/api'
|
||||
import { useDisplay, useTheme } from 'vuetify'
|
||||
import { formatFileSize } from '@/@core/utils/formatters'
|
||||
@@ -23,6 +23,9 @@ const emit = defineEmits(['close'])
|
||||
// 进度框
|
||||
const progressDialog = ref(false)
|
||||
|
||||
// 失败状态保留对应操作,重试时不改变已展示的站点数据。
|
||||
const failedOperation = ref<'load' | 'refresh'>()
|
||||
|
||||
const vuetifyTheme = useTheme()
|
||||
|
||||
const currentTheme = controlledComputed(
|
||||
@@ -33,6 +36,9 @@ const currentTheme = controlledComputed(
|
||||
// 站点数据列表
|
||||
const siteDatas = ref<SiteUserData[]>([])
|
||||
|
||||
// 只有最近一次读取可以更新弹窗状态,避免首载慢响应覆盖手动刷新结果。
|
||||
let fetchGeneration = 0
|
||||
|
||||
// 最新一天的数据
|
||||
const siteData = computed(() => siteDatas.value[siteDatas.value.length - 1])
|
||||
|
||||
@@ -58,8 +64,8 @@ const historyChartOptions = computed(() => {
|
||||
parentHeightOffset: 0,
|
||||
toolbar: { show: false },
|
||||
animations: { enabled: true },
|
||||
background: currentTheme.value.surface, // 新增背景色同步
|
||||
foreColor: currentTheme.value.onSurface, // 新增文字颜色同步
|
||||
background: currentTheme.value.surface, // 图表背景随应用主题切换
|
||||
foreColor: currentTheme.value.onSurface, // 图表文字随应用主题切换
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
},
|
||||
@@ -139,10 +145,12 @@ const historyChartOptions = computed(() => {
|
||||
|
||||
// 做种分布列,seeding_info的格式为[[x, y], [x, y], ...],x为做种数,y为做种体积,做种体积需要转换为GB
|
||||
const seedingSeries = computed(() => {
|
||||
const seedingInfo = siteData.value?.seeding_info as [number?, number?][] | undefined
|
||||
|
||||
return [
|
||||
{
|
||||
name: t('dialog.siteUserData.volumeTitle'),
|
||||
data: siteData.value?.seeding_info?.map(item => [item[0] ?? 0, Math.round((item[1] ?? 0) / 1024 / 1024 / 1024)]),
|
||||
data: seedingInfo?.map(item => [item[0] ?? 0, Math.round((item[1] ?? 0) / 1024 / 1024 / 1024)]) ?? [],
|
||||
},
|
||||
]
|
||||
})
|
||||
@@ -155,8 +163,8 @@ const seedingChartOptions = computed(() => {
|
||||
parentHeightOffset: 0,
|
||||
toolbar: { show: false },
|
||||
animations: { enabled: true },
|
||||
background: currentTheme.value.surface, // 新增背景色同步
|
||||
foreColor: currentTheme.value.onSurface, // 新增文字颜色同步
|
||||
background: currentTheme.value.surface, // 图表背景随应用主题切换
|
||||
foreColor: currentTheme.value.onSurface, // 图表文字随应用主题切换
|
||||
zoom: {
|
||||
enabled: false,
|
||||
allowMouseWheelZoom: false,
|
||||
@@ -214,7 +222,7 @@ const seedingChartOptions = computed(() => {
|
||||
})
|
||||
|
||||
// 根据传入属性,计算列表数据中第一条与第二条的差值,如果没有第二条则差值为全部
|
||||
const diffData: { [key: string]: any } = computed(() => {
|
||||
const diffData = computed(() => {
|
||||
if (siteDatas.value.length < 2) {
|
||||
return siteData.value
|
||||
}
|
||||
@@ -253,34 +261,58 @@ function getDiffClass(diff: number | undefined) {
|
||||
}
|
||||
|
||||
// 查询站点用户数据
|
||||
async function fetchSiteUserData() {
|
||||
async function fetchSiteUserData(failureOperation: 'load' | 'refresh' = 'load') {
|
||||
const generation = ++fetchGeneration
|
||||
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(`site/userdata/${props.site?.id}`)
|
||||
const result = await api.get<ApiResponse<SiteUserData[]>, ApiResponse<SiteUserData[]>>(
|
||||
`site/userdata/${props.site?.id}`,
|
||||
)
|
||||
if (generation !== fetchGeneration) return false
|
||||
|
||||
if (result.success) {
|
||||
// 使用nextTick确保DOM更新完成后再更新图表数据
|
||||
await nextTick()
|
||||
siteDatas.value = result.data.sort((a: { updated_day: any }, b: { updated_day: any }) =>
|
||||
(a.updated_day || '').localeCompare(b.updated_day || ''),
|
||||
)
|
||||
siteDatas.value = result.data.sort((a, b) => (a.updated_day || '').localeCompare(b.updated_day || ''))
|
||||
failedOperation.value = undefined
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
if (generation !== fetchGeneration) return false
|
||||
|
||||
console.error(error)
|
||||
failedOperation.value = failureOperation
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 刷新站点数据
|
||||
async function refreshSiteData() {
|
||||
progressDialog.value = true
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(`site/userdata/${props.site?.id}`)
|
||||
const result = await api.post<ApiResponse<unknown>, ApiResponse<unknown>>(`site/userdata/${props.site?.id}`)
|
||||
if (result.success) {
|
||||
await fetchSiteUserData()
|
||||
await fetchSiteUserData('refresh')
|
||||
} else {
|
||||
failedOperation.value = 'refresh'
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
console.error(error)
|
||||
failedOperation.value = 'refresh'
|
||||
} finally {
|
||||
progressDialog.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重试最近失败的请求,刷新失败时继续保留当前数据。
|
||||
function retryFailedOperation() {
|
||||
if (failedOperation.value === 'refresh') {
|
||||
refreshSiteData()
|
||||
} else {
|
||||
fetchSiteUserData()
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
// 延迟加载,确保组件完全挂载
|
||||
@@ -302,6 +334,20 @@ onBeforeMount(() => {
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText class="pt-5">
|
||||
<VAlert v-if="failedOperation" type="error" variant="tonal" class="mb-5">
|
||||
<div class="d-flex flex-wrap align-center justify-space-between gap-3">
|
||||
<span>
|
||||
{{
|
||||
failedOperation === 'refresh'
|
||||
? t('dialog.siteUserData.refreshFailed')
|
||||
: t('dialog.siteUserData.loadFailed')
|
||||
}}
|
||||
</span>
|
||||
<VBtn size="small" variant="tonal" prepend-icon="mdi-refresh" @click="retryFailedOperation">
|
||||
{{ t('common.retry') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VAlert>
|
||||
<VRow class="match-height">
|
||||
<!-- 用户信息 -->
|
||||
<VCol cols="12" md="3">
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
import SiteUserDataDialog from '@/components/dialog/SiteUserDataDialog.vue'
|
||||
import type { ApiResponse, SiteUserData } from '@/api/types'
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/vue'
|
||||
import { createSite, createSiteUserData } from '@tests/support/factories/site'
|
||||
import { refreshSiteUserDataHandler, siteUserDataHandler } from '@tests/support/msw/handlers/site'
|
||||
import { server } from '@tests/support/msw/server'
|
||||
import { renderWithProviders } from '@tests/support/render'
|
||||
import { getActiveRequestsCount } from '@/utils/requestOptimizer'
|
||||
import { flushPromises } from '@vue/test-utils'
|
||||
import { defineComponent, h, type PropType } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
type ChartInput = {
|
||||
options: Record<string, unknown>
|
||||
series: { data?: unknown[]; name?: string }[]
|
||||
type: string
|
||||
}
|
||||
|
||||
const chartInputs: ChartInput[] = []
|
||||
|
||||
const ApexChartStub = defineComponent({
|
||||
name: 'VApexChart',
|
||||
props: {
|
||||
options: { type: Object as PropType<Record<string, unknown>>, required: true },
|
||||
series: { type: Array as PropType<ChartInput['series']>, required: true },
|
||||
type: { type: String, required: true },
|
||||
},
|
||||
setup(props) {
|
||||
chartInputs.push(props)
|
||||
return () => h('div', { 'data-testid': `chart-${props.type}` })
|
||||
},
|
||||
})
|
||||
|
||||
const ProgressDialogStub = defineComponent({
|
||||
name: 'ProgressDialog',
|
||||
props: { text: String },
|
||||
setup: props => () => h('div', { 'data-testid': 'refresh-progress' }, props.text),
|
||||
})
|
||||
|
||||
const DialogCloseButtonStub = defineComponent({
|
||||
name: 'VDialogCloseBtn',
|
||||
setup:
|
||||
(_, { attrs }) =>
|
||||
() =>
|
||||
h('button', { ...attrs, 'aria-label': '关闭' }),
|
||||
})
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void
|
||||
const promise = new Promise<T>(resolvePromise => {
|
||||
resolve = resolvePromise
|
||||
})
|
||||
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
async function renderDialog(
|
||||
initialResult: Pick<ApiResponse<SiteUserData[]>, 'data' | 'message' | 'success'> = { data: [], success: false },
|
||||
status = 200,
|
||||
onRequest: () => void | Promise<void> = () => {},
|
||||
) {
|
||||
const site = createSite()
|
||||
server.use(siteUserDataHandler(site.id, initialResult, status, onRequest))
|
||||
const result = await renderWithProviders(SiteUserDataDialog, {
|
||||
global: {
|
||||
stubs: {
|
||||
ProgressDialog: ProgressDialogStub,
|
||||
VApexChart: ApexChartStub,
|
||||
VDialogCloseBtn: DialogCloseButtonStub,
|
||||
},
|
||||
},
|
||||
props: { site },
|
||||
})
|
||||
return { ...result, site }
|
||||
}
|
||||
|
||||
function getChart(type: string) {
|
||||
const chart = chartInputs.find(input => input.type === type)
|
||||
if (!chart) throw new Error(`Missing ${type} chart input`)
|
||||
return chart
|
||||
}
|
||||
|
||||
function getRefreshButton() {
|
||||
const button = document.querySelector<HTMLButtonElement>('.v-card-title button')
|
||||
if (!button) throw new Error('Missing refresh button')
|
||||
return button
|
||||
}
|
||||
|
||||
function getRetryButton() {
|
||||
const button = Array.from(document.querySelectorAll<HTMLButtonElement>('.v-alert button')).find(element =>
|
||||
element.textContent?.includes('重试'),
|
||||
)
|
||||
if (!button) throw new Error('Missing retry button')
|
||||
return button
|
||||
}
|
||||
|
||||
describe('SiteUserDataDialog projections', () => {
|
||||
beforeEach(() => {
|
||||
chartInputs.length = 0
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
it('sorts history, renders the latest record, and projects positive, negative, and zero deltas', async () => {
|
||||
const older = createSiteUserData({
|
||||
bonus: 100,
|
||||
download: 4 * 1024 ** 3,
|
||||
ratio: 1.5,
|
||||
seeding: 2,
|
||||
seeding_size: 3 * 1024 ** 3,
|
||||
updated_day: '2026-07-17',
|
||||
upload: 1024 ** 3,
|
||||
user_level: 'Old',
|
||||
})
|
||||
const latest = createSiteUserData({
|
||||
bonus: 150,
|
||||
download: 2 * 1024 ** 3,
|
||||
ratio: 1.25,
|
||||
seeding: 2,
|
||||
seeding_info: [
|
||||
[10, 2 * 1024 ** 3],
|
||||
[0, 0],
|
||||
],
|
||||
seeding_size: 4 * 1024 ** 3,
|
||||
updated_day: '2026-07-19',
|
||||
upload: 3 * 1024 ** 3,
|
||||
user_level: 'Elite',
|
||||
})
|
||||
await renderDialog({ data: [latest, older], success: true })
|
||||
|
||||
await screen.findByText('Elite')
|
||||
|
||||
expect(screen.getByText('150')).toBeInTheDocument()
|
||||
expect(screen.getByText('(+50)')).toHaveClass('text-success')
|
||||
expect(screen.getByText('(-0.25)')).toHaveClass('text-error')
|
||||
expect(screen.getByText('(+0)')).not.toHaveClass('text-success', 'text-error')
|
||||
expect(screen.getByText('3.00 GB')).toBeInTheDocument()
|
||||
expect(screen.getByText('(+2.00 GB)')).toHaveClass('text-success')
|
||||
expect(screen.getByText('2.00 GB')).toBeInTheDocument()
|
||||
expect(screen.getByText('(-2.00 GB)')).toHaveClass('text-error')
|
||||
expect(document.body).toHaveTextContent('2024-01-02')
|
||||
|
||||
const history = getChart('line')
|
||||
expect(history.series).toEqual([
|
||||
{ data: [1, 3], name: '上传量' },
|
||||
{ data: [4, 2], name: '下载量' },
|
||||
])
|
||||
expect((history.options.xaxis as { categories: string[] }).categories).toEqual(['2026-07-17', '2026-07-19'])
|
||||
expect((history.options.theme as { mode: string }).mode).toBe('light')
|
||||
expect((history.options.chart as { background: string; foreColor: string }).background).toBeTruthy()
|
||||
expect(
|
||||
(history.options.xaxis as { labels: { formatter: (value: string) => string } }).labels.formatter('2026-07-19'),
|
||||
).toBe(new Date('2026-07-19').toLocaleDateString('zh-CN'))
|
||||
expect((history.options.yaxis as { labels: { formatter: (value: number) => string } }).labels.formatter(1234)).toBe(
|
||||
(1234).toLocaleString(),
|
||||
)
|
||||
|
||||
const seeding = getChart('scatter')
|
||||
expect(seeding.series).toEqual([
|
||||
{
|
||||
data: [
|
||||
[10, 2],
|
||||
[0, 0],
|
||||
],
|
||||
name: '体积',
|
||||
},
|
||||
])
|
||||
expect((seeding.options.theme as { mode: string }).mode).toBe('light')
|
||||
expect((seeding.options.tooltip as { x: { formatter: (value: number) => string } }).x.formatter(1234)).toBe(
|
||||
`数量:${(1234).toLocaleString()}`,
|
||||
)
|
||||
expect((seeding.options.xaxis as { labels: { formatter: (value: number) => string } }).labels.formatter(1.6)).toBe(
|
||||
'2',
|
||||
)
|
||||
expect((seeding.options.yaxis as { labels: { formatter: (value: number) => string } }).labels.formatter(2048)).toBe(
|
||||
`${(2048).toLocaleString()} GB`,
|
||||
)
|
||||
})
|
||||
|
||||
it('uses the only record as both latest data and the baseline delta', async () => {
|
||||
const only = createSiteUserData({ bonus: 42, ratio: 0, seeding: 5, updated_day: '2026-07-18' })
|
||||
await renderDialog({ data: [only], success: true })
|
||||
|
||||
await screen.findByText('42')
|
||||
|
||||
expect(screen.getByText('(+42)')).toHaveClass('text-success')
|
||||
expect(screen.getByText('(+0)')).not.toHaveClass('text-success', 'text-error')
|
||||
expect(screen.getByText('(+5)')).toHaveClass('text-success')
|
||||
})
|
||||
|
||||
it('normalizes missing historical counters and seeding tuple values to zero', async () => {
|
||||
const sparseRecord = () =>
|
||||
createSiteUserData({
|
||||
bonus: undefined,
|
||||
download: undefined,
|
||||
ratio: undefined,
|
||||
seeding: undefined,
|
||||
seeding_info: [[undefined, undefined]],
|
||||
seeding_size: undefined,
|
||||
updated_day: undefined,
|
||||
upload: undefined,
|
||||
})
|
||||
await renderDialog({ data: [sparseRecord(), sparseRecord()], success: true })
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getChart('line').series).toEqual([
|
||||
{ data: [0, 0], name: '上传量' },
|
||||
{ data: [0, 0], name: '下载量' },
|
||||
]),
|
||||
)
|
||||
expect(getChart('scatter').series).toEqual([{ data: [[0, 0]], name: '体积' }])
|
||||
})
|
||||
|
||||
it('keeps a legal empty response renderable without fabricated values', async () => {
|
||||
const requested = vi.fn()
|
||||
await renderDialog({ data: [], success: false }, 200, requested)
|
||||
|
||||
await waitFor(() => expect(requested).toHaveBeenCalledOnce())
|
||||
await waitFor(() => expect(getActiveRequestsCount()).toBe(0))
|
||||
expect(getChart('line').series).toEqual([
|
||||
{ data: [], name: '上传量' },
|
||||
{ data: [], name: '下载量' },
|
||||
])
|
||||
expect(screen.getByText('无')).toBeInTheDocument()
|
||||
expect(getChart('scatter').series).toEqual([{ data: [], name: '体积' }])
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('SiteUserDataDialog refresh and recovery', () => {
|
||||
beforeEach(() => {
|
||||
chartInputs.length = 0
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
it('reloads history after a successful refresh and keeps progress until both requests finish', async () => {
|
||||
const initial = createSiteUserData({ bonus: 1 })
|
||||
const refreshed = createSiteUserData({ bonus: 222, updated_day: '2026-07-19' })
|
||||
const refreshRequest = deferred<void>()
|
||||
let loadCount = 0
|
||||
const { site } = await renderDialog({ data: [initial], success: true }, 200, () => {
|
||||
loadCount += 1
|
||||
})
|
||||
server.use(refreshSiteUserDataHandler(site.id, { data: {}, success: true }, 200, () => refreshRequest.promise))
|
||||
await screen.findByText('1')
|
||||
|
||||
await fireEvent.click(getRefreshButton())
|
||||
expect(screen.getByTestId('refresh-progress')).toHaveTextContent('正在刷新站点数据...')
|
||||
server.use(
|
||||
siteUserDataHandler(site.id, { data: [refreshed], success: true }, 200, () => {
|
||||
loadCount += 1
|
||||
}),
|
||||
)
|
||||
refreshRequest.resolve()
|
||||
|
||||
await screen.findByText('222')
|
||||
await waitFor(() => expect(screen.queryByTestId('refresh-progress')).not.toBeInTheDocument())
|
||||
expect(loadCount).toBe(2)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['late success', 200, { data: [createSiteUserData({ bonus: 1 })] as SiteUserData[], success: true }],
|
||||
['late HTTP failure', 500, { data: [] as SiteUserData[], message: 'old request failed', success: false }],
|
||||
] as const)('ignores an initial request that settles after refreshed data on %s', async (_case, status, response) => {
|
||||
const initialRequest = deferred<void>()
|
||||
const initialRequested = vi.fn()
|
||||
const initialReleased = vi.fn()
|
||||
const refreshed = createSiteUserData({ bonus: 222, updated_day: '2026-07-19' })
|
||||
const { site } = await renderDialog(response, status, async () => {
|
||||
initialRequested()
|
||||
await initialRequest.promise
|
||||
initialReleased()
|
||||
})
|
||||
await waitFor(() => expect(initialRequested).toHaveBeenCalledOnce())
|
||||
server.use(
|
||||
refreshSiteUserDataHandler(site.id, { data: {}, success: true }),
|
||||
siteUserDataHandler(site.id, { data: [refreshed], success: true }),
|
||||
)
|
||||
|
||||
await fireEvent.click(getRefreshButton())
|
||||
await screen.findByText('222')
|
||||
|
||||
initialRequest.resolve()
|
||||
await waitFor(() => expect(initialReleased).toHaveBeenCalledOnce())
|
||||
await waitFor(() => expect(getActiveRequestsCount()).toBe(0))
|
||||
await flushPromises()
|
||||
expect(screen.getByText('222')).toBeInTheDocument()
|
||||
expect(screen.queryByText('1')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['business failure', 200, { data: {}, message: '站点不支持刷新', success: false }],
|
||||
['HTTP failure', 500, { data: {}, message: 'server down', success: false }],
|
||||
] as const)('ends progress and exposes a retry after refresh %s', async (_case, status, response) => {
|
||||
const refreshRequested = vi.fn()
|
||||
const { site } = await renderDialog({ data: [createSiteUserData()], success: true })
|
||||
server.use(refreshSiteUserDataHandler(site.id, response, status, refreshRequested))
|
||||
await screen.findByText('Elite')
|
||||
|
||||
await fireEvent.click(getRefreshButton())
|
||||
|
||||
await waitFor(() => expect(refreshRequested).toHaveBeenCalledOnce())
|
||||
await waitFor(() => expect(screen.queryByTestId('refresh-progress')).not.toBeInTheDocument())
|
||||
expect(await screen.findByText(/刷新站点数据失败/)).toBeInTheDocument()
|
||||
expect(getRetryButton()).toBeInTheDocument()
|
||||
|
||||
server.use(refreshSiteUserDataHandler(site.id, { data: {}, success: true }, 200, refreshRequested))
|
||||
await fireEvent.click(getRetryButton())
|
||||
|
||||
await waitFor(() => expect(refreshRequested).toHaveBeenCalledTimes(2))
|
||||
await waitFor(() => expect(screen.queryByText(/刷新站点数据失败/)).not.toBeInTheDocument())
|
||||
})
|
||||
|
||||
it('distinguishes initial HTTP failure from empty data and retries the same site', async () => {
|
||||
let attempt = 0
|
||||
const { site } = await renderDialog({ data: [], success: false }, 500, () => {
|
||||
attempt += 1
|
||||
})
|
||||
|
||||
expect(await screen.findByText(/加载站点数据失败/)).toBeInTheDocument()
|
||||
server.use(
|
||||
siteUserDataHandler(site.id, { data: [createSiteUserData({ bonus: 88 })], success: true }, 200, () => {
|
||||
attempt += 1
|
||||
}),
|
||||
)
|
||||
await fireEvent.click(getRetryButton())
|
||||
|
||||
await screen.findByText('88')
|
||||
expect(attempt).toBe(2)
|
||||
expect(screen.queryByText(/加载站点数据失败/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('emits close without mutating loaded user data', async () => {
|
||||
const { emitted } = await renderDialog({ data: [createSiteUserData()], success: true })
|
||||
await screen.findByText('Elite')
|
||||
|
||||
const closeButton = document.querySelector<HTMLButtonElement>('button[aria-label="关闭"]')
|
||||
if (!closeButton) throw new Error('Missing close button')
|
||||
await fireEvent.click(closeButton)
|
||||
|
||||
expect(emitted('close')).toHaveLength(1)
|
||||
expect(screen.getByText('Elite')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -3254,6 +3254,8 @@ export default {
|
||||
countTitle: 'Count:',
|
||||
noData: 'None',
|
||||
refreshing: 'Refreshing site data...',
|
||||
loadFailed: 'Failed to load site data',
|
||||
refreshFailed: 'Failed to refresh site data',
|
||||
close: 'Close',
|
||||
},
|
||||
siteResource: {
|
||||
|
||||
@@ -3202,6 +3202,8 @@ export default {
|
||||
countTitle: '数量:',
|
||||
noData: '无',
|
||||
refreshing: '正在刷新站点数据...',
|
||||
loadFailed: '加载站点数据失败',
|
||||
refreshFailed: '刷新站点数据失败',
|
||||
close: '关闭',
|
||||
},
|
||||
siteResource: {
|
||||
|
||||
@@ -3201,6 +3201,8 @@ export default {
|
||||
countTitle: '數量:',
|
||||
noData: '無',
|
||||
refreshing: '正在刷新站點數據...',
|
||||
loadFailed: '載入站點數據失敗',
|
||||
refreshFailed: '刷新站點數據失敗',
|
||||
close: '關閉',
|
||||
},
|
||||
siteResource: {
|
||||
|
||||
@@ -31,9 +31,14 @@ export function createSiteUserData(overrides: Partial<SiteUserData> = {}): SiteU
|
||||
return {
|
||||
bonus: 100,
|
||||
download: 1024,
|
||||
join_at: '2024-01-02 10:00:00',
|
||||
ratio: 2,
|
||||
seeding: 3,
|
||||
seeding_info: [],
|
||||
seeding_size: 4 * 1024 ** 3,
|
||||
updated_day: '2026-07-18',
|
||||
upload: 2048,
|
||||
user_level: 'Elite',
|
||||
username: 'site-user',
|
||||
...overrides,
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export const siteApiUrls = {
|
||||
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,
|
||||
userData: (id: number) => new URL(`site/userdata/${id}`, API_BASE_URL).href,
|
||||
userDataLatest: new URL('site/userdata/latest', API_BASE_URL).href,
|
||||
}
|
||||
|
||||
@@ -80,6 +81,30 @@ export function siteUserDataLatestHandler(
|
||||
})
|
||||
}
|
||||
|
||||
export function siteUserDataHandler(
|
||||
id: number,
|
||||
result: Pick<ApiResponse<SiteUserData[]>, 'data' | 'message' | 'success'>,
|
||||
status = 200,
|
||||
onRequest: () => void | Promise<void> = () => {},
|
||||
) {
|
||||
return http.get(siteApiUrls.userData(id), async () => {
|
||||
await onRequest()
|
||||
return response(result as unknown as JsonBodyType, status)
|
||||
})
|
||||
}
|
||||
|
||||
export function refreshSiteUserDataHandler(
|
||||
id: number,
|
||||
result: Pick<ApiResponse<Record<string, unknown>>, 'data' | 'message' | 'success'>,
|
||||
status = 200,
|
||||
onRequest: () => void | Promise<void> = () => {},
|
||||
) {
|
||||
return http.post(siteApiUrls.userData(id), async () => {
|
||||
await onRequest()
|
||||
return response(result as unknown as JsonBodyType, status)
|
||||
})
|
||||
}
|
||||
|
||||
export function saveSitePrioritiesHandler(
|
||||
onSave: (priorities: Array<{ id: number; pri: number }>) => void | Promise<void> = () => {},
|
||||
options: { status?: number; success?: boolean } = {},
|
||||
|
||||
@@ -302,6 +302,7 @@ export default defineConfig(({ mode }) => ({
|
||||
'src/components/dialog/SubscribeShareStatisticsDialog.vue',
|
||||
'src/components/dialog/DiscoverTabOrderDialog.vue',
|
||||
'src/components/dialog/SiteResourceDialog.vue',
|
||||
'src/components/dialog/SiteUserDataDialog.vue',
|
||||
'src/views/discover/TheMovieDbView.vue',
|
||||
'src/views/discover/DoubanView.vue',
|
||||
'src/views/discover/BangumiView.vue',
|
||||
@@ -492,6 +493,12 @@ export default defineConfig(({ mode }) => ({
|
||||
lines: 90,
|
||||
statements: 90,
|
||||
},
|
||||
'src/components/dialog/SiteUserDataDialog.vue': {
|
||||
branches: 85,
|
||||
functions: 90,
|
||||
lines: 90,
|
||||
statements: 90,
|
||||
},
|
||||
'src/views/discover/TheMovieDbView.vue': {
|
||||
branches: 75,
|
||||
functions: 80,
|
||||
|
||||
Reference in New Issue
Block a user