Files
OpenFlare/frontend/tests/a11y.test.tsx
T
ryan 7d03154a8a axe a11y 审计扩展到登录 OTP 验证表单(input-otp 分段输入,FieldLabel htmlFor 正确关联,零违规)与人机验证小部件手动模式(零违规)。环境修复:tests/setup.ts 加 ResizeObserver mock(input-otp 依赖,jsdom 未内置)。vitest 118→120 全绿。
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":120,"measure_s":71}
2026-08-16 20:58:24 +08:00

132 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { OTPForm } from '@/components/auth/otp-form';
import { CapWidget } from '@/components/auth/cap-widget';
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([]);
});
it('登录 OTP 验证表单无 axe 违规', async () => {
renderWithProviders(
<main>
<OTPForm
code=''
setCode={() => {}}
loginCodeTip='验证码已发送至邮箱'
loginCooldown={0}
isPending={false}
onResend={() => {}}
onSubmit={() => {}}
/>
</main>,
);
await screen.findByRole('button', { name: /重新发送/ });
expect(await runAxe(document.body)).toEqual([]);
});
it('人机验证小部件(手动模式)无 axe 违规', async () => {
renderWithProviders(<main><CapWidget autoStart={false} onToken={() => {}} /></main>);
await screen.findByRole('button');
expect(await runAxe(document.body)).toEqual([]);
});
});