mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-09 01:36:45 +08:00
test(setting): cover directory and rule settings (#668)
This commit is contained in:
@@ -841,10 +841,7 @@
|
|||||||
},
|
},
|
||||||
"src/views/setting/AccountSettingDirectory.vue": {
|
"src/views/setting/AccountSettingDirectory.vue": {
|
||||||
"@typescript-eslint/no-explicit-any": {
|
"@typescript-eslint/no-explicit-any": {
|
||||||
"count": 9
|
"count": 8
|
||||||
},
|
|
||||||
"no-prototype-builtins": {
|
|
||||||
"count": 1
|
|
||||||
},
|
},
|
||||||
"sonarjs/super-linear-regex": {
|
"sonarjs/super-linear-regex": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@@ -861,9 +858,6 @@
|
|||||||
"src/views/setting/AccountSettingRule.vue": {
|
"src/views/setting/AccountSettingRule.vue": {
|
||||||
"@typescript-eslint/no-explicit-any": {
|
"@typescript-eslint/no-explicit-any": {
|
||||||
"count": 14
|
"count": 14
|
||||||
},
|
|
||||||
"prefer-const": {
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/views/setting/AccountSettingSearch.vue": {
|
"src/views/setting/AccountSettingSearch.vue": {
|
||||||
|
|||||||
@@ -126,7 +126,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])
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,6 +176,7 @@ async function saveStorages() {
|
|||||||
else $toast.error(t('setting.directory.storageSaveFailed'))
|
else $toast.error(t('setting.directory.storageSaveFailed'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
|
$toast.error(t('setting.directory.storageSaveFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,6 +205,7 @@ async function saveDirectories() {
|
|||||||
} else $toast.error(t('setting.directory.directorySaveFailed'))
|
} else $toast.error(t('setting.directory.directorySaveFailed'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
|
$toast.error(t('setting.directory.directorySaveFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,6 +300,7 @@ async function saveSystemSettings(value: any) {
|
|||||||
} else $toast.error(t('setting.directory.organizeSaveFailed'))
|
} else $toast.error(t('setting.directory.organizeSaveFailed'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
|
$toast.error(t('setting.directory.organizeSaveFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ async function saveCustomRules() {
|
|||||||
else $toast.error(t('setting.rule.customRuleSaveFailed'))
|
else $toast.error(t('setting.rule.customRuleSaveFailed'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
|
$toast.error(t('setting.rule.customRuleSaveFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +142,7 @@ async function saveFilterRuleGroups() {
|
|||||||
else $toast.error(t('setting.rule.ruleGroupSaveFailed'))
|
else $toast.error(t('setting.rule.ruleGroupSaveFailed'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
|
$toast.error(t('setting.rule.ruleGroupSaveFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,8 +168,7 @@ async function shareRules(rules: CustomRule[] | FilterRuleGroup[], type: string)
|
|||||||
|
|
||||||
// 复制到剪贴板
|
// 复制到剪贴板
|
||||||
try {
|
try {
|
||||||
let success
|
const success = copyToClipboard(value)
|
||||||
success = copyToClipboard(value)
|
|
||||||
if (await success)
|
if (await success)
|
||||||
$toast.success(
|
$toast.success(
|
||||||
type === 'custom' ? t('setting.rule.customRuleCopySuccess') : t('setting.rule.ruleGroupCopySuccess'),
|
type === 'custom' ? t('setting.rule.customRuleCopySuccess') : t('setting.rule.ruleGroupCopySuccess'),
|
||||||
@@ -374,6 +375,7 @@ async function saveTorrentPriority() {
|
|||||||
else $toast.error('优先规则保存失败!')
|
else $toast.error('优先规则保存失败!')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
|
$toast.error('优先规则保存失败!')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,52 +2,93 @@ import AccountSettingDirectory from '@/views/setting/AccountSettingDirectory.vue
|
|||||||
import { fireEvent, screen, waitFor, within } from '@testing-library/vue'
|
import { fireEvent, screen, waitFor, within } from '@testing-library/vue'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import { renderWithProviders } from '@tests/support/render'
|
import { renderWithProviders } from '@tests/support/render'
|
||||||
import { defineComponent } from 'vue'
|
import { computed, defineComponent } from 'vue'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
apiGet: vi.fn(),
|
apiGet: vi.fn(),
|
||||||
apiPost: vi.fn(),
|
apiPost: vi.fn(),
|
||||||
|
openSharedDialog: vi.fn(),
|
||||||
toastError: vi.fn(),
|
toastError: vi.fn(),
|
||||||
toastSuccess: vi.fn(),
|
toastSuccess: vi.fn(),
|
||||||
|
useSilentSettingRefresh: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/api', () => ({
|
vi.mock('@/api', () => ({
|
||||||
default: {
|
default: { get: mocks.apiGet, post: mocks.apiPost },
|
||||||
get: mocks.apiGet,
|
|
||||||
post: mocks.apiPost,
|
|
||||||
},
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('vue-toastification', () => ({
|
vi.mock('vue-toastification', () => ({
|
||||||
useToast: () => ({
|
useToast: () => ({ error: mocks.toastError, success: mocks.toastSuccess }),
|
||||||
error: mocks.toastError,
|
|
||||||
success: mocks.toastSuccess,
|
|
||||||
}),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/composables/useSilentSettingRefresh', () => ({
|
vi.mock('@/composables/useSilentSettingRefresh', () => ({
|
||||||
useSilentSettingRefresh: vi.fn(),
|
useSilentSettingRefresh: mocks.useSilentSettingRefresh,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/composables/useSharedDialog', () => ({
|
||||||
|
openSharedDialog: mocks.openSharedDialog,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/components/cards/DirectoryCard.vue', async () => {
|
vi.mock('@/components/cards/DirectoryCard.vue', async () => {
|
||||||
const { defineComponent } = await import('vue')
|
const { defineComponent } = await import('vue')
|
||||||
return { default: defineComponent({ name: 'DirectoryCardStub', template: '<div />' }) }
|
return {
|
||||||
|
default: defineComponent({
|
||||||
|
name: 'DirectoryCardStub',
|
||||||
|
props: { directory: { type: Object, required: true } },
|
||||||
|
emits: ['close', 'update:modelValue'],
|
||||||
|
template: `
|
||||||
|
<section :aria-label="'directory-' + directory.name">
|
||||||
|
<span>{{ directory.name }}</span>
|
||||||
|
<button :aria-label="'rename-' + directory.name" @click="directory.name = '目录1'">rename</button>
|
||||||
|
<button :aria-label="'remove-' + directory.name" @click="$emit('close')">remove</button>
|
||||||
|
<button
|
||||||
|
:aria-label="'paths-' + directory.name"
|
||||||
|
@click="directory.download_path = '/new-download'; directory.library_path = '/new-library'"
|
||||||
|
>paths</button>
|
||||||
|
</section>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
vi.mock('@/components/cards/StorageCard.vue', async () => {
|
vi.mock('@/components/cards/StorageCard.vue', async () => {
|
||||||
const { defineComponent } = await import('vue')
|
const { defineComponent } = await import('vue')
|
||||||
return { default: defineComponent({ name: 'StorageCardStub', template: '<div />' }) }
|
return {
|
||||||
|
default: defineComponent({
|
||||||
|
name: 'StorageCardStub',
|
||||||
|
props: { storage: { type: Object, required: true } },
|
||||||
|
emits: ['close', 'done'],
|
||||||
|
template: `
|
||||||
|
<section :aria-label="'storage-' + storage.name">
|
||||||
|
<span>{{ storage.name }}</span>
|
||||||
|
<button :aria-label="'remove-' + storage.name" @click="$emit('close')">remove</button>
|
||||||
|
<button :aria-label="'reload-' + storage.name" @click="$emit('done')">done</button>
|
||||||
|
</section>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
vi.mock('vuedraggable', async () => {
|
vi.mock('vuedraggable', async () => {
|
||||||
const { defineComponent, h } = await import('vue')
|
const { defineComponent, h } = await import('vue')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
default: defineComponent({
|
default: defineComponent({
|
||||||
name: 'DraggableStub',
|
name: 'DraggableStub',
|
||||||
setup(_props, { slots }) {
|
props: { modelValue: { type: Array, default: () => [] } },
|
||||||
return () => h('div', slots.default?.())
|
emits: ['update:modelValue', 'end'],
|
||||||
|
setup(props, { emit, slots }) {
|
||||||
|
const reverse = () => {
|
||||||
|
emit('update:modelValue', [...props.modelValue].reverse())
|
||||||
|
emit('end')
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
const items = props.modelValue as Array<{ name?: string }>
|
||||||
|
return h('div', [
|
||||||
|
h('button', { 'aria-label': `reverse-${items[0]?.name ?? 'empty'}`, onClick: reverse }, 'reverse'),
|
||||||
|
...items.map(element => slots.item?.({ element })),
|
||||||
|
])
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@@ -55,20 +96,60 @@ vi.mock('vuedraggable', async () => {
|
|||||||
|
|
||||||
const AceEditorStub = defineComponent({
|
const AceEditorStub = defineComponent({
|
||||||
name: 'VAceEditor',
|
name: 'VAceEditor',
|
||||||
props: {
|
props: { value: { type: String, default: '' } },
|
||||||
value: { type: String, default: '' },
|
emits: ['update:value'],
|
||||||
},
|
template: '<textarea :value="value" @input="$emit(\'update:value\', $event.target.value)" />',
|
||||||
template: '<div />',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function mockSettings(settingValue: boolean | null) {
|
const storagesFixture = [
|
||||||
|
{ name: '本地存储', type: 'local', config: {} },
|
||||||
|
{ name: '自定义存储 1', type: 'custom1', config: {} },
|
||||||
|
]
|
||||||
|
|
||||||
|
const directoriesFixture = [
|
||||||
|
{
|
||||||
|
name: '目录1',
|
||||||
|
storage: 'local',
|
||||||
|
download_path: '/downloads/a',
|
||||||
|
library_path: '/media/a',
|
||||||
|
priority: 8,
|
||||||
|
monitor_type: '',
|
||||||
|
media_type: '',
|
||||||
|
media_category: '',
|
||||||
|
transfer_type: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '目录3',
|
||||||
|
storage: 'local',
|
||||||
|
download_path: '/downloads/b',
|
||||||
|
library_path: '/media/b',
|
||||||
|
priority: 2,
|
||||||
|
monitor_type: '',
|
||||||
|
media_type: '',
|
||||||
|
media_category: '',
|
||||||
|
transfer_type: '',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function mockLoadedSettings(options: { mountedDisk?: boolean | null } = {}) {
|
||||||
mocks.apiGet.mockImplementation((endpoint: string) => {
|
mocks.apiGet.mockImplementation((endpoint: string) => {
|
||||||
if (endpoint === 'system/setting/public/Directories') return { data: { value: [] } }
|
if (endpoint === 'system/setting/public/Directories')
|
||||||
if (endpoint === 'system/setting/public/Storages') return { data: { value: [] } }
|
return { data: { value: structuredClone(directoriesFixture) } }
|
||||||
if (endpoint === 'media/category') return {}
|
if (endpoint === 'system/setting/public/Storages') return { data: { value: structuredClone(storagesFixture) } }
|
||||||
if (endpoint === 'system/env') return { data: {}, success: true }
|
if (endpoint === 'media/category') return { 电影: ['华语'] }
|
||||||
|
if (endpoint === 'system/env') {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
MOVIE_RENAME_FORMAT: '{{ title }}',
|
||||||
|
TV_RENAME_FORMAT: '{{ name }}',
|
||||||
|
MUSIC_RENAME_FORMAT: '{{ artist }}',
|
||||||
|
UNRELATED: 'ignored',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
if (endpoint === 'system/setting/MountedLocalDiskDeleteEmptyDirs') {
|
if (endpoint === 'system/setting/MountedLocalDiskDeleteEmptyDirs') {
|
||||||
return { data: { value: settingValue }, success: true }
|
return { data: { value: options.mountedDisk ?? null }, success: true }
|
||||||
}
|
}
|
||||||
throw new Error(`Unexpected GET ${endpoint}`)
|
throw new Error(`Unexpected GET ${endpoint}`)
|
||||||
})
|
})
|
||||||
@@ -77,57 +158,188 @@ function mockSettings(settingValue: boolean | null) {
|
|||||||
|
|
||||||
async function renderDirectorySettings() {
|
async function renderDirectorySettings() {
|
||||||
return renderWithProviders(AccountSettingDirectory, {
|
return renderWithProviders(AccountSettingDirectory, {
|
||||||
global: {
|
global: { stubs: { VAceEditor: AceEditorStub } },
|
||||||
stubs: {
|
|
||||||
VAceEditor: AceEditorStub,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('mounted local disk empty directory cleanup setting', () => {
|
function getCard(title: string) {
|
||||||
|
const card = screen.getByText(title).closest('.v-card')
|
||||||
|
expect(card).not.toBeNull()
|
||||||
|
return within(card as HTMLElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRenameEditors() {
|
||||||
|
return screen.getAllByRole('textbox').filter(element => element.tagName === 'TEXTAREA')
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('AccountSettingDirectory', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||||
mocks.apiGet.mockReset()
|
mocks.apiGet.mockReset()
|
||||||
mocks.apiPost.mockReset()
|
mocks.apiPost.mockReset()
|
||||||
|
mocks.openSharedDialog.mockReset()
|
||||||
mocks.toastError.mockReset()
|
mocks.toastError.mockReset()
|
||||||
mocks.toastSuccess.mockReset()
|
mocks.toastSuccess.mockReset()
|
||||||
|
mocks.useSilentSettingRefresh.mockReset()
|
||||||
|
mockLoadedSettings()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('defaults to enabled when no saved value exists', async () => {
|
it('loads owned values, keeps the mounted-disk default, and follows active refresh state', async () => {
|
||||||
mockSettings(null)
|
const { rerender } = await renderDirectorySettings()
|
||||||
|
|
||||||
await renderDirectorySettings()
|
expect(await screen.findByText('目录1')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('目录3')).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('checkbox', { name: '挂载盘删除空目录' })).toBeChecked()
|
||||||
|
expect(getRenameEditors().map(input => (input as HTMLTextAreaElement).value)).toEqual([
|
||||||
|
'{{ title }}',
|
||||||
|
'{{ artist }}',
|
||||||
|
'{{ name }}',
|
||||||
|
])
|
||||||
|
|
||||||
expect(await screen.findByRole('checkbox', { name: '挂载盘删除空目录' })).toBeChecked()
|
const refreshOptions = mocks.useSilentSettingRefresh.mock.calls[0]?.[1]
|
||||||
|
expect(computed(() => refreshOptions.active.value).value).toBe(true)
|
||||||
|
await rerender({ active: false })
|
||||||
|
expect(refreshOptions.active.value).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('lists every supported music metadata source for scraping', async () => {
|
it('adds a non-conflicting directory name, updates paths, and saves current priority order', async () => {
|
||||||
mockSettings(null)
|
|
||||||
await renderDirectorySettings()
|
|
||||||
|
|
||||||
const organizeCard = (await screen.findByText('整理 & 刮削')).closest('.v-card')
|
|
||||||
expect(organizeCard).not.toBeNull()
|
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
await user.click(within(organizeCard as HTMLElement).getByLabelText('刮削数据源'))
|
|
||||||
|
|
||||||
expect(await screen.findByRole('option', { name: 'MusicBrainz' })).toBeInTheDocument()
|
|
||||||
expect(screen.getByRole('option', { name: 'TheAudioDB' })).toBeInTheDocument()
|
|
||||||
expect(screen.getByRole('option', { name: '豆瓣音乐' })).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('saves the disabled value with the organization settings', async () => {
|
|
||||||
mockSettings(true)
|
|
||||||
await renderDirectorySettings()
|
await renderDirectorySettings()
|
||||||
const cleanupSwitch = await screen.findByRole('checkbox', { name: '挂载盘删除空目录' })
|
await screen.findByText('目录1')
|
||||||
await fireEvent.click(cleanupSwitch)
|
const directoryCard = getCard('目录')
|
||||||
|
const actionButtons = directoryCard.getAllByRole('button')
|
||||||
|
await user.click(actionButtons.at(-2)!)
|
||||||
|
|
||||||
const organizeCard = screen.getByText('整理 & 刮削').closest('.v-card')
|
expect(screen.getByText('目录4')).toBeInTheDocument()
|
||||||
expect(organizeCard).not.toBeNull()
|
await user.click(screen.getByRole('button', { name: 'paths-目录1' }))
|
||||||
await fireEvent.click(within(organizeCard as HTMLElement).getByRole('button', { name: '保存' }))
|
await user.click(screen.getByRole('button', { name: 'reverse-目录1' }))
|
||||||
|
await user.click(directoryCard.getByRole('button', { name: '保存' }))
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
expect(mocks.apiPost).toHaveBeenCalledWith('system/setting/Directories', [
|
||||||
|
expect.objectContaining({ name: '目录4', priority: 0 }),
|
||||||
|
expect.objectContaining({ name: '目录3', priority: 1 }),
|
||||||
|
expect.objectContaining({
|
||||||
|
name: '目录1',
|
||||||
|
priority: 2,
|
||||||
|
download_path: '/new-download',
|
||||||
|
library_path: '/new-library',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
expect(mocks.toastSuccess).toHaveBeenCalledWith('目录设置保存成功')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('blocks duplicate directory names before the API call', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
await renderDirectorySettings()
|
||||||
|
await screen.findByText('目录3')
|
||||||
|
await user.click(screen.getByRole('button', { name: 'rename-目录3' }))
|
||||||
|
await user.click(getCard('目录').getByRole('button', { name: '保存' }))
|
||||||
|
|
||||||
|
expect(mocks.apiPost).not.toHaveBeenCalledWith('system/setting/Directories', expect.anything())
|
||||||
|
expect(mocks.toastError).toHaveBeenCalledWith('存在重复目录名称!无法保存,请修改!')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes directories and storages and persists the remaining collections', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
await renderDirectorySettings()
|
||||||
|
await screen.findByText('目录3')
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'remove-目录1' }))
|
||||||
|
await user.click(getCard('目录').getByRole('button', { name: '保存' }))
|
||||||
|
expect(mocks.apiPost).toHaveBeenCalledWith('system/setting/Directories', [
|
||||||
|
expect.objectContaining({ name: '目录3', priority: 0 }),
|
||||||
|
])
|
||||||
|
|
||||||
|
mocks.apiPost.mockClear()
|
||||||
|
await user.click(screen.getByRole('button', { name: 'remove-自定义存储 1' }))
|
||||||
|
await user.click(getCard('存储').getByRole('button', { name: '保存' }))
|
||||||
|
expect(mocks.apiPost).toHaveBeenCalledWith('system/setting/Storages', [
|
||||||
|
expect.objectContaining({ name: '本地存储', type: 'local' }),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('filters existing storage types and creates unique custom storage names', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
await renderDirectorySettings()
|
||||||
|
await screen.findByText('自定义存储 1')
|
||||||
|
const storageCard = getCard('存储')
|
||||||
|
const actionButtons = storageCard.getAllByRole('button')
|
||||||
|
await user.click(actionButtons.at(-1)!)
|
||||||
|
|
||||||
|
expect(screen.queryByText('本地', { selector: '.v-list-item-title' })).not.toBeInTheDocument()
|
||||||
|
await user.click(await screen.findByText('自定义', { selector: '.v-list-item-title' }))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mocks.apiPost).toHaveBeenCalledWith(
|
||||||
|
'system/setting/Storages',
|
||||||
|
expect.arrayContaining([expect.objectContaining({ name: '自定义 3', type: 'custom3' })]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('converts cleared rename formats to null and requires both organization saves to succeed', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
await renderDirectorySettings()
|
||||||
|
await screen.findByText('整理 & 刮削')
|
||||||
|
for (const editor of getRenameEditors()) await fireEvent.update(editor, '')
|
||||||
|
await user.click(screen.getByRole('checkbox', { name: '挂载盘删除空目录' }))
|
||||||
|
await user.click(getCard('整理 & 刮削').getByRole('button', { name: '保存' }))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mocks.apiPost).toHaveBeenCalledWith('system/env', {
|
||||||
|
SCRAP_SOURCE: 'themoviedb',
|
||||||
|
MOVIE_RENAME_FORMAT: null,
|
||||||
|
TV_RENAME_FORMAT: null,
|
||||||
|
MUSIC_RENAME_FORMAT: null,
|
||||||
|
})
|
||||||
expect(mocks.apiPost).toHaveBeenCalledWith('system/setting/MountedLocalDiskDeleteEmptyDirs', false)
|
expect(mocks.apiPost).toHaveBeenCalledWith('system/setting/MountedLocalDiskDeleteEmptyDirs', false)
|
||||||
})
|
})
|
||||||
expect(mocks.toastSuccess).toHaveBeenCalledWith('整理选项设置保存成功')
|
expect(mocks.toastSuccess).toHaveBeenCalledWith('整理选项设置保存成功')
|
||||||
|
|
||||||
|
mocks.apiPost.mockReset()
|
||||||
|
mocks.toastSuccess.mockReset()
|
||||||
|
mocks.apiPost.mockResolvedValueOnce({ success: true }).mockResolvedValueOnce({ success: false })
|
||||||
|
await user.click(getCard('整理 & 刮削').getByRole('button', { name: '保存' }))
|
||||||
|
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('整理选项设置保存失败!'))
|
||||||
|
expect(mocks.toastSuccess).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reports HTTP failures for storage, directory, and organization saves', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
await renderDirectorySettings()
|
||||||
|
await screen.findByText('目录1')
|
||||||
|
|
||||||
|
mocks.apiPost.mockRejectedValueOnce(new Error('offline'))
|
||||||
|
await user.click(getCard('存储').getByRole('button', { name: '保存' }))
|
||||||
|
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('存储设置保存失败!'))
|
||||||
|
|
||||||
|
mocks.apiPost.mockRejectedValueOnce(new Error('offline'))
|
||||||
|
await user.click(getCard('目录').getByRole('button', { name: '保存' }))
|
||||||
|
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('目录设置保存失败!'))
|
||||||
|
|
||||||
|
mocks.apiPost.mockRejectedValueOnce(new Error('offline'))
|
||||||
|
await user.click(getCard('整理 & 刮削').getByRole('button', { name: '保存' }))
|
||||||
|
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('整理选项设置保存失败!'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens the shared category editor and reloads storage data after a card completes', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
await renderDirectorySettings()
|
||||||
|
await screen.findByText('本地存储')
|
||||||
|
const directoryCard = getCard('目录')
|
||||||
|
await user.click(directoryCard.getByRole('button', { name: '分类策略' }))
|
||||||
|
expect(mocks.openSharedDialog).toHaveBeenCalledOnce()
|
||||||
|
|
||||||
|
const initialStorageLoads = mocks.apiGet.mock.calls.filter(
|
||||||
|
([url]) => url === 'system/setting/public/Storages',
|
||||||
|
).length
|
||||||
|
await user.click(screen.getByRole('button', { name: 'reload-本地存储' }))
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mocks.apiGet.mock.calls.filter(([url]) => url === 'system/setting/public/Storages')).toHaveLength(
|
||||||
|
initialStorageLoads + 1,
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,373 @@
|
|||||||
|
import AccountSettingRule from '@/views/setting/AccountSettingRule.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(),
|
||||||
|
copyToClipboard: vi.fn(),
|
||||||
|
openSharedDialog: vi.fn(),
|
||||||
|
toastError: vi.fn(),
|
||||||
|
toastSuccess: vi.fn(),
|
||||||
|
toastWarning: 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, warning: mocks.toastWarning }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/@core/utils/navigator', () => ({
|
||||||
|
copyToClipboard: mocks.copyToClipboard,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/composables/useSharedDialog', () => ({
|
||||||
|
openSharedDialog: mocks.openSharedDialog,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/composables/useSilentSettingRefresh', () => ({
|
||||||
|
useSilentSettingRefresh: mocks.useSilentSettingRefresh,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/components/cards/CustomRuleCard.vue', async () => {
|
||||||
|
const { defineComponent } = await import('vue')
|
||||||
|
return {
|
||||||
|
default: defineComponent({
|
||||||
|
name: 'CustomRuleCardStub',
|
||||||
|
props: { rule: { type: Object, required: true } },
|
||||||
|
emits: ['close', 'change'],
|
||||||
|
template: `
|
||||||
|
<section :aria-label="'custom-' + rule.id">
|
||||||
|
<span>{{ rule.id }} / {{ rule.name }}</span>
|
||||||
|
<input
|
||||||
|
:aria-label="'custom-id-' + rule.id"
|
||||||
|
:value="rule.id"
|
||||||
|
@input="$emit('change', { ...rule, id: $event.target.value }, rule.id)"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
:aria-label="'custom-name-' + rule.id"
|
||||||
|
:value="rule.name"
|
||||||
|
@input="$emit('change', { ...rule, name: $event.target.value }, rule.id)"
|
||||||
|
/>
|
||||||
|
<button :aria-label="'remove-custom-' + rule.id" @click="$emit('close')">remove</button>
|
||||||
|
</section>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('@/components/cards/FilterRuleGroupCard.vue', async () => {
|
||||||
|
const { defineComponent } = await import('vue')
|
||||||
|
return {
|
||||||
|
default: defineComponent({
|
||||||
|
name: 'FilterRuleGroupCardStub',
|
||||||
|
props: { group: { type: Object, required: true } },
|
||||||
|
emits: ['close', 'change'],
|
||||||
|
template: `
|
||||||
|
<section :aria-label="'group-' + group.name">
|
||||||
|
<span>{{ group.name }}</span>
|
||||||
|
<input
|
||||||
|
:aria-label="'group-name-' + group.name"
|
||||||
|
:value="group.name"
|
||||||
|
@input="$emit('change', { ...group, name: $event.target.value }, group.name)"
|
||||||
|
/>
|
||||||
|
<button :aria-label="'remove-group-' + group.name" @click="$emit('close')">remove</button>
|
||||||
|
</section>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('vuedraggable', async () => {
|
||||||
|
const { defineComponent, h } = await import('vue')
|
||||||
|
return {
|
||||||
|
default: defineComponent({
|
||||||
|
name: 'DraggableStub',
|
||||||
|
props: { modelValue: { type: Array, default: () => [] } },
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
setup(props, { emit, slots }) {
|
||||||
|
const reverse = () => emit('update:modelValue', [...props.modelValue].reverse())
|
||||||
|
return () => {
|
||||||
|
const items = props.modelValue as Array<{ name?: string }>
|
||||||
|
return h('div', [
|
||||||
|
h('button', { 'aria-label': `reverse-${items[0]?.name ?? 'empty'}`, onClick: reverse }, 'reverse'),
|
||||||
|
...items.map(element => slots.item?.({ element })),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const customRulesFixture = [
|
||||||
|
{ id: 'RULE1', name: '规则1', include: 'WEB-DL' },
|
||||||
|
{ id: 'RULE3', name: '规则3', exclude: 'CAM' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const groupsFixture = [
|
||||||
|
{ name: '规则组1', rule_string: 'RULE1', media_type: '电影', category: '' },
|
||||||
|
{ name: '规则组3', rule_string: 'RULE3', media_type: '', category: '' },
|
||||||
|
]
|
||||||
|
|
||||||
|
function mockLoadedRules() {
|
||||||
|
mocks.apiGet.mockImplementation((endpoint: string) => {
|
||||||
|
if (endpoint === 'media/category') return { 电影: ['华语'] }
|
||||||
|
if (endpoint === 'system/setting/CustomFilterRules') {
|
||||||
|
return { success: true, data: { value: structuredClone(customRulesFixture) } }
|
||||||
|
}
|
||||||
|
if (endpoint === 'system/setting/UserFilterRuleGroups') {
|
||||||
|
return { success: true, data: { value: structuredClone(groupsFixture) } }
|
||||||
|
}
|
||||||
|
if (endpoint === 'system/setting/TorrentsPriority') {
|
||||||
|
return { success: true, data: { value: ['site', 'seeder'] } }
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected GET ${endpoint}`)
|
||||||
|
})
|
||||||
|
mocks.apiPost.mockResolvedValue({ success: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderRuleSettings() {
|
||||||
|
return renderWithProviders(AccountSettingRule)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCard(title: string) {
|
||||||
|
const card = screen.getByText(title).closest('.v-card')
|
||||||
|
expect(card).not.toBeNull()
|
||||||
|
return within(card as HTMLElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCommandButtons(title: string) {
|
||||||
|
const card = screen.getByText(title).closest('.v-card')
|
||||||
|
expect(card).not.toBeNull()
|
||||||
|
return Array.from((card as HTMLElement).querySelectorAll<HTMLButtonElement>('button.v-btn'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImportSave(callIndex: number) {
|
||||||
|
return mocks.openSharedDialog.mock.calls[callIndex]?.[2]?.save as (type: string, value: { value: string }) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('AccountSettingRule', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||||
|
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||||
|
mocks.apiGet.mockReset()
|
||||||
|
mocks.apiPost.mockReset()
|
||||||
|
mocks.copyToClipboard.mockReset()
|
||||||
|
mocks.openSharedDialog.mockReset()
|
||||||
|
mocks.toastError.mockReset()
|
||||||
|
mocks.toastSuccess.mockReset()
|
||||||
|
mocks.toastWarning.mockReset()
|
||||||
|
mocks.useSilentSettingRefresh.mockReset()
|
||||||
|
mocks.copyToClipboard.mockResolvedValue(true)
|
||||||
|
mockLoadedRules()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('loads rules, groups, priority, and follows active refresh state', async () => {
|
||||||
|
const { rerender } = await renderRuleSettings()
|
||||||
|
|
||||||
|
expect(await screen.findByText('RULE1 / 规则1')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('规则组1')).toBeInTheDocument()
|
||||||
|
expect(screen.getByLabelText('当前使用下载优先规则')).toHaveValue('site, seeder')
|
||||||
|
|
||||||
|
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 custom rules, groups, and torrent priority in current order', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
await renderRuleSettings()
|
||||||
|
await screen.findByText('RULE1 / 规则1')
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'reverse-规则1' }))
|
||||||
|
await user.click(getCard('自定义规则').getByRole('button', { name: '保存' }))
|
||||||
|
expect(mocks.apiPost).toHaveBeenCalledWith('system/setting/CustomFilterRules', [
|
||||||
|
expect.objectContaining({ id: 'RULE3' }),
|
||||||
|
expect.objectContaining({ id: 'RULE1' }),
|
||||||
|
])
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'reverse-规则组1' }))
|
||||||
|
await user.click(getCard('优先级规则组').getByRole('button', { name: '保存' }))
|
||||||
|
expect(mocks.apiPost).toHaveBeenCalledWith('system/setting/UserFilterRuleGroups', [
|
||||||
|
expect.objectContaining({ name: '规则组3' }),
|
||||||
|
expect.objectContaining({ name: '规则组1' }),
|
||||||
|
])
|
||||||
|
|
||||||
|
await user.click(getCard('下载规则').getByRole('button', { name: '保存' }))
|
||||||
|
expect(mocks.apiPost).toHaveBeenCalledWith('system/setting/TorrentsPriority', ['site', 'seeder'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds unique automatic names and removes the selected items', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
await renderRuleSettings()
|
||||||
|
await screen.findByText('RULE1 / 规则1')
|
||||||
|
|
||||||
|
await user.click(getCommandButtons('自定义规则')[1])
|
||||||
|
expect(screen.getByText('RULE4 / 规则4')).toBeInTheDocument()
|
||||||
|
await user.click(screen.getByRole('button', { name: 'remove-custom-RULE1' }))
|
||||||
|
expect(screen.queryByText('RULE1 / 规则1')).not.toBeInTheDocument()
|
||||||
|
|
||||||
|
await user.click(getCommandButtons('优先级规则组')[1])
|
||||||
|
expect(screen.getByText('规则组4')).toBeInTheDocument()
|
||||||
|
await user.click(screen.getByRole('button', { name: 'remove-group-规则组1' }))
|
||||||
|
expect(screen.queryByText('规则组1')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('blocks empty and duplicate custom rule identifiers or names', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
await renderRuleSettings()
|
||||||
|
await screen.findByText('RULE1 / 规则1')
|
||||||
|
const save = getCard('自定义规则').getByRole('button', { name: '保存' })
|
||||||
|
const idInput = screen.getByLabelText('custom-id-RULE1')
|
||||||
|
const nameInput = screen.getByLabelText('custom-name-RULE1')
|
||||||
|
|
||||||
|
await fireEvent.update(idInput, '')
|
||||||
|
await user.click(save)
|
||||||
|
expect(mocks.toastError).toHaveBeenCalledWith('存在空ID的规则,无法保存,请修改!')
|
||||||
|
|
||||||
|
await fireEvent.update(idInput, 'RULE3')
|
||||||
|
await user.click(save)
|
||||||
|
expect(mocks.toastError).toHaveBeenCalledWith('存在重复规则ID!无法保存,请修改!')
|
||||||
|
|
||||||
|
await fireEvent.update(idInput, 'RULE1')
|
||||||
|
await fireEvent.update(nameInput, '')
|
||||||
|
await user.click(save)
|
||||||
|
expect(mocks.toastError).toHaveBeenCalledWith('存在空名字的规则,无法保存,请修改!')
|
||||||
|
|
||||||
|
await fireEvent.update(nameInput, '规则3')
|
||||||
|
await user.click(save)
|
||||||
|
expect(mocks.toastError).toHaveBeenCalledWith('存在重复规则名称!无法保存,请修改!')
|
||||||
|
expect(mocks.apiPost).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('blocks empty and duplicate rule group names', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
await renderRuleSettings()
|
||||||
|
await screen.findByText('规则组1')
|
||||||
|
const save = getCard('优先级规则组').getByRole('button', { name: '保存' })
|
||||||
|
const nameInput = screen.getByLabelText('group-name-规则组1')
|
||||||
|
|
||||||
|
await fireEvent.update(nameInput, '')
|
||||||
|
await user.click(save)
|
||||||
|
expect(mocks.toastError).toHaveBeenCalledWith('存在空名字的规则组!无法保存,请修改!')
|
||||||
|
|
||||||
|
await fireEvent.update(nameInput, '规则组3')
|
||||||
|
await user.click(save)
|
||||||
|
expect(mocks.toastError).toHaveBeenCalledWith('存在重复规则组名称!无法保存,请修改!')
|
||||||
|
expect(mocks.apiPost).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('imports only contract fields for custom rules and rule groups', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
await renderRuleSettings()
|
||||||
|
await screen.findByText('规则组1')
|
||||||
|
|
||||||
|
await user.click(getCommandButtons('自定义规则')[2])
|
||||||
|
getImportSave(0)('custom', {
|
||||||
|
value: JSON.stringify([{ id: 'RULE9', name: '规则9', include: 'REMUX', unexpected: 'ignored' }]),
|
||||||
|
})
|
||||||
|
expect(await screen.findByText('RULE9 / 规则9')).toBeInTheDocument()
|
||||||
|
await user.click(getCard('自定义规则').getByRole('button', { name: '保存' }))
|
||||||
|
expect(mocks.apiPost).toHaveBeenLastCalledWith(
|
||||||
|
'system/setting/CustomFilterRules',
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
id: 'RULE9',
|
||||||
|
name: '规则9',
|
||||||
|
include: 'REMUX',
|
||||||
|
exclude: undefined,
|
||||||
|
size_range: undefined,
|
||||||
|
seeders: undefined,
|
||||||
|
publish_time: undefined,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
|
||||||
|
await user.click(getCommandButtons('优先级规则组')[2])
|
||||||
|
getImportSave(1)('group', {
|
||||||
|
value: JSON.stringify([{ name: '规则组9', rule_string: 'RULE9', media_type: '电影', extra: true }]),
|
||||||
|
})
|
||||||
|
expect(await screen.findByText('规则组9')).toBeInTheDocument()
|
||||||
|
await user.click(getCard('优先级规则组').getByRole('button', { name: '保存' }))
|
||||||
|
expect(mocks.apiPost).toHaveBeenLastCalledWith(
|
||||||
|
'system/setting/UserFilterRuleGroups',
|
||||||
|
expect.arrayContaining([{ name: '规则组9', rule_string: 'RULE9', media_type: '电影', category: undefined }]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects malformed or structurally invalid imports without mutating rules', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
await renderRuleSettings()
|
||||||
|
await screen.findByText('RULE1 / 规则1')
|
||||||
|
|
||||||
|
await user.click(getCommandButtons('自定义规则')[2])
|
||||||
|
getImportSave(0)('custom', { value: '{bad json' })
|
||||||
|
expect(mocks.toastError).toHaveBeenCalledWith('导入规则失败!无法解析输入的数据!')
|
||||||
|
expect(screen.queryByText('RULE9 / 规则9')).not.toBeInTheDocument()
|
||||||
|
|
||||||
|
getImportSave(0)('unknown', { value: JSON.stringify([{ id: 'RULE9', name: '规则9' }]) })
|
||||||
|
expect(mocks.toastError).toHaveBeenCalledWith('导入规则失败!未知的数据类型!')
|
||||||
|
expect(screen.queryByText('RULE9 / 规则9')).not.toBeInTheDocument()
|
||||||
|
|
||||||
|
getImportSave(0)('custom', { value: JSON.stringify([{ name: '缺少ID' }]) })
|
||||||
|
expect(mocks.toastError).toHaveBeenCalledWith('导入失败!发现有规则不存在ID,可能属于优先级规则组!')
|
||||||
|
|
||||||
|
await user.click(getCommandButtons('优先级规则组')[2])
|
||||||
|
getImportSave(1)('group', { value: JSON.stringify([{ id: 'RULE1', name: '不合法规则组' }]) })
|
||||||
|
expect(mocks.toastError).toHaveBeenCalledWith('导入失败!发现有规则存在相同ID,可能属于自定义规则!')
|
||||||
|
expect(screen.queryByText('不合法规则组')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shares and clears each collection through its command group', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
await renderRuleSettings()
|
||||||
|
await screen.findByText('RULE1 / 规则1')
|
||||||
|
|
||||||
|
await user.click(getCommandButtons('自定义规则')[3])
|
||||||
|
expect(mocks.copyToClipboard).toHaveBeenCalledWith(JSON.stringify(customRulesFixture))
|
||||||
|
await waitFor(() => expect(mocks.toastSuccess).toHaveBeenCalledWith('自定义规则已复制到剪贴板!'))
|
||||||
|
await user.click(getCommandButtons('自定义规则')[4])
|
||||||
|
expect(screen.queryByText('RULE1 / 规则1')).not.toBeInTheDocument()
|
||||||
|
|
||||||
|
mocks.copyToClipboard.mockResolvedValueOnce(false)
|
||||||
|
await user.click(getCommandButtons('优先级规则组')[3])
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(mocks.toastError).toHaveBeenCalledWith('优先级规则组复制失败:可能是浏览器不支持或被用户阻止!'),
|
||||||
|
)
|
||||||
|
await user.click(getCommandButtons('优先级规则组')[4])
|
||||||
|
expect(screen.queryByText('规则组1')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reports business and HTTP failures for each save responsibility', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
await renderRuleSettings()
|
||||||
|
await screen.findByText('RULE1 / 规则1')
|
||||||
|
|
||||||
|
const responsibilities = [
|
||||||
|
{ card: '自定义规则', failure: '自定义规则保存失败!' },
|
||||||
|
{ card: '优先级规则组', failure: '优先级规则组保存失败!' },
|
||||||
|
{ card: '下载规则', failure: '优先规则保存失败!' },
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const responsibility of responsibilities) {
|
||||||
|
mocks.apiPost.mockResolvedValueOnce({ success: false })
|
||||||
|
await user.click(getCard(responsibility.card).getByRole('button', { name: '保存' }))
|
||||||
|
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith(responsibility.failure))
|
||||||
|
expect(mocks.toastSuccess).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
mocks.toastError.mockClear()
|
||||||
|
mocks.apiPost.mockRejectedValueOnce(new Error('offline'))
|
||||||
|
await user.click(getCard(responsibility.card).getByRole('button', { name: '保存' }))
|
||||||
|
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith(responsibility.failure))
|
||||||
|
expect(mocks.toastSuccess).not.toHaveBeenCalled()
|
||||||
|
mocks.toastError.mockClear()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -388,6 +388,8 @@ 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/AccountSettingDirectory.vue',
|
||||||
|
'src/views/setting/AccountSettingRule.vue',
|
||||||
'src/views/setting/AccountSettingSearch.vue',
|
'src/views/setting/AccountSettingSearch.vue',
|
||||||
'src/views/setting/AccountSettingSite.vue',
|
'src/views/setting/AccountSettingSite.vue',
|
||||||
'src/views/setting/AccountSettingSubscribe.vue',
|
'src/views/setting/AccountSettingSubscribe.vue',
|
||||||
@@ -447,6 +449,18 @@ export default defineConfig(({ command, mode, isPreview }) => ({
|
|||||||
lines: 80,
|
lines: 80,
|
||||||
statements: 80,
|
statements: 80,
|
||||||
},
|
},
|
||||||
|
'src/views/setting/AccountSettingDirectory.vue': {
|
||||||
|
branches: 75,
|
||||||
|
functions: 80,
|
||||||
|
lines: 80,
|
||||||
|
statements: 80,
|
||||||
|
},
|
||||||
|
'src/views/setting/AccountSettingRule.vue': {
|
||||||
|
branches: 75,
|
||||||
|
functions: 80,
|
||||||
|
lines: 80,
|
||||||
|
statements: 80,
|
||||||
|
},
|
||||||
'src/views/setting/AccountSettingSearch.vue': {
|
'src/views/setting/AccountSettingSearch.vue': {
|
||||||
branches: 75,
|
branches: 75,
|
||||||
functions: 80,
|
functions: 80,
|
||||||
|
|||||||
Reference in New Issue
Block a user