test(setting): cover search subscribe and site settings (#667)

This commit is contained in:
InfinityPacer
2026-08-11 23:37:49 +08:00
committed by GitHub
parent 26fcda661d
commit d29033c233
8 changed files with 611 additions and 39 deletions
+8 -3
View File
@@ -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])
}
})
}
}
+7 -4
View File
@@ -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('订阅站点保存失败!'))
})
})