mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-26 09:52:14 +03:00
[Feature]: Add a tooltip/hint to the "Password" field in the client form clarifying which protocols use it (#5809)
* feat(clients): clarify which protocols use the Password and Hysteria Auth fields Add tooltips to the Password and Hysteria Auth Form.Items in the client form, explaining that Password is only consumed by Trojan and Shadowsocks (ignored for VLESS, VMess, Hysteria, WireGuard) and that Hysteria Auth is the credential Hysteria actually uses. Adds passwordDesc/hysteriaAuthDesc keys to all 13 locale files, following the existing limitIpDesc/totalGBDesc tooltip convention. Closes #5803 * test(clients): assert Password/Hysteria Auth tooltip hints render
This commit is contained in:
@@ -791,7 +791,7 @@ export default function ClientFormModal({
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('pages.clients.password')}>
|
||||
<Form.Item label={t('pages.clients.password')} tooltip={t('pages.clients.passwordDesc')}>
|
||||
<Space.Compact style={{ display: 'flex' }}>
|
||||
<Input value={form.password} style={{ flex: 1 }} onChange={(e) => update('password', e.target.value)} />
|
||||
<Button aria-label={t('regenerate')} icon={<ReloadOutlined />} onClick={regeneratePassword} />
|
||||
@@ -805,7 +805,7 @@ export default function ClientFormModal({
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('pages.clients.hysteriaAuth')}>
|
||||
<Form.Item label={t('pages.clients.hysteriaAuth')} tooltip={t('pages.clients.hysteriaAuthDesc')}>
|
||||
<Space.Compact style={{ display: 'flex' }}>
|
||||
<Input value={form.auth} style={{ flex: 1 }} onChange={(e) => update('auth', e.target.value)} />
|
||||
<Button aria-label={t('regenerate')} icon={<ReloadOutlined />} onClick={() => update('auth', RandomUtil.randomLowerAndNum(16))} />
|
||||
|
||||
71
frontend/src/test/client-form-modal.test.tsx
Normal file
71
frontend/src/test/client-form-modal.test.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { fireEvent, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import ClientFormModal from '@/pages/clients/ClientFormModal';
|
||||
import { renderWithProviders } from './test-utils';
|
||||
|
||||
// ClientFormModal reads server state via react-query (useFail2banStatusQuery),
|
||||
// so it needs a QueryClientProvider on top of the shared ThemeProvider wrapper.
|
||||
function renderModal() {
|
||||
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
renderWithProviders(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ClientFormModal
|
||||
open
|
||||
mode="add"
|
||||
client={null}
|
||||
inbounds={[]}
|
||||
save={vi.fn().mockResolvedValue(null)}
|
||||
onOpenChange={() => {}}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
function openCredentialsTab() {
|
||||
const tab = Array.from(document.querySelectorAll('.ant-tabs-tab'))
|
||||
.find((t) => (t.textContent ?? '').trim() === 'Credentials');
|
||||
if (!tab) throw new Error('Credentials tab not found');
|
||||
fireEvent.click(tab);
|
||||
}
|
||||
|
||||
function tooltipIconForLabel(label: string): HTMLElement {
|
||||
const labelEl = Array.from(document.querySelectorAll('.ant-form-item-label label'))
|
||||
.find((l) => (l.textContent ?? '').trim() === label);
|
||||
const item = labelEl?.closest('.ant-form-item') as HTMLElement | null;
|
||||
if (!item) throw new Error(`Form item not found for label: ${label}`);
|
||||
const tip = item.querySelector('.ant-form-item-tooltip') as HTMLElement | null;
|
||||
if (!tip) throw new Error(`No tooltip on form item: ${label}`);
|
||||
return tip;
|
||||
}
|
||||
|
||||
describe('ClientFormModal credential tooltips', () => {
|
||||
it('explains that the Password field is only consumed by Trojan/Shadowsocks', async () => {
|
||||
renderModal();
|
||||
openCredentialsTab();
|
||||
|
||||
const tip = tooltipIconForLabel('Password');
|
||||
fireEvent.mouseEnter(tip);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.body.textContent).toContain(
|
||||
'Only used by Trojan and Shadowsocks clients; ignored for VLESS, VMess, Hysteria, and WireGuard.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('explains that Hysteria Auth is the credential Hysteria actually uses', async () => {
|
||||
renderModal();
|
||||
openCredentialsTab();
|
||||
|
||||
const tip = tooltipIconForLabel('Hysteria Auth');
|
||||
fireEvent.mouseEnter(tip);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.body.textContent).toContain(
|
||||
'Credential used only by Hysteria clients. Trojan and Shadowsocks use the Password field instead.',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user