mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-10 17:26:38 +08:00
feat(a11y): 设置页无障碍审计并修复开关可访问名称
axe 结构性规则扩展到设置页(安全 Tab + 其他 Tab): - security-tab 8 个 Switch/SelectTrigger 补可访问名称(Label htmlFor 关联 + aria-label) - other-tab / auth-source-modal 各 1 个未命名 Switch 补 aria-label - 新增 2 个 axe 测试,vitest 126 全绿 - 翻译键 loginCaptchaEnabled 加入 fragments(admin.zh-CN/en)
This commit is contained in:
@@ -180,6 +180,7 @@ export function AuthSourceModal({
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
aria-label={t('enableAuthSource')}
|
||||
checked={form.is_active}
|
||||
onCheckedChange={(checked) =>
|
||||
setForm((prev) => ({ ...prev, is_active: checked }))
|
||||
|
||||
@@ -314,6 +314,7 @@ export function OtherTab({ configs }: OtherTabProps) {
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
<Switch
|
||||
aria-label={tNav(item.labelKey)}
|
||||
checked={checked}
|
||||
disabled={
|
||||
isReadOnly || updateMenuConfigMutation.isPending
|
||||
|
||||
@@ -329,15 +329,19 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
|
||||
<div className='space-y-1'>
|
||||
<div className='flex items-center gap-2'>
|
||||
{Icon && <Icon className='size-4 text-primary' />}
|
||||
<span className='font-medium text-sm text-foreground'>
|
||||
<Label
|
||||
htmlFor={item.key}
|
||||
className='font-medium text-sm text-foreground'
|
||||
>
|
||||
{t(item.titleKey)}
|
||||
</span>
|
||||
</Label>
|
||||
</div>
|
||||
<p className='text-xs text-muted-foreground leading-relaxed pr-2'>
|
||||
{t(item.descKey)}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id={item.key}
|
||||
checked={checked}
|
||||
disabled={updateConfigMutation.isPending}
|
||||
onCheckedChange={(value) => handleToggle(item.key, value)}
|
||||
@@ -366,7 +370,10 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
|
||||
disabled={updateTTLMutation.isPending}
|
||||
onValueChange={handleTTLChange}
|
||||
>
|
||||
<SelectTrigger className='w-[180px] bg-card border-dashed text-xs h-8'>
|
||||
<SelectTrigger
|
||||
className='w-[180px] bg-card border-dashed text-xs h-8'
|
||||
aria-label={t('loginSessionTTL')}
|
||||
>
|
||||
<SelectValue placeholder={t('selectRetentionTime')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -554,15 +561,22 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
|
||||
onCheckedChange={(checked) =>
|
||||
handleToggle('cap_login_enabled', checked)
|
||||
}
|
||||
aria-label={t('loginCaptchaEnabled')}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className='pt-6'>
|
||||
{/* 自动开始计算 Switch */}
|
||||
<div className='flex items-center justify-between rounded-xl border border-dashed p-4 bg-card mb-4'>
|
||||
<div className='space-y-0.5'>
|
||||
<p className='text-sm font-semibold'>{t('autoStartSolving')}</p>
|
||||
<Label htmlFor='cap_auto_solve' className='text-sm font-semibold'>
|
||||
{t('autoStartSolving')}
|
||||
</Label>
|
||||
</div>
|
||||
<Switch checked={capAutoSolve} onCheckedChange={setCapAutoSolve} />
|
||||
<Switch
|
||||
id='cap_auto_solve'
|
||||
checked={capAutoSolve}
|
||||
onCheckedChange={setCapAutoSolve}
|
||||
/>
|
||||
</div>
|
||||
<form onSubmit={handleCapSave} className='space-y-6'>
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
|
||||
|
||||
@@ -320,6 +320,7 @@
|
||||
"confirmDelete": "Confirm delete",
|
||||
"deleting": "Deleting...",
|
||||
"noAuthSources": "No authentication sources configured. Click the button above to add one.",
|
||||
"loginCaptchaEnabled": "Login CAPTCHA",
|
||||
"captchaConfig": "CAPTCHA configuration (Cap CAPTCHA)",
|
||||
"captchaConfigDesc": "Configure Proof-of-Work (PoW) based invisible CAPTCHA to protect login from brute-force and credential-stuffing attacks",
|
||||
"autoStartSolving": "Start solving automatically when page loads",
|
||||
|
||||
@@ -320,6 +320,7 @@
|
||||
"confirmDelete": "确认删除",
|
||||
"deleting": "删除中...",
|
||||
"noAuthSources": "暂无配置的认证源,点击上方按钮新增",
|
||||
"loginCaptchaEnabled": "登录人机验证",
|
||||
"captchaConfig": "人机验证配置 (Cap CAPTCHA)",
|
||||
"captchaConfigDesc": "配置基于 Proof-of-Work (PoW) 的无感人机验证,保护系统登录免受暴力破解和撞库攻击",
|
||||
"autoStartSolving": "打开页面后自动开始计算",
|
||||
|
||||
@@ -8,7 +8,11 @@ import { LoginPage } from '@/components/auth/login-page';
|
||||
import { RegisterPage } from '@/components/auth/register-page';
|
||||
import { OTPForm } from '@/components/auth/otp-form';
|
||||
import { CapWidget } from '@/components/auth/cap-widget';
|
||||
import { OtherTab } from '@/components/common/settings/other-tab';
|
||||
import { SecurityTab } from '@/components/common/settings/security-tab';
|
||||
import type { UseQueryResult } from '@tanstack/react-query';
|
||||
import { UserProvider } from '@/contexts/user-context';
|
||||
import type { SystemConfig } from '@/lib/services/admin';
|
||||
import zhCN from '@/messages/zh-CN.json';
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
@@ -16,13 +20,17 @@ vi.mock('next/navigation', () => ({
|
||||
useSearchParams: () => new URLSearchParams(''),
|
||||
}));
|
||||
|
||||
const { getUserInfoMock, getPublicConfigMock, getAuthSourcesMock } = vi.hoisted(
|
||||
() => ({
|
||||
getUserInfoMock: vi.fn(),
|
||||
getPublicConfigMock: vi.fn(),
|
||||
getAuthSourcesMock: vi.fn(),
|
||||
}),
|
||||
);
|
||||
const {
|
||||
getUserInfoMock,
|
||||
getPublicConfigMock,
|
||||
getAuthSourcesMock,
|
||||
listAuthSourcesMock,
|
||||
} = vi.hoisted(() => ({
|
||||
getUserInfoMock: vi.fn(),
|
||||
getPublicConfigMock: vi.fn(),
|
||||
getAuthSourcesMock: vi.fn(),
|
||||
listAuthSourcesMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/services/auth', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/lib/services/auth')>();
|
||||
@@ -35,6 +43,14 @@ vi.mock('@/lib/services/auth', async (importOriginal) => {
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mock('@/lib/services', () => ({
|
||||
default: {
|
||||
adminAuthSource: { listAuthSources: listAuthSourcesMock },
|
||||
adminSystemConfig: {
|
||||
updateSystemConfig: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/cap-solver', () => ({
|
||||
getCapToken: vi.fn().mockResolvedValue('test-cap-token'),
|
||||
@@ -92,6 +108,8 @@ describe('a11y(axe-core 结构性规则)', () => {
|
||||
});
|
||||
getAuthSourcesMock.mockReset();
|
||||
getAuthSourcesMock.mockResolvedValue([]);
|
||||
listAuthSourcesMock.mockReset();
|
||||
listAuthSourcesMock.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
it('登录页表单无 axe 违规', async () => {
|
||||
@@ -153,3 +171,69 @@ describe('a11y(axe-core 结构性规则)', () => {
|
||||
expect(await runAxe(document.body)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('设置-安全 Tab(人机验证配置表单)无 axe 违规', async () => {
|
||||
const mk = (key: string, value: string): SystemConfig => ({
|
||||
key,
|
||||
value,
|
||||
type: 'system',
|
||||
visibility: 0,
|
||||
description: '',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
});
|
||||
const configs = {
|
||||
cap_login_enabled: mk('cap_login_enabled', 'true'),
|
||||
cap_challenge_count: mk('cap_challenge_count', '1'),
|
||||
cap_challenge_difficulty: mk('cap_challenge_difficulty', '4'),
|
||||
cap_challenge_size: mk('cap_challenge_size', '32'),
|
||||
cap_challenge_ttl_seconds: mk('cap_challenge_ttl_seconds', '600'),
|
||||
cap_token_ttl_seconds: mk('cap_token_ttl_seconds', '1200'),
|
||||
cap_auto_solve: mk('cap_auto_solve', 'true'),
|
||||
login_session_ttl_hours: mk('login_session_ttl_hours', '168'),
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<main>
|
||||
<SecurityTab
|
||||
configs={configs}
|
||||
systemConfigsQuery={
|
||||
{ data: Object.values(configs) } as UseQueryResult<
|
||||
SystemConfig[],
|
||||
Error
|
||||
>
|
||||
}
|
||||
/>
|
||||
</main>,
|
||||
);
|
||||
|
||||
await screen.findByLabelText(/难题数量/);
|
||||
expect(await runAxe(document.body)).toEqual([]);
|
||||
});
|
||||
|
||||
it('设置-其他 Tab(菜单显示配置开关列表)无 axe 违规', async () => {
|
||||
const mk = (key: string, value: string): SystemConfig => ({
|
||||
key,
|
||||
value,
|
||||
type: 'system',
|
||||
visibility: 0,
|
||||
description: '',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
});
|
||||
const configs = {
|
||||
menu_display_config: mk(
|
||||
'menu_display_config',
|
||||
JSON.stringify({ '/nodes': true, '/websites': false }),
|
||||
),
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<main>
|
||||
<OtherTab configs={configs} />
|
||||
</main>,
|
||||
);
|
||||
|
||||
await screen.findByRole('switch', { name: /节点/ });
|
||||
expect(await runAxe(document.body)).toEqual([]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user