mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 23:56:42 +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/dialog/WorkflowActionsDialog.vue',
|
||||||
'src/components/workflow/InvokePluginAction.vue',
|
'src/components/workflow/InvokePluginAction.vue',
|
||||||
'src/components/workflow/WorkflowSidebar.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/OTPAuthDialog.vue',
|
||||||
'src/components/dialog/PasskeyDialog.vue',
|
'src/components/dialog/PasskeyDialog.vue',
|
||||||
'src/components/dialog/SiteCookieUpdateDialog.vue',
|
'src/components/dialog/SiteCookieUpdateDialog.vue',
|
||||||
@@ -608,6 +612,30 @@ export default defineConfig(({ command, mode, isPreview }) => ({
|
|||||||
lines: 85,
|
lines: 85,
|
||||||
statements: 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': {
|
'src/@core/utils/workflow.ts': {
|
||||||
branches: 85,
|
branches: 85,
|
||||||
functions: 90,
|
functions: 90,
|
||||||
|
|||||||
Reference in New Issue
Block a user