mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-04 23:18:44 +08:00
test(setting): cover search subscribe and site settings (#667)
This commit is contained in:
@@ -119,13 +119,13 @@ async function querySelectedSites() {
|
||||
// 保存用户选中的站点
|
||||
async function saveSelectedSites() {
|
||||
try {
|
||||
// 用户名密码
|
||||
const result: { [key: string]: any } = await api.post('system/setting/IndexerSites', selectedSites.value)
|
||||
|
||||
if (result.success) $toast.success('搜索站点保存成功')
|
||||
else $toast.error('搜索站点保存失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
$toast.error('搜索站点保存失败!')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +149,9 @@ async function saveSystemSetting(value: { [key: string]: any }) {
|
||||
if (result.success) {
|
||||
return true
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -180,6 +182,7 @@ async function saveSearchSetting() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
$toast.error('搜索基础设置保存失败!')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +194,9 @@ async function loadSystemSettings() {
|
||||
// 将API返回的值赋值给SystemSettings
|
||||
for (const sectionKey of Object.keys(SystemSettings.value) as Array<keyof typeof SystemSettings.value>) {
|
||||
Object.keys(SystemSettings.value[sectionKey]).forEach((key: string) => {
|
||||
if (result.data.hasOwnProperty(key)) (SystemSettings.value[sectionKey] as any)[key] = result.data[key]
|
||||
if (Object.prototype.hasOwnProperty.call(result.data, key)) {
|
||||
Reflect.set(SystemSettings.value[sectionKey], key, result.data[key])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,11 +87,12 @@ async function resetSites() {
|
||||
const result: { [key: string]: any } = await api.get('site/reset')
|
||||
if (result.success) $toast.success(t('setting.site.resetSuccess'))
|
||||
else $toast.error(t('setting.site.resetFailed'))
|
||||
|
||||
resetSitesDisabled.value = false
|
||||
resetSitesText.value = t('setting.site.resetSites')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
$toast.error(t('setting.site.resetFailed'))
|
||||
} finally {
|
||||
resetSitesDisabled.value = false
|
||||
resetSitesText.value = t('setting.site.resetSites')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +104,9 @@ async function loadSiteSettings() {
|
||||
// 将API返回的值赋值给SystemSettings
|
||||
for (const sectionKey of Object.keys(siteSetting.value) as Array<keyof typeof siteSetting.value>) {
|
||||
Object.keys(siteSetting.value[sectionKey]).forEach((key: string) => {
|
||||
if (result.data.hasOwnProperty(key)) (siteSetting.value[sectionKey] as any)[key] = result.data[key]
|
||||
if (Object.prototype.hasOwnProperty.call(result.data, key)) {
|
||||
Reflect.set(siteSetting.value[sectionKey], key, result.data[key])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ async function saveSelectedRssSites() {
|
||||
else $toast.error(t('setting.subscribe.saveFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
$toast.error(t('setting.subscribe.saveFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +131,9 @@ async function loadSystemSettings() {
|
||||
// 将API返回的值赋值给SystemSettings
|
||||
for (const sectionKey of Object.keys(SystemSettings.value) as Array<keyof typeof SystemSettings.value>) {
|
||||
Object.keys(SystemSettings.value[sectionKey]).forEach((key: string) => {
|
||||
if (result.data.hasOwnProperty(key)) (SystemSettings.value[sectionKey] as any)[key] = result.data[key]
|
||||
if (Object.prototype.hasOwnProperty.call(result.data, key)) {
|
||||
Reflect.set(SystemSettings.value[sectionKey], key, result.data[key])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -147,7 +150,9 @@ async function saveSystemSetting(value: { [key: string]: any }) {
|
||||
if (result.success) {
|
||||
return true
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -185,6 +190,7 @@ async function saveSubscribeSetting() {
|
||||
} else $toast.error(t('setting.subscribe.settingsSaveFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
$toast.error(t('setting.subscribe.settingsSaveFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import AccountSettingSearch from '@/views/setting/AccountSettingSearch.vue'
|
||||
import { fireEvent, screen, waitFor, within } from '@testing-library/vue'
|
||||
import { renderWithProviders } from '@tests/support/render'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
apiGet: vi.fn(),
|
||||
apiPost: vi.fn(),
|
||||
toastError: vi.fn(),
|
||||
toastSuccess: vi.fn(),
|
||||
useSilentSettingRefresh: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
default: {
|
||||
get: mocks.apiGet,
|
||||
post: mocks.apiPost,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('vue-toastification', () => ({
|
||||
useToast: () => ({
|
||||
error: mocks.toastError,
|
||||
success: mocks.toastSuccess,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useSilentSettingRefresh', () => ({
|
||||
useSilentSettingRefresh: mocks.useSilentSettingRefresh,
|
||||
}))
|
||||
|
||||
function mockLoadedSettings() {
|
||||
mocks.apiGet.mockImplementation((endpoint: string) => {
|
||||
if (endpoint === 'site/') {
|
||||
return [
|
||||
{ id: 1, name: 'Alpha', is_active: true },
|
||||
{ id: 2, name: 'Disabled', is_active: false },
|
||||
]
|
||||
}
|
||||
if (endpoint === 'system/setting/UserFilterRuleGroups') {
|
||||
return { success: true, data: { value: [{ name: 'HDR' }, { name: 'Remux' }] } }
|
||||
}
|
||||
if (endpoint === 'system/setting/public/IndexerSites') {
|
||||
return { success: true, data: { value: [1] } }
|
||||
}
|
||||
if (endpoint === 'system/setting/SEARCH_SOURCE') {
|
||||
return { success: true, data: { value: 'douban,bangumi' } }
|
||||
}
|
||||
if (endpoint === 'system/setting/SearchFilterRuleGroups') {
|
||||
return { success: true, data: { value: ['HDR'] } }
|
||||
}
|
||||
if (endpoint === 'system/env') {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
SEARCH_MULTIPLE_NAME: true,
|
||||
DOWNLOAD_SUBTITLE: true,
|
||||
AUTO_DOWNLOAD_USER: '42',
|
||||
UNRELATED: 'ignored',
|
||||
},
|
||||
}
|
||||
}
|
||||
throw new Error(`Unexpected GET ${endpoint}`)
|
||||
})
|
||||
}
|
||||
|
||||
async function renderSettings() {
|
||||
return renderWithProviders(AccountSettingSearch)
|
||||
}
|
||||
|
||||
function getCard(title: string) {
|
||||
const card = screen.getByText(title).closest('.v-card')
|
||||
expect(card).not.toBeNull()
|
||||
return within(card as HTMLElement)
|
||||
}
|
||||
|
||||
describe('AccountSettingSearch', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
mocks.apiGet.mockReset()
|
||||
mocks.apiPost.mockReset()
|
||||
mocks.toastError.mockReset()
|
||||
mocks.toastSuccess.mockReset()
|
||||
mocks.useSilentSettingRefresh.mockReset()
|
||||
mockLoadedSettings()
|
||||
mocks.apiPost.mockResolvedValue({ success: true })
|
||||
})
|
||||
|
||||
it('loads declared settings, filters disabled sites, and registers active-only refresh', async () => {
|
||||
const { rerender } = await renderSettings()
|
||||
|
||||
expect(await screen.findByDisplayValue('MOVIEPILOT')).toBeInTheDocument()
|
||||
expect(screen.getByText('Alpha')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Disabled')).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('checkbox', { name: '多名称资源搜索' })).toBeChecked()
|
||||
expect(screen.getByRole('checkbox', { name: '下载站点字幕' })).toBeChecked()
|
||||
|
||||
const refreshOptions = mocks.useSilentSettingRefresh.mock.calls[0]?.[1]
|
||||
expect(refreshOptions.active.value).toBe(true)
|
||||
await rerender({ active: false })
|
||||
expect(refreshOptions.active.value).toBe(false)
|
||||
})
|
||||
|
||||
it('saves media sources, rule groups, and only the owned environment keys', async () => {
|
||||
await renderSettings()
|
||||
await screen.findByDisplayValue('MOVIEPILOT')
|
||||
|
||||
await fireEvent.update(screen.getByLabelText('下载任务标签'), 'UPDATED')
|
||||
await fireEvent.update(screen.getByLabelText('远程搜索自动下载用户'), '84')
|
||||
await fireEvent.click(screen.getByRole('checkbox', { name: '多名称资源搜索' }))
|
||||
await fireEvent.click(screen.getByRole('checkbox', { name: '下载站点字幕' }))
|
||||
|
||||
await fireEvent.click(getCard('基础设置').getByRole('button', { name: '保存' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.apiPost).toHaveBeenNthCalledWith(1, 'system/setting/SEARCH_SOURCE', 'douban,bangumi')
|
||||
expect(mocks.apiPost).toHaveBeenNthCalledWith(2, 'system/setting/SearchFilterRuleGroups', ['HDR'])
|
||||
expect(mocks.apiPost).toHaveBeenNthCalledWith(3, 'system/env', {
|
||||
SEARCH_MULTIPLE_NAME: false,
|
||||
DOWNLOAD_SUBTITLE: false,
|
||||
AUTO_DOWNLOAD_USER: '84',
|
||||
TORRENT_TAG: 'UPDATED',
|
||||
})
|
||||
})
|
||||
await waitFor(() => expect(mocks.toastSuccess).toHaveBeenCalledWith('搜索基础设置保存成功'))
|
||||
})
|
||||
|
||||
it('stops the combined save when the media source is rejected', async () => {
|
||||
mocks.apiPost.mockResolvedValueOnce({ success: false, message: 'source rejected' })
|
||||
await renderSettings()
|
||||
|
||||
await fireEvent.click(getCard('基础设置').getByRole('button', { name: '保存' }))
|
||||
|
||||
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('媒体搜索数据源保存失败:source rejected!'))
|
||||
expect(mocks.apiPost).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.toastSuccess).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reports an environment business failure and an HTTP failure without reporting success', async () => {
|
||||
mocks.apiPost
|
||||
.mockResolvedValueOnce({ success: true })
|
||||
.mockResolvedValueOnce({ success: true })
|
||||
.mockResolvedValueOnce({ success: false })
|
||||
await renderSettings()
|
||||
|
||||
await fireEvent.click(getCard('基础设置').getByRole('button', { name: '保存' }))
|
||||
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('搜索基础设置保存失败!'))
|
||||
expect(mocks.toastSuccess).not.toHaveBeenCalled()
|
||||
|
||||
mocks.toastError.mockReset()
|
||||
mocks.apiPost.mockReset()
|
||||
mocks.apiPost.mockRejectedValueOnce(new Error('offline'))
|
||||
await fireEvent.click(getCard('基础设置').getByRole('button', { name: '保存' }))
|
||||
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('搜索基础设置保存失败!'))
|
||||
expect(mocks.toastSuccess).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('saves selected search sites and exposes business and HTTP failures', async () => {
|
||||
await renderSettings()
|
||||
await screen.findByText('Alpha')
|
||||
const saveSites = getCard('搜索站点').getByRole('button', { name: '保存' })
|
||||
|
||||
await fireEvent.click(screen.getByText('Alpha'))
|
||||
|
||||
await fireEvent.click(saveSites)
|
||||
await waitFor(() => expect(mocks.apiPost).toHaveBeenCalledWith('system/setting/IndexerSites', []))
|
||||
expect(mocks.toastSuccess).toHaveBeenCalledWith('搜索站点保存成功')
|
||||
|
||||
mocks.apiPost.mockReset()
|
||||
mocks.toastSuccess.mockReset()
|
||||
mocks.apiPost.mockResolvedValueOnce({ success: false })
|
||||
await fireEvent.click(saveSites)
|
||||
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('搜索站点保存失败!'))
|
||||
|
||||
mocks.apiPost.mockReset()
|
||||
mocks.toastError.mockReset()
|
||||
mocks.apiPost.mockRejectedValueOnce(new Error('offline'))
|
||||
await fireEvent.click(saveSites)
|
||||
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('搜索站点保存失败!'))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,210 @@
|
||||
import AccountSettingSite from '@/views/setting/AccountSettingSite.vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { fireEvent, screen, waitFor, within } from '@testing-library/vue'
|
||||
import { renderWithProviders } from '@tests/support/render'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
apiGet: vi.fn(),
|
||||
apiPost: vi.fn(),
|
||||
toastError: vi.fn(),
|
||||
toastSuccess: vi.fn(),
|
||||
useSilentSettingRefresh: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
default: { get: mocks.apiGet, post: mocks.apiPost },
|
||||
}))
|
||||
|
||||
vi.mock('vue-toastification', () => ({
|
||||
useToast: () => ({ error: mocks.toastError, success: mocks.toastSuccess }),
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useSilentSettingRefresh', () => ({
|
||||
useSilentSettingRefresh: mocks.useSilentSettingRefresh,
|
||||
}))
|
||||
|
||||
function mockLoadedSettings() {
|
||||
mocks.apiGet.mockImplementation((endpoint: string) => {
|
||||
if (endpoint === 'system/env') {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
COOKIECLOUD_HOST: 'https://cookies.example.com',
|
||||
COOKIECLOUD_KEY: 'key',
|
||||
COOKIECLOUD_PASSWORD: 'password',
|
||||
COOKIECLOUD_AUTH_HEADER: 'header',
|
||||
COOKIECLOUD_INTERVAL: 60,
|
||||
COOKIECLOUD_ENABLE_LOCAL: false,
|
||||
COOKIECLOUD_BLACKLIST: 'blocked.example.com',
|
||||
SITEDATA_REFRESH_INTERVAL: 6,
|
||||
SITE_MESSAGE: true,
|
||||
BROWSER_EMULATION: 'flaresolverr',
|
||||
OCR_HOST: 'https://ocr.example.com',
|
||||
FLARESOLVERR_URL: 'http://solver:8191',
|
||||
UNRELATED: 'ignored',
|
||||
},
|
||||
}
|
||||
}
|
||||
if (endpoint === 'site/reset') return { success: true }
|
||||
throw new Error(`Unexpected GET ${endpoint}`)
|
||||
})
|
||||
}
|
||||
|
||||
async function renderSettings() {
|
||||
return renderWithProviders(AccountSettingSite)
|
||||
}
|
||||
|
||||
function getCard(title: string) {
|
||||
const card = screen.getByText(title).closest('.v-card')
|
||||
expect(card).not.toBeNull()
|
||||
return within(card as HTMLElement)
|
||||
}
|
||||
|
||||
describe('AccountSettingSite', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
mocks.apiGet.mockReset()
|
||||
mocks.apiPost.mockReset()
|
||||
mocks.toastError.mockReset()
|
||||
mocks.toastSuccess.mockReset()
|
||||
mocks.useSilentSettingRefresh.mockReset()
|
||||
mockLoadedSettings()
|
||||
mocks.apiPost.mockResolvedValue({ success: true })
|
||||
})
|
||||
|
||||
it('loads only declared site settings and follows active refresh state', async () => {
|
||||
const { rerender } = await renderSettings()
|
||||
|
||||
expect(await screen.findByDisplayValue('https://cookies.example.com')).toBeInTheDocument()
|
||||
expect(screen.getByDisplayValue('https://ocr.example.com')).toBeInTheDocument()
|
||||
expect(screen.getByDisplayValue('http://solver:8191')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('搜索资源获取页数')).toHaveValue(1)
|
||||
expect(screen.getByRole('checkbox', { name: '阅读站点消息' })).toBeChecked()
|
||||
|
||||
const refreshOptions = mocks.useSilentSettingRefresh.mock.calls[0]?.[1]
|
||||
expect(refreshOptions.active.value).toBe(true)
|
||||
await rerender({ active: false })
|
||||
expect(refreshOptions.active.value).toBe(false)
|
||||
})
|
||||
|
||||
it('saves CookieCloud and site sections with only their owned keys', async () => {
|
||||
const user = userEvent.setup()
|
||||
const updatedCredential = 'updated-value'
|
||||
await renderSettings()
|
||||
await screen.findByDisplayValue('https://cookies.example.com')
|
||||
|
||||
await fireEvent.click(screen.getByRole('checkbox', { name: '启用本地CookieCloud服务器' }))
|
||||
await fireEvent.update(screen.getByLabelText('服务地址'), 'https://new-cookies.example.com')
|
||||
await fireEvent.update(screen.getByLabelText('用户KEY'), 'new-key')
|
||||
await fireEvent.update(screen.getByLabelText('端对端加密密码'), updatedCredential)
|
||||
await fireEvent.update(screen.getByLabelText('上传认证 Header'), 'new-header')
|
||||
await fireEvent.update(screen.getByLabelText('同步域名黑名单'), 'new-blocked.example.com')
|
||||
await user.click(screen.getByLabelText('自动同步间隔'))
|
||||
await user.click(await screen.findByRole('option', { name: '每6小时' }))
|
||||
|
||||
await fireEvent.click(getCard('站点同步').getByRole('button', { name: '保存' }))
|
||||
await waitFor(() => {
|
||||
expect(mocks.apiPost).toHaveBeenNthCalledWith(1, 'system/env', {
|
||||
COOKIECLOUD_HOST: 'https://new-cookies.example.com',
|
||||
COOKIECLOUD_KEY: 'new-key',
|
||||
COOKIECLOUD_PASSWORD: updatedCredential,
|
||||
COOKIECLOUD_AUTH_HEADER: 'new-header',
|
||||
COOKIECLOUD_INTERVAL: 360,
|
||||
COOKIECLOUD_ENABLE_LOCAL: true,
|
||||
COOKIECLOUD_BLACKLIST: 'new-blocked.example.com',
|
||||
})
|
||||
})
|
||||
|
||||
await fireEvent.update(screen.getByLabelText('搜索资源获取页数'), '4')
|
||||
await fireEvent.update(screen.getByLabelText('验证码识别服务器'), 'https://new-ocr.example.com')
|
||||
await fireEvent.update(screen.getByLabelText('FlareSolverr 服务地址'), 'http://new-solver:8191')
|
||||
await fireEvent.click(screen.getByRole('checkbox', { name: '阅读站点消息' }))
|
||||
await user.click(screen.getByLabelText('站点数据刷新间隔'))
|
||||
await user.click(await screen.findByRole('option', { name: '每12小时' }))
|
||||
await user.click(screen.getByLabelText('浏览器仿真'))
|
||||
await user.click(await screen.findByRole('option', { name: 'CloakBrowser' }))
|
||||
expect(screen.queryByLabelText('FlareSolverr 服务地址')).not.toBeInTheDocument()
|
||||
|
||||
await fireEvent.click(getCard('站点选项').getByRole('button', { name: '保存' }))
|
||||
await waitFor(() => {
|
||||
expect(mocks.apiPost).toHaveBeenNthCalledWith(2, 'system/env', {
|
||||
SITEDATA_REFRESH_INTERVAL: 12,
|
||||
SITE_MESSAGE: false,
|
||||
SEARCH_RESOURCE_PAGES: 4,
|
||||
BROWSER_EMULATION: 'cloakbrowser',
|
||||
OCR_HOST: 'https://new-ocr.example.com',
|
||||
FLARESOLVERR_URL: 'http://new-solver:8191',
|
||||
})
|
||||
})
|
||||
expect(mocks.toastSuccess).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('exposes business and HTTP failures when saving a section', async () => {
|
||||
mocks.apiPost.mockResolvedValueOnce({ success: false })
|
||||
await renderSettings()
|
||||
const save = getCard('站点同步').getByRole('button', { name: '保存' })
|
||||
|
||||
await fireEvent.click(save)
|
||||
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('站点设置保存失败!'))
|
||||
|
||||
mocks.toastError.mockReset()
|
||||
mocks.apiPost.mockRejectedValueOnce(new Error('offline'))
|
||||
await fireEvent.click(save)
|
||||
await waitFor(() => expect(mocks.toastError).toHaveBeenCalled())
|
||||
expect(mocks.toastSuccess).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('requires confirmation and restores the reset action after success or business failure', async () => {
|
||||
const user = userEvent.setup()
|
||||
await renderSettings()
|
||||
const resetCard = getCard('站点重置')
|
||||
const resetButton = resetCard.getByRole('button', { name: '重置站点数据' })
|
||||
let resolveReset: ((value: { success: boolean }) => void) | undefined
|
||||
mocks.apiGet.mockImplementation((endpoint: string) => {
|
||||
if (endpoint === 'site/reset') {
|
||||
return new Promise(resolve => {
|
||||
resolveReset = resolve
|
||||
})
|
||||
}
|
||||
throw new Error(`Unexpected GET ${endpoint}`)
|
||||
})
|
||||
expect(resetButton).toBeDisabled()
|
||||
|
||||
await user.click(resetCard.getByRole('checkbox', { name: '确认删除所有站点数据并重新同步。' }))
|
||||
expect(resetButton).toBeEnabled()
|
||||
await user.click(resetButton)
|
||||
await waitFor(() => expect(mocks.apiGet).toHaveBeenCalledWith('site/reset'))
|
||||
expect(resetCard.getByRole('button', { name: '正在重置...' })).toBeDisabled()
|
||||
await user.click(resetCard.getByRole('button', { name: '正在重置...' }))
|
||||
expect(mocks.apiGet.mock.calls.filter(([endpoint]) => endpoint === 'site/reset')).toHaveLength(1)
|
||||
|
||||
resolveReset?.({ success: true })
|
||||
await waitFor(() => expect(mocks.toastSuccess).toHaveBeenCalledWith('站点重置成功,请等待CookieCloud同步完成!'))
|
||||
expect(resetCard.getByRole('button', { name: '重置站点数据' })).toBeEnabled()
|
||||
|
||||
mocks.apiGet.mockImplementation((endpoint: string) => {
|
||||
if (endpoint === 'site/reset') return { success: false }
|
||||
return mockLoadedSettings()
|
||||
})
|
||||
mocks.toastSuccess.mockReset()
|
||||
await user.click(resetCard.getByRole('button', { name: '重置站点数据' }))
|
||||
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('站点重置失败!'))
|
||||
expect(resetCard.getByRole('button', { name: '重置站点数据' })).toBeEnabled()
|
||||
})
|
||||
|
||||
it('restores the reset action and shows failure after an HTTP error', async () => {
|
||||
await renderSettings()
|
||||
const resetCard = getCard('站点重置')
|
||||
await fireEvent.click(resetCard.getByRole('checkbox', { name: '确认删除所有站点数据并重新同步。' }))
|
||||
mocks.apiGet.mockImplementation((endpoint: string) => {
|
||||
if (endpoint === 'site/reset') throw new Error('offline')
|
||||
throw new Error(`Unexpected GET ${endpoint}`)
|
||||
})
|
||||
|
||||
await fireEvent.click(resetCard.getByRole('button', { name: '重置站点数据' }))
|
||||
|
||||
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('站点重置失败!'))
|
||||
expect(resetCard.getByRole('button', { name: '重置站点数据' })).toBeEnabled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,173 @@
|
||||
import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { fireEvent, screen, waitFor, within } from '@testing-library/vue'
|
||||
import { renderWithProviders } from '@tests/support/render'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
apiGet: vi.fn(),
|
||||
apiPost: vi.fn(),
|
||||
toastError: vi.fn(),
|
||||
toastSuccess: vi.fn(),
|
||||
useSilentSettingRefresh: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
default: { get: mocks.apiGet, post: mocks.apiPost },
|
||||
}))
|
||||
|
||||
vi.mock('vue-toastification', () => ({
|
||||
useToast: () => ({ error: mocks.toastError, success: mocks.toastSuccess }),
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useSilentSettingRefresh', () => ({
|
||||
useSilentSettingRefresh: mocks.useSilentSettingRefresh,
|
||||
}))
|
||||
|
||||
function mockLoadedSettings() {
|
||||
mocks.apiGet.mockImplementation((endpoint: string) => {
|
||||
if (endpoint === 'site/') {
|
||||
return [
|
||||
{ id: 3, name: 'RSS Alpha', is_active: true },
|
||||
{ id: 4, name: 'RSS Disabled', is_active: false },
|
||||
]
|
||||
}
|
||||
if (endpoint === 'system/setting/UserFilterRuleGroups') {
|
||||
return { success: true, data: { value: [{ name: 'HDR' }, { name: 'Remux' }] } }
|
||||
}
|
||||
if (endpoint === 'system/setting/RssSites') return { success: true, data: { value: [3] } }
|
||||
if (endpoint === 'system/setting/SubscribeFilterRuleGroups') {
|
||||
return { success: true, data: { value: ['HDR'] } }
|
||||
}
|
||||
if (endpoint === 'system/setting/BestVersionFilterRuleGroups') {
|
||||
return { success: true, data: { value: ['Remux'] } }
|
||||
}
|
||||
if (endpoint === 'system/env') {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
SUBSCRIBE_SEARCH: true,
|
||||
SUBSCRIBE_SEARCH_INTERVAL: 72,
|
||||
SUBSCRIBE_RSS_INTERVAL: 20,
|
||||
LOCAL_EXISTS_SEARCH: true,
|
||||
UNRELATED: 'ignored',
|
||||
},
|
||||
}
|
||||
}
|
||||
throw new Error(`Unexpected GET ${endpoint}`)
|
||||
})
|
||||
}
|
||||
|
||||
async function renderSettings() {
|
||||
return renderWithProviders(AccountSettingSubscribe)
|
||||
}
|
||||
|
||||
function getCard(title: string) {
|
||||
const card = screen.getByText(title).closest('.v-card')
|
||||
expect(card).not.toBeNull()
|
||||
return within(card as HTMLElement)
|
||||
}
|
||||
|
||||
describe('AccountSettingSubscribe', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
mocks.apiGet.mockReset()
|
||||
mocks.apiPost.mockReset()
|
||||
mocks.toastError.mockReset()
|
||||
mocks.toastSuccess.mockReset()
|
||||
mocks.useSilentSettingRefresh.mockReset()
|
||||
mockLoadedSettings()
|
||||
mocks.apiPost.mockResolvedValue({ success: true })
|
||||
})
|
||||
|
||||
it('loads owned settings, filters inactive sites, and follows active refresh state', async () => {
|
||||
const { rerender } = await renderSettings()
|
||||
|
||||
expect(await screen.findByText('RSS Alpha')).toBeInTheDocument()
|
||||
expect(screen.queryByText('RSS Disabled')).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('checkbox', { name: '订阅定时搜索' })).toBeChecked()
|
||||
expect(screen.getByRole('checkbox', { name: '检查文件系统资源' })).toBeChecked()
|
||||
expect(screen.getByLabelText('订阅模式')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('订阅搜索时间间隔')).toBeInTheDocument()
|
||||
|
||||
const refreshOptions = mocks.useSilentSettingRefresh.mock.calls[0]?.[1]
|
||||
expect(refreshOptions.active.value).toBe(true)
|
||||
await rerender({ active: false })
|
||||
expect(refreshOptions.active.value).toBe(false)
|
||||
})
|
||||
|
||||
it('saves rule groups and only the owned subscription environment keys', async () => {
|
||||
const user = userEvent.setup()
|
||||
await renderSettings()
|
||||
await screen.findByText('RSS Alpha')
|
||||
|
||||
await user.click(screen.getByRole('checkbox', { name: '订阅定时搜索' }))
|
||||
await user.click(screen.getByRole('checkbox', { name: '检查文件系统资源' }))
|
||||
|
||||
await user.click(screen.getByLabelText('订阅模式'))
|
||||
await user.click(await screen.findByText('自动'))
|
||||
await user.click(screen.getByLabelText('站点RSS周期'))
|
||||
await user.click(await screen.findByText('半小时'))
|
||||
await user.click(screen.getByLabelText('订阅优先级规则组'))
|
||||
await user.click(await screen.findByRole('option', { name: 'Remux' }))
|
||||
|
||||
await user.click(getCard('基础设置').getByRole('button', { name: '保存' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.apiPost).toHaveBeenNthCalledWith(1, 'system/setting/SubscribeFilterRuleGroups', ['HDR', 'Remux'])
|
||||
expect(mocks.apiPost).toHaveBeenNthCalledWith(2, 'system/setting/BestVersionFilterRuleGroups', ['Remux'])
|
||||
expect(mocks.apiPost).toHaveBeenNthCalledWith(3, 'system/env', {
|
||||
SUBSCRIBE_MODE: 'spider',
|
||||
SUBSCRIBE_SEARCH: false,
|
||||
SUBSCRIBE_SEARCH_INTERVAL: 72,
|
||||
SUBSCRIBE_RSS_INTERVAL: 30,
|
||||
LOCAL_EXISTS_SEARCH: false,
|
||||
})
|
||||
})
|
||||
await waitFor(() => expect(mocks.toastSuccess).toHaveBeenCalledWith('订阅基础设置保存成功'))
|
||||
})
|
||||
|
||||
it('reports environment business and HTTP failures from the combined settings save', async () => {
|
||||
mocks.apiPost
|
||||
.mockResolvedValueOnce({ success: true })
|
||||
.mockResolvedValueOnce({ success: true })
|
||||
.mockResolvedValueOnce({ success: false })
|
||||
await renderSettings()
|
||||
const save = getCard('基础设置').getByRole('button', { name: '保存' })
|
||||
|
||||
await fireEvent.click(save)
|
||||
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('订阅基础设置保存失败!'))
|
||||
expect(mocks.apiPost).toHaveBeenNthCalledWith(3, 'system/env', expect.objectContaining({ SUBSCRIBE_MODE: 'auto' }))
|
||||
expect(mocks.toastSuccess).not.toHaveBeenCalled()
|
||||
|
||||
mocks.apiPost.mockReset()
|
||||
mocks.toastError.mockReset()
|
||||
mocks.apiPost.mockRejectedValueOnce(new Error('offline'))
|
||||
await fireEvent.click(save)
|
||||
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('订阅基础设置保存失败!'))
|
||||
})
|
||||
|
||||
it('saves selected RSS sites and exposes business and HTTP failures', async () => {
|
||||
await renderSettings()
|
||||
await screen.findByText('RSS Alpha')
|
||||
const saveSites = getCard('订阅站点').getByRole('button', { name: '保存' })
|
||||
|
||||
await fireEvent.click(screen.getByText('RSS Alpha'))
|
||||
|
||||
await fireEvent.click(saveSites)
|
||||
await waitFor(() => expect(mocks.apiPost).toHaveBeenCalledWith('system/setting/RssSites', []))
|
||||
expect(mocks.toastSuccess).toHaveBeenCalledWith('订阅站点保存成功')
|
||||
|
||||
mocks.apiPost.mockReset()
|
||||
mocks.toastSuccess.mockReset()
|
||||
mocks.apiPost.mockResolvedValueOnce({ success: false })
|
||||
await fireEvent.click(saveSites)
|
||||
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('订阅站点保存失败!'))
|
||||
|
||||
mocks.apiPost.mockReset()
|
||||
mocks.toastError.mockReset()
|
||||
mocks.apiPost.mockRejectedValueOnce(new Error('offline'))
|
||||
await fireEvent.click(saveSites)
|
||||
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('订阅站点保存失败!'))
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user