gongxiangp2/components/admin/NotificationCenterPanel.test.tsx

173 lines
7.0 KiB
TypeScript

import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import NotificationCenterPanel from './NotificationCenterPanel'
jest.mock('@gsap/react', () => ({
useGSAP: jest.fn(),
}))
jest.mock('gsap', () => ({
registerPlugin: jest.fn(),
matchMedia: () => ({
add: jest.fn(),
revert: jest.fn(),
}),
fromTo: jest.fn(),
}))
const project = {
name: '素材平台',
projectCode: 'material_library',
isActive: true,
allowedHosts: ['https://sc.jiansun.vip'],
recipientMode: 'alias',
recipientCount: 1,
recipients: [{ alias: '测试员工', userId: 'ding-user-1' }],
updatedAt: '2026-08-28T08:00:00.000Z',
deliverySummary: { lastStatus: 'sent', lastSentAt: '2026-08-28T08:00:00.000Z', recentFailedCount: 0 },
}
const jsonResponse = (body: unknown, status = 200) =>
Promise.resolve({
ok: status >= 200 && status < 300,
status,
json: async () => body,
} as Response)
describe('NotificationCenterPanel', () => {
const showToast = jest.fn()
beforeEach(() => {
jest.clearAllMocks()
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: { writeText: jest.fn().mockResolvedValue(undefined) },
})
global.fetch = jest.fn((input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input)
if (url.includes('/api/admin/notification-center/projects?includeRecipients=1') && (!init?.method || init.method === 'GET')) {
return jsonResponse({ success: true, data: { projects: [project] } })
}
if (url.endsWith('/api/admin/notification-center/projects') && init?.method === 'POST') {
return jsonResponse({
success: true,
data: {
project,
secret: 'one-time-project-secret',
env: {
YYD_NOTIFICATION_CENTER_URL: 'https://sc.jiansun.vip/notification-center',
YYD_NOTIFICATION_CENTER_PROJECT_CODE: 'fixed_assets',
YYD_NOTIFICATION_CENTER_PROJECT_SECRET: 'one-time-project-secret',
},
},
}, 201)
}
if (url.includes('/recipients') && init?.method === 'PUT') {
return jsonResponse({ project })
}
if (url.includes('/rotate-secret') && init?.method === 'POST') {
return jsonResponse({
success: true,
data: {
secret: 'rotated-one-time-secret',
env: { YYD_NOTIFICATION_CENTER_PROJECT_SECRET: 'rotated-one-time-secret' },
},
})
}
if (url.endsWith('/test') && init?.method === 'POST') {
return jsonResponse({ deliveryId: 'delivery-1', status: 'pending' }, 202)
}
if (url.includes('/deliveries')) {
return jsonResponse({ deliveries: [] })
}
return jsonResponse({ error: { message: 'unexpected request' } }, 500)
}) as jest.Mock
})
test('默认展示项目总览并明确区分素材外发', async () => {
render(<NotificationCenterPanel showToast={showToast} />)
expect(await screen.findByText('素材平台')).toBeInTheDocument()
expect(screen.getByText(/素材平台向外部系统发送 Webhook 的能力位于“素材外发”/)).toBeInTheDocument()
expect(screen.getAllByText('1', { selector: 'span' })).toHaveLength(3)
expect(screen.getByRole('button', { name: '新增接入项目' })).toBeInTheDocument()
})
test('新建项目后只展示一次项目密钥和环境变量', async () => {
const user = userEvent.setup()
render(<NotificationCenterPanel showToast={showToast} />)
await screen.findByText('素材平台')
await user.click(screen.getByRole('button', { name: '新增接入项目' }))
await user.type(screen.getByLabelText('项目中文名称'), '固定资产系统')
await user.type(screen.getByLabelText('projectCode'), 'fixed_assets')
await user.type(screen.getByLabelText('允许跳转域名 allowedHosts'), 'https://assets.example.com')
await user.click(screen.getByRole('button', { name: '创建并生成密钥' }))
expect(await screen.findByText('one-time-project-secret')).toBeInTheDocument()
expect(screen.getByText(/YYD_NOTIFICATION_CENTER_PROJECT_CODE=fixed_assets/)).toBeInTheDocument()
expect(global.fetch).toHaveBeenCalledWith(
'/api/admin/notification-center/projects',
expect.objectContaining({ method: 'POST' }),
)
})
test('保存花名使用项目专属 recipients 代理接口', async () => {
const user = userEvent.setup()
render(<NotificationCenterPanel showToast={showToast} />)
await screen.findByText('素材平台')
await user.click(screen.getByRole('button', { name: /收件人花名/ }))
expect(await screen.findByDisplayValue('测试员工')).toBeInTheDocument()
await user.click(screen.getByRole('button', { name: '保存花名清单' }))
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(
'/api/admin/notification-center/projects/material_library/recipients',
expect.objectContaining({ method: 'PUT' }),
)
})
})
test('密钥轮换要求二次确认且新密钥只在确认后展示', async () => {
const user = userEvent.setup()
render(<NotificationCenterPanel showToast={showToast} />)
await screen.findByText('素材平台')
await user.click(screen.getByRole('button', { name: '管理项目 素材平台' }))
await user.click(screen.getByRole('button', { name: '轮换项目密钥' }))
expect(screen.getByText(/旧密钥的调用会立即鉴权失败/)).toBeInTheDocument()
await user.click(screen.getByRole('button', { name: '确认轮换,旧密钥立即失效' }))
expect(await screen.findByText('rotated-one-time-secret')).toBeInTheDocument()
})
test('测试通知在确认前不发送,确认后才调用 mock 接口', async () => {
const user = userEvent.setup()
render(<NotificationCenterPanel showToast={showToast} />)
await screen.findByText('素材平台')
await user.click(screen.getByRole('button', { name: /测试通知/ }))
fireEvent.change(screen.getByLabelText('真实收件人'), { target: { value: '测试员工' } })
await user.click(screen.getByRole('button', { name: '向指定员工发送测试通知' }))
expect(global.fetch).not.toHaveBeenCalledWith(expect.stringContaining('/test'), expect.anything())
expect(screen.getByText(/确认后会真实调用钉钉/)).toBeInTheDocument()
await user.click(screen.getByRole('button', { name: '确认发送给该员工' }))
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(
'/api/admin/notification-center/projects/material_library/test',
expect.objectContaining({ method: 'POST' }),
)
})
})
test('通知中心离线时显示可恢复错误且不伪装成空数据', async () => {
global.fetch = jest.fn().mockRejectedValue(new Error('连接超时'))
render(<NotificationCenterPanel showToast={showToast} />)
expect(await screen.findByRole('alert')).toHaveTextContent('连接超时')
expect(screen.getByRole('alert')).toHaveTextContent('项目配置没有被修改')
})
})