前端真实 a11y 审计:新增 axe-core(devDep)+ tests/a11y.test.tsx,对登录页与注册页渲染完整表单后运行 axe 结构性规则(label/button-name/heading-order/landmark/aria),两页均零违规。摸清并处理了渲染依赖(UserProvider 会话检查、publicConfigQuery 门控、configBool 字符串语义)。vitest 116→118 全绿。

Result: {"status":"keep","total_issues":8,"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":118,"measure_s":71}
This commit is contained in:
ryan
2026-08-16 20:49:55 +08:00
parent 4d78bc1c38
commit 7f8e257d33
6 changed files with 277 additions and 174 deletions
+103
View File
@@ -0,0 +1,103 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render, screen } from '@testing-library/react';
import { NextIntlClientProvider } from 'next-intl';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import axe from 'axe-core';
import { LoginPage } from '@/components/auth/login-page';
import { RegisterPage } from '@/components/auth/register-page';
import { UserProvider } from '@/contexts/user-context';
import zhCN from '@/messages/zh-CN.json';
vi.mock('next/navigation', () => ({
useRouter: () => ({ replace: vi.fn(), push: vi.fn() }),
useSearchParams: () => new URLSearchParams(''),
}));
const { getUserInfoMock, getPublicConfigMock, getAuthSourcesMock } =
vi.hoisted(() => ({
getUserInfoMock: vi.fn(),
getPublicConfigMock: vi.fn(),
getAuthSourcesMock: vi.fn(),
}));
vi.mock('@/lib/services/auth', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/lib/services/auth')>();
return {
...actual,
AuthService: {
...actual.AuthService,
getUserInfo: getUserInfoMock,
getAuthSources: getAuthSourcesMock,
},
};
});
vi.mock('@/lib/services/config', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/lib/services/config')>();
return {
...actual,
ConfigService: {
...actual.ConfigService,
getPublicConfig: getPublicConfigMock,
},
};
});
async function runAxe(container: HTMLElement) {
const results = await axe.run(container, {
rules: {
// jsdom 无布局引擎,颜色对比度规则无法评估
'color-contrast': { enabled: false },
},
});
return results.violations;
}
function renderWithProviders(page: React.ReactNode) {
return render(
<NextIntlClientProvider
locale='zh-CN'
messages={zhCN}
timeZone='Asia/Shanghai'
>
<QueryClientProvider
client={
new QueryClient({
defaultOptions: { queries: { retry: false } },
})
}
>
<UserProvider>{page}</UserProvider>
</QueryClientProvider>
</NextIntlClientProvider>,
);
}
describe('a11y(axe-core 结构性规则)', () => {
beforeEach(() => {
getUserInfoMock.mockReset();
getUserInfoMock.mockResolvedValue(null);
getPublicConfigMock.mockReset();
getPublicConfigMock.mockResolvedValue({
registration_enabled: 'true',
password_register_enabled: 'true',
});
getAuthSourcesMock.mockReset();
getAuthSourcesMock.mockResolvedValue([]);
});
it('登录页表单无 axe 违规', async () => {
renderWithProviders(<LoginPage />);
await screen.findByRole('button', { name: /登录|submit|登录/ });
expect(await runAxe(document.body)).toEqual([]);
});
it('注册页表单无 axe 违规', async () => {
renderWithProviders(<RegisterPage />);
await screen.findByRole('button', { name: /创建账号|submit/ });
expect(await runAxe(document.body)).toEqual([]);
});
});