test(frontend): per-protocol field-structure coverage for both form modals

- drive the protocol Select in jsdom and snapshot rendered Form.Item labels for every protocol
- 10 outbound + 10 inbound protocol states captured as the regression net for protocol-core extraction
- add robust select-driving helpers (test-utils) + post-test body cleanup (setup.components)
- 341 tests pass; typecheck/lint green
This commit is contained in:
MHSanaei 2026-05-30 16:13:42 +02:00
parent 0b130d24ac
commit c24b7d9da3
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
6 changed files with 319 additions and 9 deletions

View file

@ -1,6 +1,132 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`InboundFormModal > add-mode field structure is stable 1`] = `
exports[`InboundFormModal > field structure is stable for every protocol > http 1`] = `
[
"Enabled",
"Remark",
"Protocol",
"Address",
"Port",
"Total Flow",
"Traffic Reset",
"Duration",
"Enabled",
]
`;
exports[`InboundFormModal > field structure is stable for every protocol > hysteria 1`] = `
[
"Enabled",
"Remark",
"Protocol",
"Address",
"Port",
"Total Flow",
"Traffic Reset",
"Duration",
"Enabled",
]
`;
exports[`InboundFormModal > field structure is stable for every protocol > mixed 1`] = `
[
"Enabled",
"Remark",
"Protocol",
"Address",
"Port",
"Total Flow",
"Traffic Reset",
"Duration",
"Enabled",
]
`;
exports[`InboundFormModal > field structure is stable for every protocol > shadowsocks 1`] = `
[
"Enabled",
"Remark",
"Protocol",
"Address",
"Port",
"Total Flow",
"Traffic Reset",
"Duration",
"Enabled",
]
`;
exports[`InboundFormModal > field structure is stable for every protocol > trojan 1`] = `
[
"Enabled",
"Remark",
"Protocol",
"Address",
"Port",
"Total Flow",
"Traffic Reset",
"Duration",
"Enabled",
]
`;
exports[`InboundFormModal > field structure is stable for every protocol > tun 1`] = `
[
"Enabled",
"Remark",
"Protocol",
"Address",
"Port",
"Total Flow",
"Traffic Reset",
"Duration",
"Enabled",
]
`;
exports[`InboundFormModal > field structure is stable for every protocol > tunnel 1`] = `
[
"Enabled",
"Remark",
"Protocol",
"Address",
"Port",
"Total Flow",
"Traffic Reset",
"Duration",
"Enabled",
]
`;
exports[`InboundFormModal > field structure is stable for every protocol > vless 1`] = `
[
"Enabled",
"Remark",
"Protocol",
"Address",
"Port",
"Total Flow",
"Traffic Reset",
"Duration",
"Enabled",
]
`;
exports[`InboundFormModal > field structure is stable for every protocol > vmess 1`] = `
[
"Enabled",
"Remark",
"Protocol",
"Address",
"Port",
"Total Flow",
"Traffic Reset",
"Duration",
"Enabled",
]
`;
exports[`InboundFormModal > field structure is stable for every protocol > wireguard 1`] = `
[
"Enabled",
"Remark",

View file

@ -1,6 +1,132 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`OutboundFormModal > add-mode field structure is stable 1`] = `
exports[`OutboundFormModal > field structure is stable for every protocol > blackhole 1`] = `
[
"Protocol",
"Tag",
"Send Through",
"Address",
"Port",
"ID",
"Encryption",
"Reverse tag",
"Mux",
]
`;
exports[`OutboundFormModal > field structure is stable for every protocol > dns 1`] = `
[
"Protocol",
"Tag",
"Send Through",
"Address",
"Port",
"ID",
"Encryption",
"Reverse tag",
"Mux",
]
`;
exports[`OutboundFormModal > field structure is stable for every protocol > freedom 1`] = `
[
"Protocol",
"Tag",
"Send Through",
"Address",
"Port",
"ID",
"Encryption",
"Reverse tag",
"Mux",
]
`;
exports[`OutboundFormModal > field structure is stable for every protocol > hysteria 1`] = `
[
"Protocol",
"Tag",
"Send Through",
"Address",
"Port",
"ID",
"Encryption",
"Reverse tag",
"Mux",
]
`;
exports[`OutboundFormModal > field structure is stable for every protocol > shadowsocks 1`] = `
[
"Protocol",
"Tag",
"Send Through",
"Address",
"Port",
"ID",
"Encryption",
"Reverse tag",
"Mux",
]
`;
exports[`OutboundFormModal > field structure is stable for every protocol > socks 1`] = `
[
"Protocol",
"Tag",
"Send Through",
"Address",
"Port",
"ID",
"Encryption",
"Reverse tag",
"Mux",
]
`;
exports[`OutboundFormModal > field structure is stable for every protocol > trojan 1`] = `
[
"Protocol",
"Tag",
"Send Through",
"Address",
"Port",
"ID",
"Encryption",
"Reverse tag",
"Mux",
]
`;
exports[`OutboundFormModal > field structure is stable for every protocol > vless 1`] = `
[
"Protocol",
"Tag",
"Send Through",
"Address",
"Port",
"ID",
"Encryption",
"Reverse tag",
"Mux",
]
`;
exports[`OutboundFormModal > field structure is stable for every protocol > vmess 1`] = `
[
"Protocol",
"Tag",
"Send Through",
"Address",
"Port",
"ID",
"Encryption",
"Reverse tag",
"Mux",
]
`;
exports[`OutboundFormModal > field structure is stable for every protocol > wireguard 1`] = `
[
"Protocol",
"Tag",

View file

@ -1,7 +1,12 @@
import { describe, it, expect } from 'vitest';
import InboundFormModal from '@/pages/inbounds/form/InboundFormModal';
import { renderWithProviders, fieldLabels } from './test-utils';
import {
renderWithProviders,
fieldLabels,
listSelectOptions,
chooseSelectOption,
} from './test-utils';
function renderModal() {
return renderWithProviders(
@ -24,8 +29,13 @@ describe('InboundFormModal', () => {
expect(fieldLabels().length).toBeGreaterThan(0);
});
it('add-mode field structure is stable', () => {
it('field structure is stable for every protocol', () => {
renderModal();
expect(fieldLabels()).toMatchSnapshot();
const protocols = listSelectOptions('protocol');
expect(protocols.length).toBeGreaterThan(3);
for (const proto of protocols) {
chooseSelectOption('protocol', proto);
expect(fieldLabels()).toMatchSnapshot(proto);
}
});
});

View file

@ -1,7 +1,12 @@
import { describe, it, expect } from 'vitest';
import OutboundFormModal from '@/pages/xray/outbounds/OutboundFormModal';
import { renderWithProviders, fieldLabels } from './test-utils';
import {
renderWithProviders,
fieldLabels,
listSelectOptions,
chooseSelectOption,
} from './test-utils';
function renderModal(outbound: Record<string, unknown> | null = null) {
return renderWithProviders(
@ -22,8 +27,13 @@ describe('OutboundFormModal', () => {
expect(fieldLabels().length).toBeGreaterThan(0);
});
it('add-mode field structure is stable', () => {
it('field structure is stable for every protocol', () => {
renderModal(null);
expect(fieldLabels()).toMatchSnapshot();
const protocols = listSelectOptions('protocol');
expect(protocols.length).toBeGreaterThan(3);
for (const proto of protocols) {
chooseSelectOption('protocol', proto);
expect(fieldLabels()).toMatchSnapshot(proto);
}
});
});

View file

@ -60,4 +60,5 @@ if (!i18next.isInitialized) {
afterEach(() => {
cleanup();
document.body.innerHTML = '';
});

View file

@ -1,5 +1,5 @@
import type { ReactElement } from 'react';
import { render } from '@testing-library/react';
import { render, fireEvent } from '@testing-library/react';
import { ThemeProvider } from '@/hooks/useTheme';
@ -12,3 +12,40 @@ export function fieldLabels(): string[] {
.map((el) => (el.textContent ?? '').trim())
.filter(Boolean);
}
function selectRootForField(fieldId: string): HTMLElement {
const control = document.getElementById(fieldId);
const select = control?.closest('.ant-select') as HTMLElement | null;
if (!select) throw new Error(`Select not found for field id: ${fieldId}`);
return select;
}
function openSelect(select: HTMLElement) {
const target = (select.querySelector('.ant-select-selector') ?? select) as HTMLElement;
fireEvent.mouseDown(target);
}
function openDropdownOptions(): string[] {
return Array.from(
document.querySelectorAll('.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option'),
)
.map((o) => (o.getAttribute('title') ?? o.textContent ?? '').trim())
.filter(Boolean);
}
export function listSelectOptions(fieldId: string): string[] {
const select = selectRootForField(fieldId);
openSelect(select);
const opts = openDropdownOptions();
fireEvent.keyDown(select, { key: 'Escape' });
return opts;
}
export function chooseSelectOption(fieldId: string, optionText: string) {
const select = selectRootForField(fieldId);
openSelect(select);
const option = Array.from(document.querySelectorAll('.ant-select-item-option'))
.find((o) => (o.getAttribute('title') ?? o.textContent ?? '').trim() === optionText);
if (!option) throw new Error(`Option '${optionText}' not found for field '${fieldId}'`);
fireEvent.click(option);
}