mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
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:
parent
0b130d24ac
commit
c24b7d9da3
6 changed files with 319 additions and 9 deletions
|
|
@ -1,6 +1,132 @@
|
||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// 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",
|
"Enabled",
|
||||||
"Remark",
|
"Remark",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,132 @@
|
||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// 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",
|
"Protocol",
|
||||||
"Tag",
|
"Tag",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
import InboundFormModal from '@/pages/inbounds/form/InboundFormModal';
|
import InboundFormModal from '@/pages/inbounds/form/InboundFormModal';
|
||||||
import { renderWithProviders, fieldLabels } from './test-utils';
|
import {
|
||||||
|
renderWithProviders,
|
||||||
|
fieldLabels,
|
||||||
|
listSelectOptions,
|
||||||
|
chooseSelectOption,
|
||||||
|
} from './test-utils';
|
||||||
|
|
||||||
function renderModal() {
|
function renderModal() {
|
||||||
return renderWithProviders(
|
return renderWithProviders(
|
||||||
|
|
@ -24,8 +29,13 @@ describe('InboundFormModal', () => {
|
||||||
expect(fieldLabels().length).toBeGreaterThan(0);
|
expect(fieldLabels().length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('add-mode field structure is stable', () => {
|
it('field structure is stable for every protocol', () => {
|
||||||
renderModal();
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
import OutboundFormModal from '@/pages/xray/outbounds/OutboundFormModal';
|
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) {
|
function renderModal(outbound: Record<string, unknown> | null = null) {
|
||||||
return renderWithProviders(
|
return renderWithProviders(
|
||||||
|
|
@ -22,8 +27,13 @@ describe('OutboundFormModal', () => {
|
||||||
expect(fieldLabels().length).toBeGreaterThan(0);
|
expect(fieldLabels().length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('add-mode field structure is stable', () => {
|
it('field structure is stable for every protocol', () => {
|
||||||
renderModal(null);
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -60,4 +60,5 @@ if (!i18next.isInitialized) {
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
|
document.body.innerHTML = '';
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import { render } from '@testing-library/react';
|
import { render, fireEvent } from '@testing-library/react';
|
||||||
|
|
||||||
import { ThemeProvider } from '@/hooks/useTheme';
|
import { ThemeProvider } from '@/hooks/useTheme';
|
||||||
|
|
||||||
|
|
@ -12,3 +12,40 @@ export function fieldLabels(): string[] {
|
||||||
.map((el) => (el.textContent ?? '').trim())
|
.map((el) => (el.textContent ?? '').trim())
|
||||||
.filter(Boolean);
|
.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);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue