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: