mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-04 23:18:44 +08:00
test(settings): cover notification configuration (#674)
This commit is contained in:
@@ -809,11 +809,6 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/views/setting/AccountSettingNotification.vue": {
|
|
||||||
"sonarjs/no-all-duplicated-branches": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/views/setting/AccountSettingRule.vue": {
|
"src/views/setting/AccountSettingRule.vue": {
|
||||||
"@typescript-eslint/no-explicit-any": {
|
"@typescript-eslint/no-explicit-any": {
|
||||||
"count": 8
|
"count": 8
|
||||||
|
|||||||
@@ -283,6 +283,22 @@ describe('MediaCard', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps the local-exists marker hidden when the exists request reports a business failure', async () => {
|
||||||
|
const media = createMediaInfo({ title: '存在查询失败电影', tmdb_id: 9103 })
|
||||||
|
const existsRequest = vi.fn<(url: URL) => void>()
|
||||||
|
server.use(
|
||||||
|
querySubscribeByMediaHandler('9103', {}),
|
||||||
|
mediaExistsHandler({ data: {}, message: '查询失败', success: false }, 200, existsRequest),
|
||||||
|
)
|
||||||
|
|
||||||
|
const { container } = await renderCard(media)
|
||||||
|
getStatusObservers()[0]?.trigger()
|
||||||
|
|
||||||
|
await waitFor(() => expect(existsRequest).toHaveBeenCalledOnce())
|
||||||
|
await waitFor(() => expect(console.error).toHaveBeenCalled())
|
||||||
|
expect(container.querySelector('.bg-green-500')).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
['TMDB', createMediaInfo({ season: 3, tmdb_id: 9201, type: '电视剧' }), '9201', '3'],
|
['TMDB', createMediaInfo({ season: 3, tmdb_id: 9201, type: '电视剧' }), '9201', '3'],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -292,6 +292,27 @@ describe('AddSubtitleDownloadDialog submissions', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('reveals advanced options and submits the selected media source identity', async () => {
|
||||||
|
const submitted = vi.fn()
|
||||||
|
server.use(subtitleDownloadHandler({ data: null, success: true }, 200, submitted))
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
await renderDialog({ mediaId: '84', mediaSource: 'themoviedb' })
|
||||||
|
|
||||||
|
expect(screen.getByLabelText('识别数据源')).not.toBeVisible()
|
||||||
|
await user.click(screen.getByRole('button', { name: '显示高级选项' }))
|
||||||
|
await user.selectOptions(screen.getByLabelText('识别数据源'), 'imdb')
|
||||||
|
await user.clear(screen.getByLabelText('IMDb ID'))
|
||||||
|
await user.type(screen.getByLabelText('IMDb ID'), 'tt0111161')
|
||||||
|
await user.click(screen.getByRole('button', { name: '下载字幕' }))
|
||||||
|
|
||||||
|
await waitFor(() => expect(submitted).toHaveBeenCalledOnce())
|
||||||
|
expect(submitted.mock.calls[0][0]).toMatchObject({
|
||||||
|
media_id: 'tt0111161',
|
||||||
|
media_source: 'imdb',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('preserves the signed enclosure in download/subtitle and prevents duplicate submission', async () => {
|
it('preserves the signed enclosure in download/subtitle and prevents duplicate submission', async () => {
|
||||||
const deferred = createDeferred<JsonBodyType>()
|
const deferred = createDeferred<JsonBodyType>()
|
||||||
const submitted = vi.fn()
|
const submitted = vi.fn()
|
||||||
|
|||||||
@@ -206,14 +206,15 @@ function trackWechatClawBotRename(oldName: string, newName: string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const renameMap = { ...wechatClawBotRenameMap.value }
|
const renameMap = { ...wechatClawBotRenameMap.value }
|
||||||
|
let chainedRename = false
|
||||||
|
// 连续改名只保留原始缓存名到当前渠道名,避免为不存在的中间名发起迁移。
|
||||||
for (const [source, target] of Object.entries(renameMap)) {
|
for (const [source, target] of Object.entries(renameMap)) {
|
||||||
if (target === oldName) {
|
if (target === oldName) {
|
||||||
renameMap[source] = newName
|
renameMap[source] = newName
|
||||||
|
chainedRename = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (renameMap[oldName]) {
|
if (!chainedRename) {
|
||||||
renameMap[oldName] = newName
|
|
||||||
} else {
|
|
||||||
renameMap[oldName] = newName
|
renameMap[oldName] = newName
|
||||||
}
|
}
|
||||||
wechatClawBotRenameMap.value = Object.fromEntries(
|
wechatClawBotRenameMap.value = Object.fromEntries(
|
||||||
@@ -325,6 +326,7 @@ async function saveNotificationTime() {
|
|||||||
$toast.success(t('setting.notification.timeSaveSuccess'))
|
$toast.success(t('setting.notification.timeSaveSuccess'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
|
$toast.error(t('setting.notification.timeSaveFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,6 +369,7 @@ async function saveNotificationSwitchs() {
|
|||||||
$toast.success(t('setting.notification.switchSaveSuccess'))
|
$toast.success(t('setting.notification.switchSaveSuccess'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
|
$toast.error(t('setting.notification.switchSaveFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,329 @@
|
|||||||
|
import vuetify from '@/plugins/vuetify'
|
||||||
|
import AccountSettingNotification from '@/views/setting/AccountSettingNotification.vue'
|
||||||
|
import { fireEvent, screen, waitFor, within } from '@testing-library/vue'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { renderWithProviders } from '@tests/support/render'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
apiGet: vi.fn(),
|
||||||
|
apiPost: vi.fn(),
|
||||||
|
openSharedDialog: vi.fn(),
|
||||||
|
toastError: vi.fn(),
|
||||||
|
toastSuccess: vi.fn(),
|
||||||
|
useSilentSettingRefresh: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/api', () => ({
|
||||||
|
default: createDataApiMock({ get: mocks.apiGet, post: mocks.apiPost }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('vue-toastification', () => ({
|
||||||
|
useToast: () => ({ error: mocks.toastError, success: mocks.toastSuccess }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/composables/useSharedDialog', () => ({
|
||||||
|
openSharedDialog: mocks.openSharedDialog,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/composables/useSilentSettingRefresh', () => ({
|
||||||
|
useSilentSettingRefresh: mocks.useSilentSettingRefresh,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/components/cards/NotificationChannelCard.vue', async () => {
|
||||||
|
const { defineComponent } = await import('vue')
|
||||||
|
return {
|
||||||
|
default: defineComponent({
|
||||||
|
name: 'NotificationChannelCardStub',
|
||||||
|
props: { notification: { type: Object, required: true } },
|
||||||
|
emits: ['change', 'close'],
|
||||||
|
template: `
|
||||||
|
<section :aria-label="'channel-' + notification.name">
|
||||||
|
<span>{{ notification.name }} / {{ notification.type }}</span>
|
||||||
|
<input
|
||||||
|
:aria-label="'name-' + notification.name"
|
||||||
|
:value="notification.name"
|
||||||
|
@input="$emit('change', { ...notification, name: $event.target.value }, notification.name)"
|
||||||
|
/>
|
||||||
|
<button :aria-label="'remove-' + notification.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-channels', onClick: reverse }, 'reverse'),
|
||||||
|
...items.map(element => slots.item?.({ element })),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const notificationsFixture = [
|
||||||
|
{ name: 'Alpha', type: 'wechatclawbot', enabled: true, config: { token: 'fixture-token' } },
|
||||||
|
{ name: '通知3', type: 'telegram', enabled: false, config: {} },
|
||||||
|
]
|
||||||
|
|
||||||
|
const templateFixture = {
|
||||||
|
organizeSuccess: '{"title":"organized"}',
|
||||||
|
downloadAdded: '{"title":"downloaded"}',
|
||||||
|
subscribeAdded: '{}',
|
||||||
|
subscribeComplete: '{}',
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockLoadedSettings() {
|
||||||
|
mocks.apiGet.mockImplementation((endpoint: string) => {
|
||||||
|
if (endpoint === 'system/setting/Notifications') {
|
||||||
|
return { success: true, data: { value: structuredClone(notificationsFixture) } }
|
||||||
|
}
|
||||||
|
if (endpoint === 'system/setting/NotificationSwitchs') {
|
||||||
|
return { success: true, data: { value: [{ type: '资源下载', action: 'user' }] } }
|
||||||
|
}
|
||||||
|
if (endpoint === 'system/setting/NotificationSendTime') {
|
||||||
|
return { success: true, data: { value: { start: '08:30', end: '22:00' } } }
|
||||||
|
}
|
||||||
|
if (endpoint === 'system/setting/NotificationTemplates') {
|
||||||
|
return { success: true, data: { value: structuredClone(templateFixture) } }
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected GET ${endpoint}`)
|
||||||
|
})
|
||||||
|
mocks.apiPost.mockResolvedValue({ success: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDialogController() {
|
||||||
|
return { close: vi.fn(), id: 1, updateProps: vi.fn() }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderNotificationSettings() {
|
||||||
|
return renderWithProviders(AccountSettingNotification)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCard(title: string) {
|
||||||
|
const card = screen.getByText(title).closest('.v-card')
|
||||||
|
expect(card).not.toBeNull()
|
||||||
|
return within(card as HTMLElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDialogEvents() {
|
||||||
|
return mocks.openSharedDialog.mock.calls.at(-1)?.[2] as {
|
||||||
|
close: () => void
|
||||||
|
save: (value: string) => Promise<void>
|
||||||
|
'update:content': (value: string) => void
|
||||||
|
'update:modelValue': (value: boolean) => void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('AccountSettingNotification', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||||
|
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||||
|
mocks.apiGet.mockReset()
|
||||||
|
mocks.apiPost.mockReset()
|
||||||
|
mocks.openSharedDialog.mockReset()
|
||||||
|
mocks.toastError.mockReset()
|
||||||
|
mocks.toastSuccess.mockReset()
|
||||||
|
mocks.useSilentSettingRefresh.mockReset()
|
||||||
|
mocks.openSharedDialog.mockImplementation(() => createDialogController())
|
||||||
|
mockLoadedSettings()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('loads owned settings, adds missing switch defaults, and follows active refresh state', async () => {
|
||||||
|
const { rerender } = await renderNotificationSettings()
|
||||||
|
|
||||||
|
expect(await screen.findByText('Alpha / wechatclawbot')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('通知3 / telegram')).toBeInTheDocument()
|
||||||
|
expect(screen.getByLabelText('开始时间')).toHaveValue('08:30')
|
||||||
|
expect(screen.getByLabelText('结束时间')).toHaveValue('22:00')
|
||||||
|
expect(getCard('通知发送范围').getAllByRole('radiogroup')).toHaveLength(9)
|
||||||
|
|
||||||
|
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('creates a unique automatic channel name, removes channels, and saves the current order', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
await renderNotificationSettings()
|
||||||
|
await screen.findByText('Alpha / wechatclawbot')
|
||||||
|
const channelCard = getCard('通知渠道')
|
||||||
|
|
||||||
|
const buttons = channelCard.getAllByRole('button')
|
||||||
|
await user.click(buttons.at(-1)!)
|
||||||
|
await user.click(await screen.findByText('企业微信', { selector: '.v-list-item-title' }))
|
||||||
|
expect(screen.getByText('通知4 / wechat')).toBeInTheDocument()
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'remove-通知3' }))
|
||||||
|
await user.click(screen.getByRole('button', { name: 'reverse-channels' }))
|
||||||
|
await user.click(channelCard.getByRole('button', { name: '保存' }))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mocks.apiPost).toHaveBeenCalledWith('system/setting/Notifications', [
|
||||||
|
expect.objectContaining({ name: '通知4', type: 'wechat' }),
|
||||||
|
expect.objectContaining({ name: 'Alpha', type: 'wechatclawbot' }),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
expect(mocks.toastSuccess).toHaveBeenCalledWith('通知设置保存成功')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('compresses chained ClawBot renames and migrates the original source before saving channels', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
await renderNotificationSettings()
|
||||||
|
await screen.findByText('Alpha / wechatclawbot')
|
||||||
|
|
||||||
|
await fireEvent.update(screen.getByLabelText('name-Alpha'), 'Beta')
|
||||||
|
await fireEvent.update(await screen.findByLabelText('name-Beta'), 'Gamma')
|
||||||
|
await user.click(getCard('通知渠道').getByRole('button', { name: '保存' }))
|
||||||
|
|
||||||
|
await waitFor(() => expect(mocks.apiPost).toHaveBeenCalledTimes(2))
|
||||||
|
expect(mocks.apiPost).toHaveBeenNthCalledWith(1, 'notification/wechatclawbot/migrate', null, {
|
||||||
|
params: { old_source: 'Alpha', new_source: 'Gamma' },
|
||||||
|
})
|
||||||
|
expect(mocks.apiPost).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
'system/setting/Notifications',
|
||||||
|
expect.arrayContaining([expect.objectContaining({ name: 'Gamma', type: 'wechatclawbot' })]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps a failed ClawBot migration pending and retries it before saving channels', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
await renderNotificationSettings()
|
||||||
|
await screen.findByText('Alpha / wechatclawbot')
|
||||||
|
await fireEvent.update(screen.getByLabelText('name-Alpha'), 'Beta')
|
||||||
|
const save = getCard('通知渠道').getByRole('button', { name: '保存' })
|
||||||
|
|
||||||
|
mocks.apiPost.mockResolvedValueOnce({ success: false, message: 'migration failed' })
|
||||||
|
await user.click(save)
|
||||||
|
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('通知设置保存失败!'))
|
||||||
|
expect(mocks.apiPost).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
mocks.apiPost.mockClear()
|
||||||
|
mocks.apiPost.mockResolvedValue({ success: true })
|
||||||
|
await user.click(save)
|
||||||
|
await waitFor(() => expect(mocks.apiPost).toHaveBeenCalledTimes(2))
|
||||||
|
expect(mocks.apiPost).toHaveBeenNthCalledWith(1, 'notification/wechatclawbot/migrate', null, {
|
||||||
|
params: { old_source: 'Alpha', new_source: 'Beta' },
|
||||||
|
})
|
||||||
|
expect(mocks.apiPost).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
'system/setting/Notifications',
|
||||||
|
expect.arrayContaining([expect.objectContaining({ name: 'Beta' })]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('loads, edits, and saves a template while pausing refresh and following the active theme', async () => {
|
||||||
|
const previousTheme = vuetify.theme.global.name.value
|
||||||
|
const controller = createDialogController()
|
||||||
|
mocks.openSharedDialog.mockReturnValue(controller)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
await renderNotificationSettings()
|
||||||
|
await screen.findByText('Alpha / wechatclawbot')
|
||||||
|
const templateButton = screen.getByRole('button', { name: /资源入库/ })
|
||||||
|
await user.click(templateButton)
|
||||||
|
|
||||||
|
await waitFor(() => expect(mocks.openSharedDialog).toHaveBeenCalledOnce())
|
||||||
|
expect(mocks.openSharedDialog.mock.calls[0]?.[1]).toEqual(
|
||||||
|
expect.objectContaining({ content: templateFixture.organizeSuccess, editorTheme: 'github_light_default' }),
|
||||||
|
)
|
||||||
|
expect(mocks.useSilentSettingRefresh.mock.calls[0]?.[1].active.value).toBe(false)
|
||||||
|
|
||||||
|
getDialogEvents()['update:content']('{"title":"updated"}')
|
||||||
|
vuetify.theme.global.name.value = 'dark'
|
||||||
|
await waitFor(() => expect(controller.updateProps).toHaveBeenCalledWith({ editorTheme: 'github_dark' }))
|
||||||
|
await getDialogEvents().save('{"title":"updated"}')
|
||||||
|
|
||||||
|
expect(mocks.apiPost).toHaveBeenCalledWith('system/setting/NotificationTemplates', {
|
||||||
|
...templateFixture,
|
||||||
|
organizeSuccess: '{"title":"updated"}',
|
||||||
|
})
|
||||||
|
expect(mocks.toastSuccess).toHaveBeenCalledWith('模板保存成功')
|
||||||
|
expect(controller.close).toHaveBeenCalledOnce()
|
||||||
|
} finally {
|
||||||
|
vuetify.theme.global.name.value = previousTheme
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps the template editor open when saving fails and reports load failures', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
const controller = createDialogController()
|
||||||
|
mocks.openSharedDialog.mockReturnValue(controller)
|
||||||
|
await renderNotificationSettings()
|
||||||
|
await screen.findByText('Alpha / wechatclawbot')
|
||||||
|
const templateButton = screen.getByRole('button', { name: /资源下载/ })
|
||||||
|
await user.click(templateButton)
|
||||||
|
await waitFor(() => expect(mocks.openSharedDialog).toHaveBeenCalledOnce())
|
||||||
|
|
||||||
|
mocks.apiPost.mockRejectedValueOnce(new Error('offline'))
|
||||||
|
await getDialogEvents().save('{"title":"failed"}')
|
||||||
|
expect(mocks.toastError).toHaveBeenCalledWith('模板保存失败!')
|
||||||
|
expect(controller.close).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
mocks.apiGet.mockImplementation((endpoint: string) => {
|
||||||
|
if (endpoint === 'system/setting/NotificationTemplates') throw new Error('offline')
|
||||||
|
return mockLoadedSettings()
|
||||||
|
})
|
||||||
|
mocks.openSharedDialog.mockClear()
|
||||||
|
await user.click(screen.getByRole('button', { name: /添加订阅/ }))
|
||||||
|
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('模板加载失败!'))
|
||||||
|
expect(mocks.openSharedDialog).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('saves edited notification time and merged message scope payloads', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
await renderNotificationSettings()
|
||||||
|
await screen.findByText('Alpha / wechatclawbot')
|
||||||
|
|
||||||
|
await fireEvent.update(screen.getByLabelText('开始时间'), '09:15')
|
||||||
|
await fireEvent.update(screen.getByLabelText('结束时间'), '21:45')
|
||||||
|
await user.click(getCard('通知发送时间').getByRole('button', { name: '保存' }))
|
||||||
|
expect(mocks.apiPost).toHaveBeenCalledWith('system/setting/NotificationSendTime', {
|
||||||
|
start: '09:15',
|
||||||
|
end: '21:45',
|
||||||
|
})
|
||||||
|
|
||||||
|
const scopeCard = getCard('通知发送范围')
|
||||||
|
await user.click(scopeCard.getAllByRole('radio', { name: '仅管理员' })[0])
|
||||||
|
await user.click(scopeCard.getByRole('button', { name: '保存' }))
|
||||||
|
expect(mocks.apiPost).toHaveBeenCalledWith(
|
||||||
|
'system/setting/NotificationSwitchs',
|
||||||
|
expect.arrayContaining([
|
||||||
|
{ type: '资源下载', action: 'admin' },
|
||||||
|
{ type: '智能体', action: 'admin' },
|
||||||
|
{ type: '其它', action: 'admin' },
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
expect(mocks.toastSuccess).toHaveBeenCalledWith('通知发送时间保存成功')
|
||||||
|
expect(mocks.toastSuccess).toHaveBeenCalledWith('消息类型开关保存成功')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reports HTTP failures when saving notification time or message scopes', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
await renderNotificationSettings()
|
||||||
|
await screen.findByText('Alpha / wechatclawbot')
|
||||||
|
|
||||||
|
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('消息类型开关保存失败!'))
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -389,6 +389,7 @@ export default defineConfig(({ command, mode, isPreview }) => ({
|
|||||||
'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/AccountSettingDirectory.vue',
|
||||||
|
'src/views/setting/AccountSettingNotification.vue',
|
||||||
'src/views/setting/AccountSettingRule.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',
|
||||||
@@ -455,6 +456,12 @@ export default defineConfig(({ command, mode, isPreview }) => ({
|
|||||||
lines: 80,
|
lines: 80,
|
||||||
statements: 80,
|
statements: 80,
|
||||||
},
|
},
|
||||||
|
'src/views/setting/AccountSettingNotification.vue': {
|
||||||
|
branches: 75,
|
||||||
|
functions: 80,
|
||||||
|
lines: 80,
|
||||||
|
statements: 80,
|
||||||
|
},
|
||||||
'src/views/setting/AccountSettingRule.vue': {
|
'src/views/setting/AccountSettingRule.vue': {
|
||||||
branches: 75,
|
branches: 75,
|
||||||
functions: 80,
|
functions: 80,
|
||||||
|
|||||||
Reference in New Issue
Block a user