146 lines
5.9 KiB
TypeScript
146 lines
5.9 KiB
TypeScript
import { fireEvent, render, screen } from '@testing-library/react';
|
||
import userEvent from '@testing-library/user-event';
|
||
import { describe, expect, it, vi } from 'vitest';
|
||
import type { AdminChannel } from '@/api/adminApi';
|
||
import { RouteConfigModal } from './RouteConfigModal';
|
||
|
||
const channel = (id: string, overrides: Partial<AdminChannel> = {}): AdminChannel => ({
|
||
id,
|
||
name: `通道${id}`,
|
||
code: `CODE-${id}`,
|
||
carriers: ['mobile'],
|
||
sendRegion: '广东省',
|
||
gatewayHost: '127.0.0.1',
|
||
gatewayPort: 7890,
|
||
account: 'test',
|
||
srcId: '106',
|
||
rateLimitPerSecond: 1,
|
||
unitPrice: 325,
|
||
status: 'active',
|
||
...overrides,
|
||
});
|
||
|
||
describe('RouteConfigModal', () => {
|
||
it('keeps historical priority and exposes costs while permitting a disconnected active channel', async () => {
|
||
const onSubmit = vi.fn(() => null);
|
||
const current = channel('current', { connectionStates: [] });
|
||
render(
|
||
<RouteConfigModal
|
||
channels={[current]}
|
||
carrier="mobile"
|
||
modal={{ type: 'national', mode: 'edit', route: { id: 'route-1', channelId: current.id, priority: 20 } }}
|
||
occupiedChannelIds={[current.id]}
|
||
nextPriority={30}
|
||
onClose={vi.fn()}
|
||
onSubmit={onSubmit}
|
||
/>,
|
||
);
|
||
expect(screen.getByRole('spinbutton', { name: /优先级/ })).toHaveValue(20);
|
||
expect(screen.getByText('0.0325')).toBeVisible();
|
||
expect(screen.getByText('暂无连接回写')).toBeVisible();
|
||
expect(screen.getByRole('radio')).toBeEnabled();
|
||
await userEvent.click(screen.getByRole('button', { name: /^确认$/ }));
|
||
expect(onSubmit).toHaveBeenCalledWith({ id: 'route-1', channelId: 'current', priority: 20 });
|
||
});
|
||
|
||
it('shows unavailable reasons and keeps the selected channel while searching', async () => {
|
||
const channels = [
|
||
channel('available'),
|
||
channel('occupied'),
|
||
channel('deleted', { status: 'deleted' }),
|
||
channel('telecom', { carriers: ['telecom'] }),
|
||
channel('disabled', { status: 'disabled' }),
|
||
];
|
||
render(
|
||
<RouteConfigModal
|
||
channels={channels}
|
||
carrier="mobile"
|
||
modal={{ type: 'national', mode: 'create' }}
|
||
occupiedChannelIds={['occupied']}
|
||
nextPriority={30}
|
||
onClose={vi.fn()}
|
||
onSubmit={() => null}
|
||
/>,
|
||
);
|
||
for (const id of ['occupied', 'deleted', 'telecom'])
|
||
expect(screen.getByRole('radio', { name: new RegExp(`CODE-${id}`) })).toBeDisabled();
|
||
expect(screen.getByText('已在当前通道组中配置')).toBeVisible();
|
||
expect(screen.getByText('通道已删除')).toBeVisible();
|
||
expect(screen.getByText('不支持移动')).toBeVisible();
|
||
expect(screen.getByText('已停用')).toBeVisible();
|
||
expect(screen.getByRole('radio', { name: /CODE-disabled/ })).toBeEnabled();
|
||
await userEvent.click(screen.getByRole('radio', { name: /CODE-available/ }));
|
||
await userEvent.type(screen.getByRole('textbox', { name: '搜索通道' }), 'CODE-telecom');
|
||
expect(screen.getAllByRole('radio')).toHaveLength(1);
|
||
expect(screen.getByText('已选:通道available(CODE-available)')).toBeVisible();
|
||
});
|
||
|
||
it('disables region mismatches but preserves a historical province option', () => {
|
||
render(
|
||
<RouteConfigModal
|
||
channels={[channel('other', { sendRegion: '江苏省' })]}
|
||
carrier="mobile"
|
||
modal={{ type: 'province', mode: 'edit', route: { id: 'p-1', channelId: 'missing', province: '浙江省' } }}
|
||
occupiedChannelIds={[]}
|
||
nextPriority={10}
|
||
onClose={vi.fn()}
|
||
onSubmit={() => null}
|
||
/>,
|
||
);
|
||
expect(screen.getByText('浙江省')).toBeVisible();
|
||
expect(screen.getByRole('radio')).toBeDisabled();
|
||
expect(screen.getByText('通道地区与所选省份不匹配')).toBeVisible();
|
||
expect(screen.getByRole('alert')).toHaveTextContent('原通道不存在,请重新选择');
|
||
});
|
||
|
||
it('retains form values after parent conflict rejection and rejects a fractional priority', async () => {
|
||
const onSubmit = vi.fn(() => '同一通道组内全国通道优先级不能重复');
|
||
const onClose = vi.fn();
|
||
render(
|
||
<RouteConfigModal
|
||
channels={[channel('1')]}
|
||
carrier="mobile"
|
||
modal={{ type: 'national', mode: 'create' }}
|
||
occupiedChannelIds={[]}
|
||
nextPriority={20}
|
||
onClose={onClose}
|
||
onSubmit={onSubmit}
|
||
/>,
|
||
);
|
||
await userEvent.click(screen.getByRole('radio'));
|
||
await userEvent.click(screen.getByRole('button', { name: /^确认$/ }));
|
||
expect(screen.getByRole('alert')).toHaveTextContent('优先级不能重复');
|
||
expect(screen.getByRole('radio')).toBeChecked();
|
||
expect(onClose).not.toHaveBeenCalled();
|
||
const input = screen.getByRole('spinbutton', { name: /优先级/ });
|
||
fireEvent.change(input, { target: { value: '1.5' } });
|
||
await userEvent.click(screen.getByRole('button', { name: /^确认$/ }));
|
||
expect(screen.getByRole('alert')).toHaveTextContent('整数');
|
||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||
fireEvent.change(input, { target: { value: '-10' } });
|
||
await userEvent.click(screen.getByRole('button', { name: /^确认$/ }));
|
||
expect(onSubmit).toHaveBeenLastCalledWith(expect.objectContaining({ priority: -10 }));
|
||
});
|
||
|
||
it('uses the shared unsaved guard when dismissing a changed selection', async () => {
|
||
const onClose = vi.fn();
|
||
render(
|
||
<RouteConfigModal
|
||
channels={[channel('1')]}
|
||
carrier="mobile"
|
||
modal={{ type: 'national', mode: 'create' }}
|
||
occupiedChannelIds={[]}
|
||
nextPriority={10}
|
||
onClose={onClose}
|
||
onSubmit={() => null}
|
||
/>,
|
||
);
|
||
await userEvent.click(screen.getByRole('radio'));
|
||
await userEvent.click(screen.getByRole('button', { name: /^取消$/ }));
|
||
expect(screen.getByRole('alertdialog')).toHaveTextContent('放弃未保存的修改');
|
||
expect(onClose).not.toHaveBeenCalled();
|
||
await userEvent.click(screen.getByRole('button', { name: '继续编辑' }));
|
||
expect(screen.getByRole('radio')).toBeChecked();
|
||
});
|
||
});
|