mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-28 19:47:49 +08:00
test(workflow): cover dynamic action options (#712)
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import FetchTorrentsAction from '@/components/workflow/FetchTorrentsAction.vue'
|
||||
import { waitFor } from '@testing-library/vue'
|
||||
import { renderWithProviders } from '@tests/support/render'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getSelectItems, workflowActionStubs } from './workflowActionTestUtils'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
apiGet: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
default: createDataApiMock({
|
||||
get: (...args: unknown[]) => mocks.apiGet(...args),
|
||||
}),
|
||||
}))
|
||||
|
||||
async function renderAction() {
|
||||
return renderWithProviders(FetchTorrentsAction, {
|
||||
props: {
|
||||
id: 'fetch-torrents',
|
||||
data: { search_type: 'media', sites: [] },
|
||||
},
|
||||
global: { stubs: workflowActionStubs },
|
||||
})
|
||||
}
|
||||
|
||||
describe('FetchTorrentsAction', () => {
|
||||
beforeEach(() => {
|
||||
mocks.apiGet.mockReset()
|
||||
})
|
||||
|
||||
it('keeps active sites and maps their names and ids to options', async () => {
|
||||
mocks.apiGet.mockResolvedValue({
|
||||
success: true,
|
||||
message: '',
|
||||
data: [
|
||||
{ id: 1, name: '启用站点', is_active: true },
|
||||
{ id: 2, name: '停用站点', is_active: false },
|
||||
],
|
||||
})
|
||||
|
||||
const { container } = await renderAction()
|
||||
|
||||
await waitFor(() => expect(mocks.apiGet).toHaveBeenCalledWith('site/rss'))
|
||||
expect(getSelectItems(container, '站点')).toEqual([{ title: '启用站点', value: 1 }])
|
||||
})
|
||||
|
||||
it('keeps site options empty when loading fails', async () => {
|
||||
const error = new Error('site list unavailable')
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
mocks.apiGet.mockRejectedValue(error)
|
||||
|
||||
const { container } = await renderAction()
|
||||
|
||||
await waitFor(() => expect(consoleError).toHaveBeenCalled())
|
||||
expect(getSelectItems(container, '站点')).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,65 @@
|
||||
import FilterTorrentsAction from '@/components/workflow/FilterTorrentsAction.vue'
|
||||
import { waitFor } from '@testing-library/vue'
|
||||
import { renderWithProviders } from '@tests/support/render'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getSelectItems, workflowActionStubs } from './workflowActionTestUtils'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
apiGet: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
default: createDataApiMock({
|
||||
get: (...args: unknown[]) => mocks.apiGet(...args),
|
||||
}),
|
||||
}))
|
||||
|
||||
async function renderAction(initialState: Record<string, Record<string, unknown>> = {}) {
|
||||
return renderWithProviders(FilterTorrentsAction, {
|
||||
props: {
|
||||
id: 'filter-torrents',
|
||||
data: { rule_groups: [] },
|
||||
},
|
||||
initialState,
|
||||
global: { stubs: workflowActionStubs },
|
||||
})
|
||||
}
|
||||
|
||||
describe('FilterTorrentsAction', () => {
|
||||
beforeEach(() => {
|
||||
mocks.apiGet.mockReset()
|
||||
})
|
||||
|
||||
it('does not request admin filter groups for regular users', async () => {
|
||||
const { container } = await renderAction()
|
||||
|
||||
expect(mocks.apiGet).not.toHaveBeenCalled()
|
||||
expect(getSelectItems(container, '过滤规则组')).toEqual([])
|
||||
})
|
||||
|
||||
it('maps unwrapped admin filter groups to name options', async () => {
|
||||
mocks.apiGet.mockResolvedValue({
|
||||
success: true,
|
||||
message: '',
|
||||
data: { value: [{ name: '高清规则' }, { name: '字幕规则' }] },
|
||||
})
|
||||
|
||||
const { container } = await renderAction({ user: { superUser: true } })
|
||||
|
||||
await waitFor(() => expect(mocks.apiGet).toHaveBeenCalledWith('system/setting/UserFilterRuleGroups'))
|
||||
expect(getSelectItems(container, '过滤规则组')).toEqual([
|
||||
{ title: '高清规则', value: '高清规则' },
|
||||
{ title: '字幕规则', value: '字幕规则' },
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps filter group options empty when loading fails', async () => {
|
||||
const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
mocks.apiGet.mockRejectedValue(new Error('rule groups unavailable'))
|
||||
|
||||
const { container } = await renderAction({ user: { superUser: true } })
|
||||
|
||||
await waitFor(() => expect(consoleLog).toHaveBeenCalled())
|
||||
expect(getSelectItems(container, '过滤规则组')).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
import ScanFileAction from '@/components/workflow/ScanFileAction.vue'
|
||||
import { waitFor } from '@testing-library/vue'
|
||||
import { renderWithProviders } from '@tests/support/render'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getSelectItems, workflowActionStubs } from './workflowActionTestUtils'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
apiGet: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
default: createDataApiMock({
|
||||
get: (...args: unknown[]) => mocks.apiGet(...args),
|
||||
}),
|
||||
}))
|
||||
|
||||
async function renderAction() {
|
||||
return renderWithProviders(ScanFileAction, {
|
||||
props: {
|
||||
id: 'scan-file',
|
||||
data: { storage: '', directory: '' },
|
||||
},
|
||||
global: { stubs: workflowActionStubs },
|
||||
})
|
||||
}
|
||||
|
||||
describe('ScanFileAction', () => {
|
||||
beforeEach(() => {
|
||||
mocks.apiGet.mockReset()
|
||||
})
|
||||
|
||||
it('maps unwrapped storage names and types to options', async () => {
|
||||
mocks.apiGet.mockResolvedValue({
|
||||
success: true,
|
||||
message: '',
|
||||
data: {
|
||||
value: [
|
||||
{ name: '本地存储', type: 'local' },
|
||||
{ name: '阿里云盘', type: 'alipan' },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const { container } = await renderAction()
|
||||
|
||||
await waitFor(() => expect(mocks.apiGet).toHaveBeenCalledWith('system/setting/public/Storages'))
|
||||
expect(getSelectItems(container, '存储')).toEqual([
|
||||
{ title: '本地存储', value: 'local' },
|
||||
{ title: '阿里云盘', value: 'alipan' },
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,70 @@
|
||||
import SendMessageAction from '@/components/workflow/SendMessageAction.vue'
|
||||
import { waitFor } from '@testing-library/vue'
|
||||
import { renderWithProviders } from '@tests/support/render'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getSelectItems, workflowActionStubs } from './workflowActionTestUtils'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
apiGet: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
default: createDataApiMock({
|
||||
get: (...args: unknown[]) => mocks.apiGet(...args),
|
||||
}),
|
||||
}))
|
||||
|
||||
async function renderAction(initialState: Record<string, Record<string, unknown>> = {}) {
|
||||
return renderWithProviders(SendMessageAction, {
|
||||
props: {
|
||||
id: 'send-message',
|
||||
data: { client: [], userid: '' },
|
||||
},
|
||||
initialState,
|
||||
global: { stubs: workflowActionStubs },
|
||||
})
|
||||
}
|
||||
|
||||
describe('SendMessageAction', () => {
|
||||
beforeEach(() => {
|
||||
mocks.apiGet.mockReset()
|
||||
})
|
||||
|
||||
it('does not request admin notification channels for regular users', async () => {
|
||||
const { container } = await renderAction()
|
||||
|
||||
expect(mocks.apiGet).not.toHaveBeenCalled()
|
||||
expect(getSelectItems(container, '渠道')).toEqual([])
|
||||
})
|
||||
|
||||
it('maps unwrapped admin notification channels to name options', async () => {
|
||||
mocks.apiGet.mockResolvedValue({
|
||||
success: true,
|
||||
message: '',
|
||||
data: {
|
||||
value: [
|
||||
{ name: 'Telegram', type: 'telegram', enabled: true },
|
||||
{ name: '企业微信', type: 'wechat', enabled: false },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const { container } = await renderAction({ user: { superUser: true } })
|
||||
|
||||
await waitFor(() => expect(mocks.apiGet).toHaveBeenCalledWith('system/setting/Notifications'))
|
||||
expect(getSelectItems(container, '渠道')).toEqual([
|
||||
{ title: 'Telegram', value: 'Telegram' },
|
||||
{ title: '企业微信', value: '企业微信' },
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps notification options empty when loading fails', async () => {
|
||||
const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
mocks.apiGet.mockRejectedValue(new Error('notifications unavailable'))
|
||||
|
||||
const { container } = await renderAction({ user: { superUser: true } })
|
||||
|
||||
await waitFor(() => expect(consoleLog).toHaveBeenCalled())
|
||||
expect(getSelectItems(container, '渠道')).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,69 @@
|
||||
import { defineComponent, h } from 'vue'
|
||||
|
||||
/** 将布局和连接器替换为可预测的容器,测试只观察动作配置数据。 */
|
||||
export const BoxStub = defineComponent({
|
||||
name: 'WorkflowActionBoxStub',
|
||||
inheritAttrs: false,
|
||||
setup(_, { attrs, slots }) {
|
||||
return () => h('div', attrs, slots.default?.())
|
||||
},
|
||||
})
|
||||
|
||||
/** 暴露 VSelect 的 items,避免测试依赖 Vuetify 菜单和 Teleport 实现。 */
|
||||
export const SelectStub = defineComponent({
|
||||
name: 'WorkflowActionSelectStub',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
return () =>
|
||||
h('div', {
|
||||
'data-select-label': props.label,
|
||||
'data-select-items': JSON.stringify(props.items),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
/** 输入控件仅保留可渲染形状;v-model 和 Vuetify 内部行为不属于本组契约。 */
|
||||
export const InputStub = defineComponent({
|
||||
name: 'WorkflowActionInputStub',
|
||||
inheritAttrs: false,
|
||||
setup(_, { attrs }) {
|
||||
return () => h('input', { 'aria-label': typeof attrs.label === 'string' ? attrs.label : undefined })
|
||||
},
|
||||
})
|
||||
|
||||
export const workflowActionStubs = {
|
||||
Handle: BoxStub,
|
||||
VAvatar: BoxStub,
|
||||
VCard: BoxStub,
|
||||
VCardItem: BoxStub,
|
||||
VCardSubtitle: BoxStub,
|
||||
VCardText: BoxStub,
|
||||
VCardTitle: BoxStub,
|
||||
VCol: BoxStub,
|
||||
VDivider: BoxStub,
|
||||
VIcon: BoxStub,
|
||||
VPathField: InputStub,
|
||||
VRow: BoxStub,
|
||||
VSelect: SelectStub,
|
||||
VSwitch: InputStub,
|
||||
VTextField: InputStub,
|
||||
}
|
||||
|
||||
/** 按业务字段读取稳定 stub 暴露的选项。 */
|
||||
export function getSelectItems(container: ParentNode, label: string): unknown[] {
|
||||
const element = [...container.querySelectorAll<HTMLElement>('[data-select-items]')].find(
|
||||
item => item.dataset.selectLabel === label,
|
||||
)
|
||||
if (!element) throw new Error(`Select stub not found: ${label}`)
|
||||
return JSON.parse(element.dataset.selectItems ?? '[]') as unknown[]
|
||||
}
|
||||
@@ -366,6 +366,10 @@ export default defineConfig(({ command, mode, isPreview }) => ({
|
||||
'src/components/dialog/WorkflowActionsDialog.vue',
|
||||
'src/components/workflow/InvokePluginAction.vue',
|
||||
'src/components/workflow/WorkflowSidebar.vue',
|
||||
'src/components/workflow/FetchTorrentsAction.vue',
|
||||
'src/components/workflow/FilterTorrentsAction.vue',
|
||||
'src/components/workflow/ScanFileAction.vue',
|
||||
'src/components/workflow/SendMessageAction.vue',
|
||||
'src/components/dialog/OTPAuthDialog.vue',
|
||||
'src/components/dialog/PasskeyDialog.vue',
|
||||
'src/components/dialog/SiteCookieUpdateDialog.vue',
|
||||
@@ -608,6 +612,30 @@ export default defineConfig(({ command, mode, isPreview }) => ({
|
||||
lines: 85,
|
||||
statements: 85,
|
||||
},
|
||||
'src/components/workflow/FetchTorrentsAction.vue': {
|
||||
branches: 90,
|
||||
functions: 30,
|
||||
lines: 90,
|
||||
statements: 90,
|
||||
},
|
||||
'src/components/workflow/FilterTorrentsAction.vue': {
|
||||
branches: 85,
|
||||
functions: 10,
|
||||
lines: 90,
|
||||
statements: 90,
|
||||
},
|
||||
'src/components/workflow/ScanFileAction.vue': {
|
||||
branches: 80,
|
||||
functions: 30,
|
||||
lines: 90,
|
||||
statements: 90,
|
||||
},
|
||||
'src/components/workflow/SendMessageAction.vue': {
|
||||
branches: 85,
|
||||
functions: 30,
|
||||
lines: 90,
|
||||
statements: 90,
|
||||
},
|
||||
'src/@core/utils/workflow.ts': {
|
||||
branches: 85,
|
||||
functions: 90,
|
||||
|
||||
Reference in New Issue
Block a user