173 lines
7.0 KiB
TypeScript
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('项目配置没有被修改')
|
|
})
|
|
})
|