| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144 |
- import { describe, it, expect } from 'vitest';
- import { screen, act, render, cleanup } from '@testing-library/react';
- import InboundFormModal from '@/pages/inbounds/form/InboundFormModal';
- import { DBInbound } from '@/models/dbinbound';
- import { ThemeProvider } from '@/hooks/useTheme';
- import {
- renderWithProviders,
- fieldLabels,
- listSelectOptions,
- chooseSelectOption,
- } from './test-utils';
- function renderModal() {
- return renderWithProviders(
- <InboundFormModal
- open
- mode="add"
- dbInbound={null}
- dbInbounds={[]}
- availableNodes={[]}
- onClose={() => {}}
- onSaved={() => {}}
- />,
- );
- }
- describe('InboundFormModal', () => {
- it('renders add mode without crashing', () => {
- renderModal();
- expect(document.querySelector('.ant-modal')).toBeTruthy();
- expect(fieldLabels().length).toBeGreaterThan(0);
- });
- it('field structure differs per protocol (not a vacuous snapshot loop)', async () => {
- renderModal();
- const protocols = listSelectOptions('protocol');
- expect(protocols.length).toBeGreaterThan(3);
- const labelsByProto: Record<string, string[]> = {};
- for (const proto of protocols) {
- chooseSelectOption('protocol', proto);
- // Flush antd Form.useWatch('protocol') before reading — without it every iteration
- // sees the same pre-update DOM and the loop asserts nothing (the original bug here).
- await act(async () => { await new Promise((r) => setTimeout(r, 0)); });
- labelsByProto[proto] = fieldLabels();
- }
- // The loop must actually exercise protocol-specific rendering: distinct protocols
- // must yield distinct field sets (a vacuous loop makes them all identical).
- const distinctShapes = new Set(Object.values(labelsByProto).map((l) => l.join('|')));
- expect(distinctShapes.size).toBeGreaterThan(1);
- // Spot-check a protocol-distinguishing field that must appear after the switch.
- if (labelsByProto.shadowsocks) {
- expect(labelsByProto.shadowsocks).toContain('Encryption method');
- }
- }, 30000); // iterates every protocol, re-rendering a heavy modal each time — slow on CI runners
- it('preserves custom share address strategy when editing a local inbound', async () => {
- renderWithProviders(
- <InboundFormModal
- open
- mode="edit"
- dbInbound={new DBInbound({
- id: 1,
- port: 12345,
- listen: '',
- protocol: 'shadowsocks',
- remark: 'edge',
- enable: true,
- settings: {
- method: '2022-blake3-aes-128-gcm',
- password: 'server-password',
- network: 'tcp,udp',
- clients: [],
- },
- streamSettings: { network: 'tcp', security: 'none', tcpSettings: {} },
- sniffing: { enabled: false },
- nodeId: null,
- shareAddrStrategy: 'custom',
- shareAddr: 'edge.example.test',
- })}
- dbInbounds={[]}
- availableNodes={[]}
- onClose={() => {}}
- onSaved={() => {}}
- />,
- );
- const shareAddrInput = await screen.findByDisplayValue('edge.example.test');
- expect((shareAddrInput as HTMLInputElement).value).toBe('edge.example.test');
- });
- it('keeps the persisted node share strategy through the nodes-loading race (#5375)', async () => {
- const node = { id: 1, name: 'arm2', enable: true, status: 'online' } as never;
- const buildInbound = () => new DBInbound({
- id: 1,
- port: 23456,
- listen: '',
- protocol: 'vless',
- remark: 'noded',
- enable: true,
- settings: { clients: [] },
- streamSettings: { network: 'tcp', security: 'none', tcpSettings: {} },
- sniffing: { enabled: false },
- nodeId: 1,
- shareAddrStrategy: 'node',
- });
- const flush = async () => { await act(async () => { await new Promise((r) => setTimeout(r, 0)); }); };
- const strategyItem = (title: string) =>
- document.querySelector(`.ant-select-content[title="${title}"]`);
- const modal = (nodes: never[], fetched: boolean) => (
- <ThemeProvider>
- <InboundFormModal
- open
- mode="edit"
- dbInbound={buildInbound()}
- dbInbounds={[]}
- availableNodes={nodes}
- availableNodesFetched={fetched}
- onClose={() => {}}
- onSaved={() => {}}
- />
- </ThemeProvider>
- );
- // Baseline: nodes already loaded, so the node option is offered and selected.
- render(modal([node], true));
- await flush();
- expect(strategyItem('Node address')).toBeTruthy();
- cleanup();
- // Race: the modal mounts before /nodes/list resolves (empty placeholder),
- // then nodes arrive. The persisted 'node' strategy must survive the gap and
- // stay selected once the option reappears — not silently revert to listen.
- const { rerender } = render(modal([], false));
- await flush();
- rerender(modal([node], true));
- await flush();
- expect(strategyItem('Node address')).toBeTruthy();
- expect(strategyItem('Inbound listen')).toBeFalsy();
- });
- });
|