2026-09-15 09:58:15 +08:00
|
|
|
import ElementPlus, { ElDrawer, ElSelect } from 'element-plus'
|
|
|
|
|
import { enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
|
import PlatformNotificationsView from './pages/PlatformNotificationsView.vue'
|
|
|
|
|
import MessageManagementView from './pages/MessageManagementView.vue'
|
|
|
|
|
import MessageTemplatesView from './pages/MessageTemplatesView.vue'
|
|
|
|
|
import MessageComposerDialog from './components/MessageComposerDialog.vue'
|
|
|
|
|
import TemplateEditorDialog from './components/TemplateEditorDialog.vue'
|
|
|
|
|
import * as api from './api/notificationCenter'
|
|
|
|
|
import type { NotificationTemplate, NotificationTask, NotificationInboxDetail, PageResult } from './types'
|
|
|
|
|
|
|
|
|
|
const can = vi.hoisted(() => vi.fn(() => true))
|
|
|
|
|
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can }) }))
|
|
|
|
|
vi.mock('./api/notificationCenter', () => ({
|
|
|
|
|
listMyNotifications: vi.fn(), getMyUnreadNotificationCount: vi.fn(), getMyNotification: vi.fn(),
|
|
|
|
|
markMyNotificationRead: vi.fn(), markAllMyNotificationsRead: vi.fn(), listTasks: vi.fn(), getTask: vi.fn(),
|
|
|
|
|
listTemplates: vi.fn(), createNotification: vi.fn(), createTemplate: vi.fn(), updateTemplate: vi.fn(),
|
|
|
|
|
cancelTask: vi.fn(), retryTask: vi.fn(), changeTemplateStatus: vi.fn(),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
const template: NotificationTemplate = { id: 'template-1', templateCode: 'ORDER_NOTICE', templateName: '订单通知', category: 'ORDER', channel: 'IN_APP', titleTemplate: '订单更新', bodyTemplate: '订单已更新', variableWhitelist: [], status: 'DRAFT', version: 3 }
|
|
|
|
|
const task: NotificationTask = { id: 'task-1', taskNo: 'NT202609140001', category: 'ORDER', title: '订单更新', status: 'CREATED', recipientCount: 1, scheduledAt: null, sentAt: null, duplicated: false }
|
|
|
|
|
const inbox: NotificationInboxDetail = { id: 'inbox-1', category: 'ORDER', title: '订单更新', summary: '订单状态有更新', body: '第一行\n第二行', status: 'UNREAD', sentAt: '2026-09-14T01:00:00Z', readAt: null, sourceType: null, sourceId: null }
|
|
|
|
|
const page = <T,>(records: T[]): PageResult<T> => ({ records, page: 1, size: 20, total: records.length, pages: 1 })
|
|
|
|
|
const options = () => ({ attachTo: document.body, global: { plugins: [ElementPlus] } })
|
|
|
|
|
async function click(wrapper: VueWrapper, label: string, selector = 'button') {
|
|
|
|
|
const button = wrapper.findAll(selector).find(item => item.text().trim() === label)
|
|
|
|
|
expect(button, label).toBeDefined()
|
|
|
|
|
await button!.trigger('click')
|
|
|
|
|
await flushPromises()
|
|
|
|
|
}
|
|
|
|
|
enableAutoUnmount(afterEach)
|
|
|
|
|
afterEach(() => { vi.unstubAllGlobals(); document.body.innerHTML = '' })
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks()
|
|
|
|
|
can.mockReturnValue(true)
|
|
|
|
|
vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} })
|
|
|
|
|
vi.mocked(api.listMyNotifications).mockResolvedValue(page([inbox]))
|
|
|
|
|
vi.mocked(api.getMyUnreadNotificationCount).mockResolvedValue(1)
|
|
|
|
|
vi.mocked(api.getMyNotification).mockResolvedValue(inbox)
|
|
|
|
|
vi.mocked(api.markMyNotificationRead).mockResolvedValue({ ...inbox, status: 'READ' })
|
|
|
|
|
vi.mocked(api.listTasks).mockResolvedValue(page([task]))
|
|
|
|
|
vi.mocked(api.getTask).mockResolvedValue({ ...task, attemptCount: 1, lastError: '测试投递失败原因', recipients: [{ id: 'recipient-1', displayName: '测试员工', appCode: 'ADMIN', status: 'FAILED', failureReason: '测试收件失败', deliveredAt: null }] })
|
|
|
|
|
vi.mocked(api.listTemplates).mockResolvedValue(page([template]))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('notification density in native lists and overlays', () => {
|
|
|
|
|
it('keeps inbox actions visible and opens the styled drawer with read marking', async () => {
|
|
|
|
|
const wrapper = mount(PlatformNotificationsView, options())
|
|
|
|
|
await flushPromises()
|
|
|
|
|
const actions = wrapper.get('.el-table__body-wrapper .density-actions')
|
|
|
|
|
expect(actions.findAll('button').map(item => item.text())).toEqual(['查看', '标为已读'])
|
|
|
|
|
expect(actions.find('svg').exists()).toBe(false)
|
|
|
|
|
await click(wrapper, '查看', '.el-table__body-wrapper button')
|
|
|
|
|
const drawer = document.querySelector('.el-drawer.admin-density-drawer')!
|
|
|
|
|
expect(drawer.textContent).toContain('第一行\n第二行')
|
|
|
|
|
expect(wrapper.getComponent(ElDrawer).props('size')).toBe('min(680px, 94vw)')
|
|
|
|
|
expect(api.markMyNotificationRead).toHaveBeenCalledWith(inbox.id)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('retains read-only inbox viewing without read permission', async () => {
|
|
|
|
|
can.mockReturnValue(false)
|
|
|
|
|
const wrapper = mount(PlatformNotificationsView, options())
|
|
|
|
|
await flushPromises()
|
|
|
|
|
expect(wrapper.get('.el-table__body-wrapper .density-actions').findAll('button').map(item => item.text())).toEqual(['查看'])
|
|
|
|
|
await click(wrapper, '查看', '.el-table__body-wrapper button')
|
|
|
|
|
expect(document.querySelector('.el-drawer')?.textContent).toContain('第一行')
|
|
|
|
|
expect(api.markMyNotificationRead).not.toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it.each([
|
|
|
|
|
['CREATED', ['查看', '取消']], ['SENT', ['查看']], ['DEAD', ['查看', '重试']],
|
|
|
|
|
] as const)('keeps task actions for %s and shows delivery evidence in the drawer', async (status, labels) => {
|
|
|
|
|
vi.mocked(api.listTasks).mockResolvedValue(page([{ ...task, status }]))
|
|
|
|
|
const wrapper = mount(MessageManagementView, options())
|
|
|
|
|
await flushPromises()
|
|
|
|
|
const actions = wrapper.get('.el-table__body-wrapper .density-actions')
|
|
|
|
|
expect(actions.findAll('button').map(item => item.text())).toEqual(labels)
|
|
|
|
|
expect(actions.find('svg').exists()).toBe(false)
|
|
|
|
|
await click(wrapper, '查看', '.el-table__body-wrapper button')
|
|
|
|
|
const drawer = document.querySelector('.el-drawer.admin-density-drawer')!
|
|
|
|
|
expect(wrapper.getComponent(ElDrawer).props('size')).toBe('min(820px, 94vw)')
|
|
|
|
|
expect(drawer.querySelectorAll('.density-fields > div')).toHaveLength(6)
|
|
|
|
|
expect(drawer.textContent).toContain(task.taskNo)
|
|
|
|
|
expect(drawer.textContent).toContain('测试员工')
|
|
|
|
|
expect(drawer.textContent).toContain('测试投递失败原因')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it.each([
|
|
|
|
|
['DRAFT', ['编辑', '激活']], ['ACTIVE', ['禁用']], ['DISABLED', []],
|
|
|
|
|
] as const)('keeps template state actions for %s', async (status, labels) => {
|
|
|
|
|
vi.mocked(api.listTemplates).mockResolvedValue(page([{ ...template, status }]))
|
|
|
|
|
const wrapper = mount(MessageTemplatesView, options())
|
|
|
|
|
await flushPromises()
|
|
|
|
|
const actions = wrapper.get('.el-table__body-wrapper .density-actions')
|
|
|
|
|
expect(actions.findAll('button').map(item => item.text())).toEqual(labels)
|
|
|
|
|
expect(actions.find('svg').exists()).toBe(false)
|
|
|
|
|
if (status === 'DRAFT') {
|
|
|
|
|
await click(wrapper, '编辑', '.el-table__body-wrapper button')
|
|
|
|
|
expect(document.querySelector('.el-dialog.admin-density-dialog .density-form')).not.toBeNull()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('preserves template code, variable preview, version and draft editing in the native modal', async () => {
|
|
|
|
|
vi.mocked(api.updateTemplate).mockResolvedValue(template)
|
|
|
|
|
const wrapper = mount(TemplateEditorDialog, { ...options(), props: { modelValue: true, template } })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
const modal = wrapper.get('.el-dialog.admin-density-dialog')
|
|
|
|
|
expect((modal.element as HTMLElement).style.getPropertyValue('--el-dialog-width')).toContain('760px')
|
|
|
|
|
expect(wrapper.get('input[placeholder="如:ORDER_SHIPPED_NOTICE"]').attributes('disabled')).toBeDefined()
|
|
|
|
|
await click(wrapper, 'order_no')
|
|
|
|
|
expect(wrapper.get('.template-preview').text()).toContain('${order_no}')
|
|
|
|
|
await click(wrapper, '保存模板')
|
|
|
|
|
expect(api.updateTemplate).toHaveBeenCalledWith(template.id, expect.objectContaining({ version: 3, variableWhitelist: ['order_no'], bodyTemplate: expect.stringContaining('${order_no}') }))
|
|
|
|
|
expect(wrapper.emitted('success')).toHaveLength(1)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('keeps required composer input and scheduled payload in the native modal', async () => {
|
|
|
|
|
vi.mocked(api.createNotification).mockResolvedValue(task)
|
|
|
|
|
const wrapper = mount(MessageComposerDialog, { ...options(), props: { modelValue: true, templates: [{ ...template, status: 'ACTIVE' }] } })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
const modal = wrapper.get('.el-dialog.admin-density-dialog')
|
|
|
|
|
expect((modal.element as HTMLElement).style.getPropertyValue('--el-dialog-width')).toContain('680px')
|
|
|
|
|
expect(modal.find('.density-form').exists()).toBe(true)
|
|
|
|
|
await click(wrapper, '立即发送')
|
|
|
|
|
expect(api.createNotification).not.toHaveBeenCalled()
|
|
|
|
|
wrapper.findAllComponents(ElSelect)[0]!.vm.$emit('update:modelValue', template.templateCode)
|
|
|
|
|
await wrapper.get('input[placeholder="请输入服务端角色编码"]').setValue('TEST_OPERATOR')
|
|
|
|
|
await wrapper.get('[role="switch"]').trigger('click')
|
|
|
|
|
await flushPromises()
|
|
|
|
|
await click(wrapper, '确认定时发送')
|
2026-09-18 09:15:40 +08:00
|
|
|
expect(api.createNotification).toHaveBeenCalledWith(expect.objectContaining({ tenantId: '0', templateCode: template.templateCode, recipientType: 'PLATFORM_ROLE', roleCode: 'TEST_OPERATOR', variables: {}, scheduledAt: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/), idempotencyKey: expect.any(String) }))
|
2026-09-15 09:58:15 +08:00
|
|
|
expect(wrapper.emitted('success')).toHaveLength(1)
|
|
|
|
|
})
|
|
|
|
|
})
|