diff --git a/packages/client/src/components/hermes/jobs/JobFormModal.vue b/packages/client/src/components/hermes/jobs/JobFormModal.vue index ac9a344..2717c0c 100644 --- a/packages/client/src/components/hermes/jobs/JobFormModal.vue +++ b/packages/client/src/components/hermes/jobs/JobFormModal.vue @@ -52,8 +52,39 @@ const schedulePresets = computed(() => [ { label: t('jobs.presetEveryMonth'), value: '0 9 1 * *' }, ]) +function hasText(value: unknown): boolean { + return typeof value === 'string' && value.trim().length > 0 +} + +function isDeliverTargetConfigured(key: string): boolean { + const config = settingsStore.platforms[key] || {} + switch (key) { + case 'telegram': + case 'discord': + case 'slack': + return hasText(config.token) + case 'whatsapp': + return config.enabled === true || config.enabled === 'true' + case 'matrix': + return hasText(config.token) && hasText(config.extra?.homeserver) + case 'weixin': + return hasText(config.token) && hasText(config.extra?.account_id) + case 'wecom': + return hasText(config.extra?.bot_id) && hasText(config.extra?.secret) + case 'feishu': + return hasText(config.extra?.app_id) && hasText(config.extra?.app_secret) + case 'dingtalk': + return (hasText(config.extra?.client_id) && hasText(config.extra?.client_secret)) + || (hasText(config.extra?.app_key) && hasText(config.extra?.client_secret)) + case 'qqbot': + return hasText(config.extra?.app_id) && hasText(config.extra?.client_secret) + default: + return false + } +} + const targetOptions = computed(() => { - const options: Array<{ label: string; value: string }> = [ + const options: Array<{ label: string; value: string; disabled?: boolean }> = [ { label: t('jobs.origin'), value: 'origin' }, { label: t('jobs.local'), value: 'local' }, ] @@ -67,12 +98,14 @@ const targetOptions = computed(() => { { key: 'wecom', label: 'WeCom' }, { key: 'feishu', label: 'Feishu' }, { key: 'dingtalk', label: 'DingTalk' }, + { key: 'qqbot', label: 'QQBot' }, ] for (const ch of channels) { - const config = settingsStore.platforms[ch.key] || {} - if (Object.keys(config).length > 0) { - options.push({ label: ch.label, value: ch.key }) - } + options.push({ + label: ch.label, + value: ch.key, + disabled: !isDeliverTargetConfigured(ch.key), + }) } return options }) @@ -80,6 +113,10 @@ const targetOptions = computed(() => { const originalJob = ref(null) onMounted(async () => { + if (Object.keys(settingsStore.platforms || {}).length === 0) { + await settingsStore.fetchSettings() + } + if (props.jobId) { try { const job = await getJob(props.jobId) diff --git a/tests/client/job-form-modal.test.ts b/tests/client/job-form-modal.test.ts new file mode 100644 index 0000000..c0a21f0 --- /dev/null +++ b/tests/client/job-form-modal.test.ts @@ -0,0 +1,135 @@ +// @vitest-environment jsdom +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { defineComponent } from 'vue' +import { flushPromises, mount } from '@vue/test-utils' + +const mockMessage = vi.hoisted(() => ({ + warning: vi.fn(), + success: vi.fn(), + error: vi.fn(), +})) + +const mockSettingsStore = vi.hoisted(() => ({ + platforms: {} as Record, + fetchSettings: vi.fn(async () => { + mockSettingsStore.platforms = { + telegram: { token: 'telegram-token' }, + discord: { token: 'discord-token' }, + slack: { token: 'slack-token' }, + whatsapp: { enabled: true }, + matrix: { token: 'matrix-token' }, + weixin: { token: 'weixin-token' }, + wecom: { extra: { bot_id: 'wecom-bot' } }, + feishu: { extra: { app_id: 'feishu-app' } }, + dingtalk: { extra: { client_id: 'dingtalk-client' } }, + qqbot: { extra: { app_id: 'qq-app', client_secret: 'qq-secret' } }, + } + }), +})) + +const mockJobsStore = vi.hoisted(() => ({ + createJob: vi.fn(), + updateJob: vi.fn(), +})) + +vi.mock('@/stores/hermes/settings', () => ({ + useSettingsStore: () => mockSettingsStore, +})) + +vi.mock('@/stores/hermes/jobs', () => ({ + useJobsStore: () => mockJobsStore, +})) + +vi.mock('@/api/hermes/jobs', async () => { + const actual = await vi.importActual('@/api/hermes/jobs') + return { + ...actual, + getJob: vi.fn(), + } +}) + +vi.mock('vue-i18n', () => ({ + useI18n: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('naive-ui', () => ({ + NModal: defineComponent({ + template: '
', + }), + NForm: defineComponent({ template: '
' }), + NFormItem: defineComponent({ template: '
' }), + NInput: defineComponent({ + props: { value: { type: String, required: false } }, + emits: ['update:value'], + template: '', + }), + NInputNumber: defineComponent({ + props: { value: { required: false } }, + emits: ['update:value'], + template: '', + }), + NSelect: defineComponent({ + props: { value: { required: false }, options: { type: Array, default: () => [] } }, + emits: ['update:value'], + template: '', + }), + NButton: defineComponent({ + emits: ['click'], + template: '', + }), + useMessage: () => mockMessage, +})) + +import JobFormModal from '@/components/hermes/jobs/JobFormModal.vue' + +describe('JobFormModal deliver targets', () => { + beforeEach(() => { + vi.clearAllMocks() + mockSettingsStore.platforms = {} + }) + + it('loads platform settings when the store has not been hydrated', async () => { + mount(JobFormModal, { + props: { jobId: null }, + }) + + await flushPromises() + + expect(mockSettingsStore.fetchSettings).toHaveBeenCalledOnce() + }) + + it('shows every supported platform channel in deliver target options', async () => { + mockSettingsStore.platforms = { + telegram: { token: 'telegram-token' }, + whatsapp: { enabled: false }, + qqbot: { extra: { app_id: 'qq-app', client_secret: 'qq-secret' } }, + } + const wrapper = mount(JobFormModal, { + props: { jobId: null }, + }) + + await flushPromises() + + expect(mockSettingsStore.fetchSettings).not.toHaveBeenCalled() + const labels = wrapper.findAll('.n-select-stub')[1].text() + expect(labels).toContain('Telegram') + expect(labels).toContain('Discord') + expect(labels).toContain('Slack') + expect(labels).toContain('WhatsApp') + expect(labels).toContain('Matrix') + expect(labels).toContain('WeChat') + expect(labels).toContain('WeCom') + expect(labels).toContain('Feishu') + expect(labels).toContain('DingTalk') + expect(labels).toContain('QQBot') + + const options = wrapper.findAll('.n-select-stub')[1].findAll('option') + const optionByValue = Object.fromEntries(options.map(option => [option.attributes('value'), option])) + expect(optionByValue.telegram.attributes('disabled')).toBeUndefined() + expect(optionByValue.qqbot.attributes('disabled')).toBeUndefined() + expect(optionByValue.discord.attributes('disabled')).toBe('') + expect(optionByValue.whatsapp.attributes('disabled')).toBe('') + }) +})