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 = (records: T[]): PageResult => ({ 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, '确认定时发送') 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) })) expect(wrapper.emitted('success')).toHaveLength(1) }) })