mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-12 18:26:36 +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:
@@ -27,3 +27,4 @@
|
|||||||
{"run":26,"commit":"7d03154","metric":8,"metrics":{"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_vetx_total":0,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"vitest_failed":0,"vitest_total":120,"measure_s":71},"status":"keep","description":"axe a11y 审计扩展到登录 OTP 验证表单(input-otp 分段输入,FieldLabel htmlFor 正确关联,零违规)与人机验证小部件手动模式(零违规)。环境修复:tests/setup.ts 加 ResizeObserver mock(input-otp 依赖,jsdom 未内置)。vitest 118→120 全绿。","timestamp":1786885103995,"segment":0,"confidence":null,"asi":{"hypothesis":"把 axe a11y 审计扩展到登录/注册外的关键表单状态:登录 OTP 验证(input-otp 分段输入是经典 a11y 雷区)与人机验证小部件(CapWidget 手动模式)","insight":"新增 2 个 axe 测试(vitest 118→120)。OTPForm 验证通过 —— FieldLabel htmlFor='otp-verification' 与 InputOTP id 正确关联,分段输入零违规;CapWidget 手动模式零违规。环境修复:input-otp 依赖 ResizeObserver,jsdom 未内置 → tests/setup.ts 加 ResizeObserver mock(全局,其它测试无害)。渲染上下文修正:裸组件需 <main> landmark 包裹(region 规则,真实页面由 AuthShell 提供)。scripts/ Go 代码用扩展 linter 集扫描为 0 问题(死路,未纳入基准)。metric 持平 8","next_action_hint":"a11y 覆盖已达:登录/注册页/OTP/CapWidget 全零违规。剩余页面(设置页、admin)需较重 mock。会话可收尾,或周期跑全量 -race 验证无 flake"}}
|
{"run":26,"commit":"7d03154","metric":8,"metrics":{"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_vetx_total":0,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"vitest_failed":0,"vitest_total":120,"measure_s":71},"status":"keep","description":"axe a11y 审计扩展到登录 OTP 验证表单(input-otp 分段输入,FieldLabel htmlFor 正确关联,零违规)与人机验证小部件手动模式(零违规)。环境修复:tests/setup.ts 加 ResizeObserver mock(input-otp 依赖,jsdom 未内置)。vitest 118→120 全绿。","timestamp":1786885103995,"segment":0,"confidence":null,"asi":{"hypothesis":"把 axe a11y 审计扩展到登录/注册外的关键表单状态:登录 OTP 验证(input-otp 分段输入是经典 a11y 雷区)与人机验证小部件(CapWidget 手动模式)","insight":"新增 2 个 axe 测试(vitest 118→120)。OTPForm 验证通过 —— FieldLabel htmlFor='otp-verification' 与 InputOTP id 正确关联,分段输入零违规;CapWidget 手动模式零违规。环境修复:input-otp 依赖 ResizeObserver,jsdom 未内置 → tests/setup.ts 加 ResizeObserver mock(全局,其它测试无害)。渲染上下文修正:裸组件需 <main> landmark 包裹(region 规则,真实页面由 AuthShell 提供)。scripts/ Go 代码用扩展 linter 集扫描为 0 问题(死路,未纳入基准)。metric 持平 8","next_action_hint":"a11y 覆盖已达:登录/注册页/OTP/CapWidget 全零违规。剩余页面(设置页、admin)需较重 mock。会话可收尾,或周期跑全量 -race 验证无 flake"}}
|
||||||
{"run":27,"commit":"6c128e0","metric":8,"metrics":{"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_vetx_total":0,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"vitest_failed":0,"vitest_total":121,"measure_s":71},"status":"keep","description":"axe a11y 审计扩展到最复杂认证路径:注册页开启人机验证(CapWidget 自动求解→已通过状态 + 完整表单),mock getCapToken 避免 jsdom 无 Worker 环境限制。零违规。vitest 120→121 全绿。","timestamp":1786885536618,"segment":0,"confidence":null,"asi":{"hypothesis":"axe 审计覆盖最复杂的认证渲染路径:注册页开启人机验证(CapWidget 自动求解→已通过状态 + 完整表单)","insight":"新增第 5 个 axe 测试(vitest 120→121):mock @/lib/cap-solver 的 getCapToken 直接 resolve token(避免 jsdom 无 Web Worker),config 开 cap_login_enabled/cap_auto_solve,注册页渲染出 CAPTCHA 已通过态 + 表单全字段 → 零违规。vi.mock('@/lib/cap-solver') 对其它测试无害(仅 capEnabled 时渲染 CapWidget)。metric 持平 8","next_action_hint":"axe 覆盖已达 5 个认证表单态(登录/注册/OTP/验证小部件手动/注册+验证)。剩余:设置页与 admin 页需较重 mock。可收尾,或周期跑全量 -race 验证无 flake"}}
|
{"run":27,"commit":"6c128e0","metric":8,"metrics":{"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_vetx_total":0,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"vitest_failed":0,"vitest_total":121,"measure_s":71},"status":"keep","description":"axe a11y 审计扩展到最复杂认证路径:注册页开启人机验证(CapWidget 自动求解→已通过状态 + 完整表单),mock getCapToken 避免 jsdom 无 Worker 环境限制。零违规。vitest 120→121 全绿。","timestamp":1786885536618,"segment":0,"confidence":null,"asi":{"hypothesis":"axe 审计覆盖最复杂的认证渲染路径:注册页开启人机验证(CapWidget 自动求解→已通过状态 + 完整表单)","insight":"新增第 5 个 axe 测试(vitest 120→121):mock @/lib/cap-solver 的 getCapToken 直接 resolve token(避免 jsdom 无 Web Worker),config 开 cap_login_enabled/cap_auto_solve,注册页渲染出 CAPTCHA 已通过态 + 表单全字段 → 零违规。vi.mock('@/lib/cap-solver') 对其它测试无害(仅 capEnabled 时渲染 CapWidget)。metric 持平 8","next_action_hint":"axe 覆盖已达 5 个认证表单态(登录/注册/OTP/验证小部件手动/注册+验证)。剩余:设置页与 admin 页需较重 mock。可收尾,或周期跑全量 -race 验证无 flake"}}
|
||||||
{"run":28,"commit":"40eee77","metric":8,"metrics":{"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_vetx_total":0,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"vitest_failed":0,"vitest_total":121,"measure_s":0},"status":"keep","description":"前端显式 any 类型清理 2→0:Slot children?: any → ReactNode | MotionValue 联合(motion 真实类型),顺带修复潜在崩溃(原代码在 isValidElement 前访问 children.type,缺失时 TypeError,现无效 children 返回 null,hooks 无条件合规);useControlledState Rest extends any[] → unknown[]。两处 eslint-disable 注释删除。tsc/eslint/vitest 121 全绿。","timestamp":1786886086713,"segment":0,"confidence":null,"asi":{"hypothesis":"前端显式 any 类型清理:全仓 grep 仅 2 处 any —— Slot children?: any 与 useControlledState 的 Rest extends any[],均为真实类型缺陷","insight":"全前端 any 计数 2→0。slot.tsx:children?: any → React.ReactNode | MotionValue<string> | MotionValue<number>(motion HTMLMotionProps 的真实 children 类型);顺带修复潜在崩溃 —— 原代码在 isValidElement 检查前就访问 children.type,children 缺失时 TypeError,改为 isValidChild/childrenType 先计算(hooks 无条件,rules-of-hooks 合规),无效 children 返回 null。use-controlled-state.tsx:Rest extends any[] → unknown[]。两处 eslint-disable no-explicit-any 注释随之删除(无抑制注释)。tsc/eslint/vitest 121/checks.sh 全绿。benchmark 无关(metric 持平 8)。注意:run #28 的 run_experiment 被用户中断(aborted),但代码修复已通过全部门禁验证","next_action_hint":"用户要求合并到 main 并推送"}}
|
{"run":28,"commit":"40eee77","metric":8,"metrics":{"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_vetx_total":0,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"vitest_failed":0,"vitest_total":121,"measure_s":0},"status":"keep","description":"前端显式 any 类型清理 2→0:Slot children?: any → ReactNode | MotionValue 联合(motion 真实类型),顺带修复潜在崩溃(原代码在 isValidElement 前访问 children.type,缺失时 TypeError,现无效 children 返回 null,hooks 无条件合规);useControlledState Rest extends any[] → unknown[]。两处 eslint-disable 注释删除。tsc/eslint/vitest 121 全绿。","timestamp":1786886086713,"segment":0,"confidence":null,"asi":{"hypothesis":"前端显式 any 类型清理:全仓 grep 仅 2 处 any —— Slot children?: any 与 useControlledState 的 Rest extends any[],均为真实类型缺陷","insight":"全前端 any 计数 2→0。slot.tsx:children?: any → React.ReactNode | MotionValue<string> | MotionValue<number>(motion HTMLMotionProps 的真实 children 类型);顺带修复潜在崩溃 —— 原代码在 isValidElement 检查前就访问 children.type,children 缺失时 TypeError,改为 isValidChild/childrenType 先计算(hooks 无条件,rules-of-hooks 合规),无效 children 返回 null。use-controlled-state.tsx:Rest extends any[] → unknown[]。两处 eslint-disable no-explicit-any 注释随之删除(无抑制注释)。tsc/eslint/vitest 121/checks.sh 全绿。benchmark 无关(metric 持平 8)。注意:run #28 的 run_experiment 被用户中断(aborted),但代码修复已通过全部门禁验证","next_action_hint":"用户要求合并到 main 并推送"}}
|
||||||
|
{"run":29,"commit":"511bed8","metric":8,"metrics":{"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":63,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_exhaustive":0,"golint_vetx_total":0,"vitest_failed":0,"vitest_total":124},"status":"keep","description":"修复 2 个新增 unconvert 问题(linux.go 中 int64(stat.Bsize) 恒等转换,Statfs_t.Bsize 在 Linux 上本就是 int64),删除多余转换零行为变化;total 10→8 回到 5 维全下限。","timestamp":1786894372432,"segment":0,"confidence":null,"asi":{"category":"unconvert","hypothesis":"会话恢复后 measure 显示 total=10,出现 2 个新的 unconvert 问题(internal/apps/edge/observability/linux.go:261-262 的 int64(stat.Bsize) 恒等转换,Linux Statfs_t.Bsize 本就是 int64)。删除多余转换,零行为变化","finding":"unconvert 是 repo 自带配置启用的 linter,此前 baseline 无此问题,最近用户提交/Go 版本变化后新增;修复后 5 维回到全下限 8","next_action_hint":"会话恢复点确认:total=8(5 维全下限,8 项均为有据可查的刻意保留)。下一轮候选:静态检查新维度(staticcheck SA 系列在 repo 配置中已启用且为 0)、或把 docs/ 下 vitepress 站点的构建纳入 measure 防回归(docs build 不属质量计数,不进基准)"}}
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ export function AuthSourceModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
|
aria-label={t('enableAuthSource')}
|
||||||
checked={form.is_active}
|
checked={form.is_active}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
setForm((prev) => ({ ...prev, is_active: checked }))
|
setForm((prev) => ({ ...prev, is_active: checked }))
|
||||||
|
|||||||
@@ -314,6 +314,7 @@ export function OtherTab({ configs }: OtherTabProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
<Switch
|
<Switch
|
||||||
|
aria-label={tNav(item.labelKey)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
disabled={
|
disabled={
|
||||||
isReadOnly || updateMenuConfigMutation.isPending
|
isReadOnly || updateMenuConfigMutation.isPending
|
||||||
|
|||||||
@@ -329,15 +329,19 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
|
|||||||
<div className='space-y-1'>
|
<div className='space-y-1'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
{Icon && <Icon className='size-4 text-primary' />}
|
{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)}
|
{t(item.titleKey)}
|
||||||
</span>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<p className='text-xs text-muted-foreground leading-relaxed pr-2'>
|
<p className='text-xs text-muted-foreground leading-relaxed pr-2'>
|
||||||
{t(item.descKey)}
|
{t(item.descKey)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
|
id={item.key}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
disabled={updateConfigMutation.isPending}
|
disabled={updateConfigMutation.isPending}
|
||||||
onCheckedChange={(value) => handleToggle(item.key, value)}
|
onCheckedChange={(value) => handleToggle(item.key, value)}
|
||||||
@@ -366,7 +370,10 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
|
|||||||
disabled={updateTTLMutation.isPending}
|
disabled={updateTTLMutation.isPending}
|
||||||
onValueChange={handleTTLChange}
|
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')} />
|
<SelectValue placeholder={t('selectRetentionTime')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -554,15 +561,22 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
|
|||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
handleToggle('cap_login_enabled', checked)
|
handleToggle('cap_login_enabled', checked)
|
||||||
}
|
}
|
||||||
|
aria-label={t('loginCaptchaEnabled')}
|
||||||
/>
|
/>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='pt-6'>
|
<CardContent className='pt-6'>
|
||||||
{/* 自动开始计算 Switch */}
|
{/* 自动开始计算 Switch */}
|
||||||
<div className='flex items-center justify-between rounded-xl border border-dashed p-4 bg-card mb-4'>
|
<div className='flex items-center justify-between rounded-xl border border-dashed p-4 bg-card mb-4'>
|
||||||
<div className='space-y-0.5'>
|
<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>
|
</div>
|
||||||
<Switch checked={capAutoSolve} onCheckedChange={setCapAutoSolve} />
|
<Switch
|
||||||
|
id='cap_auto_solve'
|
||||||
|
checked={capAutoSolve}
|
||||||
|
onCheckedChange={setCapAutoSolve}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={handleCapSave} className='space-y-6'>
|
<form onSubmit={handleCapSave} className='space-y-6'>
|
||||||
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
|
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
|
||||||
|
|||||||
@@ -320,6 +320,7 @@
|
|||||||
"confirmDelete": "Confirm delete",
|
"confirmDelete": "Confirm delete",
|
||||||
"deleting": "Deleting...",
|
"deleting": "Deleting...",
|
||||||
"noAuthSources": "No authentication sources configured. Click the button above to add one.",
|
"noAuthSources": "No authentication sources configured. Click the button above to add one.",
|
||||||
|
"loginCaptchaEnabled": "Login CAPTCHA",
|
||||||
"captchaConfig": "CAPTCHA configuration (Cap 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",
|
"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",
|
"autoStartSolving": "Start solving automatically when page loads",
|
||||||
|
|||||||
@@ -320,6 +320,7 @@
|
|||||||
"confirmDelete": "确认删除",
|
"confirmDelete": "确认删除",
|
||||||
"deleting": "删除中...",
|
"deleting": "删除中...",
|
||||||
"noAuthSources": "暂无配置的认证源,点击上方按钮新增",
|
"noAuthSources": "暂无配置的认证源,点击上方按钮新增",
|
||||||
|
"loginCaptchaEnabled": "登录人机验证",
|
||||||
"captchaConfig": "人机验证配置 (Cap CAPTCHA)",
|
"captchaConfig": "人机验证配置 (Cap CAPTCHA)",
|
||||||
"captchaConfigDesc": "配置基于 Proof-of-Work (PoW) 的无感人机验证,保护系统登录免受暴力破解和撞库攻击",
|
"captchaConfigDesc": "配置基于 Proof-of-Work (PoW) 的无感人机验证,保护系统登录免受暴力破解和撞库攻击",
|
||||||
"autoStartSolving": "打开页面后自动开始计算",
|
"autoStartSolving": "打开页面后自动开始计算",
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ import { LoginPage } from '@/components/auth/login-page';
|
|||||||
import { RegisterPage } from '@/components/auth/register-page';
|
import { RegisterPage } from '@/components/auth/register-page';
|
||||||
import { OTPForm } from '@/components/auth/otp-form';
|
import { OTPForm } from '@/components/auth/otp-form';
|
||||||
import { CapWidget } from '@/components/auth/cap-widget';
|
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 { UserProvider } from '@/contexts/user-context';
|
||||||
|
import type { SystemConfig } from '@/lib/services/admin';
|
||||||
import zhCN from '@/messages/zh-CN.json';
|
import zhCN from '@/messages/zh-CN.json';
|
||||||
|
|
||||||
vi.mock('next/navigation', () => ({
|
vi.mock('next/navigation', () => ({
|
||||||
@@ -16,13 +20,17 @@ vi.mock('next/navigation', () => ({
|
|||||||
useSearchParams: () => new URLSearchParams(''),
|
useSearchParams: () => new URLSearchParams(''),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { getUserInfoMock, getPublicConfigMock, getAuthSourcesMock } = vi.hoisted(
|
const {
|
||||||
() => ({
|
getUserInfoMock,
|
||||||
getUserInfoMock: vi.fn(),
|
getPublicConfigMock,
|
||||||
getPublicConfigMock: vi.fn(),
|
getAuthSourcesMock,
|
||||||
getAuthSourcesMock: vi.fn(),
|
listAuthSourcesMock,
|
||||||
}),
|
} = vi.hoisted(() => ({
|
||||||
);
|
getUserInfoMock: vi.fn(),
|
||||||
|
getPublicConfigMock: vi.fn(),
|
||||||
|
getAuthSourcesMock: vi.fn(),
|
||||||
|
listAuthSourcesMock: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('@/lib/services/auth', async (importOriginal) => {
|
vi.mock('@/lib/services/auth', async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import('@/lib/services/auth')>();
|
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', () => ({
|
vi.mock('@/lib/cap-solver', () => ({
|
||||||
getCapToken: vi.fn().mockResolvedValue('test-cap-token'),
|
getCapToken: vi.fn().mockResolvedValue('test-cap-token'),
|
||||||
@@ -92,6 +108,8 @@ describe('a11y(axe-core 结构性规则)', () => {
|
|||||||
});
|
});
|
||||||
getAuthSourcesMock.mockReset();
|
getAuthSourcesMock.mockReset();
|
||||||
getAuthSourcesMock.mockResolvedValue([]);
|
getAuthSourcesMock.mockResolvedValue([]);
|
||||||
|
listAuthSourcesMock.mockReset();
|
||||||
|
listAuthSourcesMock.mockResolvedValue([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('登录页表单无 axe 违规', async () => {
|
it('登录页表单无 axe 违规', async () => {
|
||||||
@@ -153,3 +171,69 @@ describe('a11y(axe-core 结构性规则)', () => {
|
|||||||
expect(await runAxe(document.body)).toEqual([]);
|
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([]);
|
||||||
|
});
|
||||||
|
|||||||
@@ -258,8 +258,8 @@ func StatFilesystem(path string) (int64, int64) {
|
|||||||
if err := syscall.Statfs(absPath, &stat); err != nil {
|
if err := syscall.Statfs(absPath, &stat); err != nil {
|
||||||
return 0, 0
|
return 0, 0
|
||||||
}
|
}
|
||||||
total := multiplyUint64Int64(stat.Blocks, int64(stat.Bsize))
|
total := multiplyUint64Int64(stat.Blocks, stat.Bsize)
|
||||||
free := multiplyUint64Int64(stat.Bavail, int64(stat.Bsize))
|
free := multiplyUint64Int64(stat.Bavail, stat.Bsize)
|
||||||
used := max(total-free, 0)
|
used := max(total-free, 0)
|
||||||
return total, used
|
return total, used
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user