feat: 优化通道组编辑交互

This commit is contained in:
hectorzhao
2026-09-05 22:55:09 +08:00
parent 839dba8d9b
commit 1e05a643e5
14 changed files with 1752 additions and 462 deletions
@@ -0,0 +1,145 @@
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('已选:通道availableCODE-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();
});
});