mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-09 01:36:45 +08:00
test(setting): cover search subscribe and site settings (#667)
This commit is contained in:
@@ -868,44 +868,17 @@
|
|||||||
},
|
},
|
||||||
"src/views/setting/AccountSettingSearch.vue": {
|
"src/views/setting/AccountSettingSearch.vue": {
|
||||||
"@typescript-eslint/no-explicit-any": {
|
"@typescript-eslint/no-explicit-any": {
|
||||||
"count": 12
|
"count": 11
|
||||||
},
|
|
||||||
"@typescript-eslint/no-unused-vars": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"no-empty": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"no-prototype-builtins": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"sonarjs/no-ignored-exceptions": {
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/views/setting/AccountSettingSite.vue": {
|
"src/views/setting/AccountSettingSite.vue": {
|
||||||
"@typescript-eslint/no-explicit-any": {
|
"@typescript-eslint/no-explicit-any": {
|
||||||
"count": 6
|
"count": 5
|
||||||
},
|
|
||||||
"no-prototype-builtins": {
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/views/setting/AccountSettingSubscribe.vue": {
|
"src/views/setting/AccountSettingSubscribe.vue": {
|
||||||
"@typescript-eslint/no-explicit-any": {
|
"@typescript-eslint/no-explicit-any": {
|
||||||
"count": 12
|
"count": 11
|
||||||
},
|
|
||||||
"@typescript-eslint/no-unused-vars": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"no-empty": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"no-prototype-builtins": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"sonarjs/no-ignored-exceptions": {
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/views/setting/AccountSettingSystem.vue": {
|
"src/views/setting/AccountSettingSystem.vue": {
|
||||||
|
|||||||
@@ -119,13 +119,13 @@ async function querySelectedSites() {
|
|||||||
// 保存用户选中的站点
|
// 保存用户选中的站点
|
||||||
async function saveSelectedSites() {
|
async function saveSelectedSites() {
|
||||||
try {
|
try {
|
||||||
// 用户名密码
|
|
||||||
const result: { [key: string]: any } = await api.post('system/setting/IndexerSites', selectedSites.value)
|
const result: { [key: string]: any } = await api.post('system/setting/IndexerSites', selectedSites.value)
|
||||||
|
|
||||||
if (result.success) $toast.success('搜索站点保存成功')
|
if (result.success) $toast.success('搜索站点保存成功')
|
||||||
else $toast.error('搜索站点保存失败!')
|
else $toast.error('搜索站点保存失败!')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
|
$toast.error('搜索站点保存失败!')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +149,9 @@ async function saveSystemSetting(value: { [key: string]: any }) {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} catch (error) {}
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,6 +182,7 @@ async function saveSearchSetting() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
|
$toast.error('搜索基础设置保存失败!')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +194,9 @@ async function loadSystemSettings() {
|
|||||||
// 将API返回的值赋值给SystemSettings
|
// 将API返回的值赋值给SystemSettings
|
||||||
for (const sectionKey of Object.keys(SystemSettings.value) as Array<keyof typeof SystemSettings.value>) {
|
for (const sectionKey of Object.keys(SystemSettings.value) as Array<keyof typeof SystemSettings.value>) {
|
||||||
Object.keys(SystemSettings.value[sectionKey]).forEach((key: string) => {
|
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')
|
const result: { [key: string]: any } = await api.get('site/reset')
|
||||||
if (result.success) $toast.success(t('setting.site.resetSuccess'))
|
if (result.success) $toast.success(t('setting.site.resetSuccess'))
|
||||||
else $toast.error(t('setting.site.resetFailed'))
|
else $toast.error(t('setting.site.resetFailed'))
|
||||||
|
|
||||||
resetSitesDisabled.value = false
|
|
||||||
resetSitesText.value = t('setting.site.resetSites')
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(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
|
// 将API返回的值赋值给SystemSettings
|
||||||
for (const sectionKey of Object.keys(siteSetting.value) as Array<keyof typeof siteSetting.value>) {
|
for (const sectionKey of Object.keys(siteSetting.value) as Array<keyof typeof siteSetting.value>) {
|
||||||
Object.keys(siteSetting.value[sectionKey]).forEach((key: string) => {
|
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'))
|
else $toast.error(t('setting.subscribe.saveFailed'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
|
$toast.error(t('setting.subscribe.saveFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +131,9 @@ async function loadSystemSettings() {
|
|||||||
// 将API返回的值赋值给SystemSettings
|
// 将API返回的值赋值给SystemSettings
|
||||||
for (const sectionKey of Object.keys(SystemSettings.value) as Array<keyof typeof SystemSettings.value>) {
|
for (const sectionKey of Object.keys(SystemSettings.value) as Array<keyof typeof SystemSettings.value>) {
|
||||||
Object.keys(SystemSettings.value[sectionKey]).forEach((key: string) => {
|
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) {
|
if (result.success) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} catch (error) {}
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,6 +190,7 @@ async function saveSubscribeSetting() {
|
|||||||
} else $toast.error(t('setting.subscribe.settingsSaveFailed'))
|
} else $toast.error(t('setting.subscribe.settingsSaveFailed'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(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('订阅站点保存失败!'))
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -388,6 +388,9 @@ export default defineConfig(({ command, mode, isPreview }) => ({
|
|||||||
'src/utils/mediaStatusCache.ts',
|
'src/utils/mediaStatusCache.ts',
|
||||||
'src/utils/searchStream.ts',
|
'src/utils/searchStream.ts',
|
||||||
'src/views/site/SiteCardListView.vue',
|
'src/views/site/SiteCardListView.vue',
|
||||||
|
'src/views/setting/AccountSettingSearch.vue',
|
||||||
|
'src/views/setting/AccountSettingSite.vue',
|
||||||
|
'src/views/setting/AccountSettingSubscribe.vue',
|
||||||
'src/views/user/UserListView.vue',
|
'src/views/user/UserListView.vue',
|
||||||
'src/views/user/UserProfileView.vue',
|
'src/views/user/UserProfileView.vue',
|
||||||
'src/views/reorganize/DownloadingListView.vue',
|
'src/views/reorganize/DownloadingListView.vue',
|
||||||
@@ -444,6 +447,24 @@ export default defineConfig(({ command, mode, isPreview }) => ({
|
|||||||
lines: 80,
|
lines: 80,
|
||||||
statements: 80,
|
statements: 80,
|
||||||
},
|
},
|
||||||
|
'src/views/setting/AccountSettingSearch.vue': {
|
||||||
|
branches: 75,
|
||||||
|
functions: 80,
|
||||||
|
lines: 80,
|
||||||
|
statements: 80,
|
||||||
|
},
|
||||||
|
'src/views/setting/AccountSettingSite.vue': {
|
||||||
|
branches: 75,
|
||||||
|
functions: 80,
|
||||||
|
lines: 80,
|
||||||
|
statements: 80,
|
||||||
|
},
|
||||||
|
'src/views/setting/AccountSettingSubscribe.vue': {
|
||||||
|
branches: 75,
|
||||||
|
functions: 80,
|
||||||
|
lines: 80,
|
||||||
|
statements: 80,
|
||||||
|
},
|
||||||
'src/components/cards/TorrentCard.vue': {
|
'src/components/cards/TorrentCard.vue': {
|
||||||
branches: 75,
|
branches: 75,
|
||||||
functions: 80,
|
functions: 80,
|
||||||
|
|||||||
Reference in New Issue
Block a user