import { App as AntApp, ConfigProvider } from 'antd'; import { act, render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { createMemoryRouter, RouterProvider } from 'react-router-dom'; import { describe, expect, it, vi } from 'vitest'; import { I18nProvider } from '../../src/frontend/i18n/react'; import { api } from '../../src/frontend/services/api'; import { mailhubTheme } from '../../src/frontend/theme'; import type { SendEvent } from '../../src/frontend/types'; import SendingLogs from '../../src/pages/SendingLogs'; describe('Activity direct detail navigation', () => { it('decodes MIME subjects and keeps verbose SMTP responses inside the detail drawer on mobile', async () => { const user = userEvent.setup(); const encodedEvent = { ...sendEvent, subject: '=?UTF-8?B?5L2g5aW977yM5LiW55WM?=', detail: '550 a deliberately verbose SMTP response that belongs in detail' }; vi.spyOn(api, 'events').mockResolvedValue({ events: [encodedEvent], total: 1, page: 1, pageSize: 20 }); vi.spyOn(api, 'domains').mockResolvedValue({ domains: [] }); vi.spyOn(api, 'event').mockResolvedValue({ event: encodedEvent }); renderPage({ initialEntries: ['/activity'], initialIndex: 0 }); expect(await screen.findByText('你好,世界')).not.toBeNull(); expect(screen.queryByText(encodedEvent.subject)).toBeNull(); expect(screen.queryByText(encodedEvent.detail)).toBeNull(); await user.click(screen.getByRole('button', { name: '查看发送详情' })); expect(await screen.findByText(encodedEvent.detail)).not.toBeNull(); expect(screen.getAllByText('你好,世界').length).toBeGreaterThan(0); }); it('ignores a stale detail response after navigating to another event', async () => { let resolveFirst: ((value: { event: SendEvent }) => void) | undefined; const secondEvent: SendEvent = { ...sendEvent, id: 12, messageId: 'mh-12', subject: 'Current event' }; vi.spyOn(api, 'events').mockResolvedValue({ events: [sendEvent, secondEvent], total: 2, page: 1, pageSize: 20 }); vi.spyOn(api, 'domains').mockResolvedValue({ domains: [] }); vi.spyOn(api, 'event').mockImplementation((id) => { if (id === 11) return new Promise((resolve) => { resolveFirst = resolve; }); return Promise.resolve({ event: secondEvent }); }); const router = renderPage({ initialEntries: ['/activity/11'], initialIndex: 0 }); await waitFor(() => expect(api.event).toHaveBeenCalledWith(11)); await act(async () => { await router.navigate('/activity/12'); }); const drawer = await screen.findByRole('dialog', { name: /mh-12/ }); expect(await within(drawer).findByText('Current event')).not.toBeNull(); await act(async () => { resolveFirst?.({ event: sendEvent }); await Promise.resolve(); }); expect(within(drawer).queryByText('Delivery test')).toBeNull(); expect(within(drawer).getByText('Current event')).not.toBeNull(); }); it('leaves the loading state when navigation changes to an invalid event id', async () => { vi.spyOn(api, 'events').mockResolvedValue({ events: [sendEvent], total: 1, page: 1, pageSize: 20 }); vi.spyOn(api, 'domains').mockResolvedValue({ domains: [] }); vi.spyOn(api, 'event').mockImplementation(() => new Promise(() => undefined)); const router = renderPage({ initialEntries: ['/activity/11'], initialIndex: 0 }); await waitFor(() => expect(api.event).toHaveBeenCalledWith(11)); await act(async () => { await router.navigate('/activity/not-a-number'); }); const drawer = await screen.findByRole('dialog', { name: '发送详情' }); expect(await within(drawer).findByText('未找到该发送记录,或当前账户没有访问权限。')).not.toBeNull(); expect(drawer.querySelector('.ant-skeleton')).toBeNull(); }); it('restores direct-link tabs with history and closes past them to the filtered list', async () => { const user = userEvent.setup(); mockActivityApis(); const router = renderPage({ initialEntries: ['/overview', '/activity/11?page=2&pageSize=25&status=failed&tab=raw'], initialIndex: 1 }); expect(await screen.findByRole('tab', { name: '原始日志', selected: true })).not.toBeNull(); await user.click(screen.getByRole('tab', { name: 'Webhook' })); await waitFor(() => expect(currentTab(router)).toBe('webhooks')); await user.click(screen.getByRole('tab', { name: '互动跟踪' })); await waitFor(() => expect(currentTab(router)).toBe('tracking')); await act(async () => { await router.navigate(-1); }); expect(currentTab(router)).toBe('webhooks'); expect(screen.getByRole('tab', { name: 'Webhook', selected: true })).not.toBeNull(); await act(async () => { await router.navigate(-1); }); expect(currentTab(router)).toBe('raw'); expect(screen.getByRole('tab', { name: '原始日志', selected: true })).not.toBeNull(); await act(async () => { await router.navigate(1); }); expect(currentTab(router)).toBe('webhooks'); await act(async () => { await router.navigate(1); }); expect(currentTab(router)).toBe('tracking'); await user.click(screen.getByRole('button', { name: 'Close' })); await waitFor(() => expect(router.state.location.pathname).toBe('/activity')); const listParams = new URLSearchParams(router.state.location.search); expect(listParams.get('page')).toBe('2'); expect(listParams.get('pageSize')).toBe('25'); expect(listParams.get('status')).toBe('failed'); expect(listParams.has('tab')).toBe(false); await act(async () => { await router.navigate(-1); }); expect(router.state.location.pathname).toBe('/overview'); }); it('replaces an untouched direct detail with its list even without prior history', async () => { const user = userEvent.setup(); mockActivityApis(); const router = renderPage({ initialEntries: ['/activity/11?page=1&pageSize=25'], initialIndex: 0 }); await screen.findByRole('dialog', { name: '发送详情' }); await user.click(screen.getByRole('button', { name: 'Close' })); await waitFor(() => expect(router.state.location.pathname).toBe('/activity')); await act(async () => { await router.navigate(-1); }); expect(router.state.location.pathname).toBe('/activity'); }); }); function mockActivityApis() { vi.spyOn(api, 'events').mockResolvedValue({ events: [sendEvent], total: 1, page: 2, pageSize: 25 }); vi.spyOn(api, 'domains').mockResolvedValue({ domains: [] }); vi.spyOn(api, 'event').mockResolvedValue({ event: sendEvent }); } function renderPage({ initialEntries, initialIndex }: { initialEntries: string[]; initialIndex: number }) { const router = createMemoryRouter([ { path: '/activity', element: }, { path: '/activity/:eventId', element: }, { path: '*', element:
other
} ], { initialEntries, initialIndex }); render( ); return router; } function currentTab(router: ReturnType) { return new URLSearchParams(router.state.location.search).get('tab'); } const sendEvent: SendEvent = { id: 11, userId: 1, domainId: null, smtpRelayId: null, domain: 'example.test', sender: 'sender@example.test', recipients: ['recipient@example.test'], subject: 'Delivery test', status: 'failed', detail: '550 mailbox unavailable', queueId: 'QUEUE-11', messageId: 'mh-11', createdAt: '2026-07-14T00:00:00.000Z' };