This commit is contained in:
277
src/components/auth/AccountModal.test.tsx
Normal file
277
src/components/auth/AccountModal.test.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import type {
|
||||
AuthAuditLogEntry,
|
||||
AuthRiskBlockSummary,
|
||||
AuthSessionSummary,
|
||||
AuthUser,
|
||||
} from '../../services/authService';
|
||||
import { AccountModal } from './AccountModal';
|
||||
|
||||
const baseUser: AuthUser = {
|
||||
id: 'user-1',
|
||||
username: 'tester',
|
||||
displayName: '138****8000',
|
||||
publicUserCode: 'user-tester',
|
||||
phoneNumberMasked: '138****8000',
|
||||
loginMethod: 'phone',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: true,
|
||||
};
|
||||
|
||||
function renderAccountModal(overrides?: {
|
||||
user?: AuthUser;
|
||||
riskBlocks?: AuthRiskBlockSummary[];
|
||||
sessions?: AuthSessionSummary[];
|
||||
auditLogs?: AuthAuditLogEntry[];
|
||||
initialSection?:
|
||||
| 'appearance'
|
||||
| 'account'
|
||||
| 'security'
|
||||
| 'devices'
|
||||
| 'logs'
|
||||
| null;
|
||||
}) {
|
||||
return render(
|
||||
<AccountModal
|
||||
user={overrides?.user ?? baseUser}
|
||||
isOpen
|
||||
initialSection={overrides?.initialSection ?? null}
|
||||
platformTheme="light"
|
||||
riskBlocks={overrides?.riskBlocks ?? []}
|
||||
sessions={overrides?.sessions ?? []}
|
||||
auditLogs={overrides?.auditLogs ?? []}
|
||||
loadingRiskBlocks={false}
|
||||
loadingSessions={false}
|
||||
loadingAuditLogs={false}
|
||||
isHydratingSettings={false}
|
||||
isPersistingSettings={false}
|
||||
settingsError={null}
|
||||
onClose={vi.fn()}
|
||||
onPlatformThemeChange={vi.fn()}
|
||||
onLogout={vi.fn().mockResolvedValue(undefined)}
|
||||
onRefreshRiskBlocks={vi.fn().mockResolvedValue(undefined)}
|
||||
onLiftRiskBlock={vi.fn().mockResolvedValue(undefined)}
|
||||
onRefreshSessions={vi.fn().mockResolvedValue(undefined)}
|
||||
onLogoutAll={vi.fn().mockResolvedValue(undefined)}
|
||||
onRefreshAuditLogs={vi.fn().mockResolvedValue(undefined)}
|
||||
onRevokeSession={vi.fn().mockResolvedValue(undefined)}
|
||||
changePhoneCaptchaChallenge={null}
|
||||
onSendChangePhoneCode={vi.fn().mockResolvedValue({
|
||||
cooldownSeconds: 60,
|
||||
expiresInSeconds: 300,
|
||||
})}
|
||||
onChangePhone={vi.fn().mockResolvedValue(undefined)}
|
||||
onChangePassword={vi.fn().mockResolvedValue(undefined)}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
test('settings header uses a generic title instead of the phone number', () => {
|
||||
renderAccountModal();
|
||||
|
||||
expect(screen.getByRole('dialog', { name: '设置与账号安全' })).toBeTruthy();
|
||||
expect(screen.getByText('设置与账号安全')).toBeTruthy();
|
||||
expect(screen.queryByText('138****8000')).toBeNull();
|
||||
expect(screen.queryByText('选择要管理的内容')).toBeNull();
|
||||
expect(
|
||||
screen.queryByText('主题、账号与设备能力统一在独立面板中管理'),
|
||||
).toBeNull();
|
||||
expect(screen.queryByText(/^安全状态$/)).toBeNull();
|
||||
expect(screen.queryByText(/^登录设备$/)).toBeNull();
|
||||
expect(screen.queryByText(/^操作记录$/)).toBeNull();
|
||||
expect(screen.queryByText('当前账号状态')).toBeNull();
|
||||
expect(screen.queryByText('当前主题')).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: '退出登录' })).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: '退出全部设备' })).toBeNull();
|
||||
});
|
||||
|
||||
test('account actions open in independent panels instead of inline expansion', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderAccountModal();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /账号信息/ }));
|
||||
|
||||
const accountDialog = screen.getByRole('dialog', { name: '账号信息' });
|
||||
expect(accountDialog).toBeTruthy();
|
||||
expect(
|
||||
within(accountDialog).getByRole('button', { name: '返回' }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(accountDialog).getByRole('button', { name: '更换手机号' }),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByLabelText('新手机号')).toBeNull();
|
||||
|
||||
await user.click(
|
||||
within(accountDialog).getByRole('button', { name: '更换手机号' }),
|
||||
);
|
||||
|
||||
const changePhoneDialog = screen.getByRole('dialog', {
|
||||
name: '绑定新手机号',
|
||||
});
|
||||
expect(within(changePhoneDialog).getByLabelText('新手机号')).toBeTruthy();
|
||||
expect(within(changePhoneDialog).getByLabelText('验证码')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('nested settings panels keep back navigation without an extra close action', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderAccountModal();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /账号信息/ }));
|
||||
|
||||
const accountDialog = screen.getByRole('dialog', { name: '账号信息' });
|
||||
const accountHeader = accountDialog.firstElementChild as HTMLElement | null;
|
||||
expect(
|
||||
within(accountDialog).getByRole('button', { name: '返回' }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
accountHeader?.lastElementChild?.textContent?.includes('返回'),
|
||||
).toBe(true);
|
||||
expect(
|
||||
within(accountDialog).queryByRole('button', { name: '关闭' }),
|
||||
).toBeNull();
|
||||
|
||||
await user.click(
|
||||
within(accountDialog).getByRole('button', { name: '更换手机号' }),
|
||||
);
|
||||
|
||||
const changePhoneDialog = screen.getByRole('dialog', {
|
||||
name: '绑定新手机号',
|
||||
});
|
||||
const changePhoneHeader =
|
||||
changePhoneDialog.firstElementChild as HTMLElement | null;
|
||||
expect(
|
||||
within(changePhoneDialog).getByRole('button', { name: '返回' }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
changePhoneHeader?.lastElementChild?.textContent?.includes('返回'),
|
||||
).toBe(true);
|
||||
expect(
|
||||
within(changePhoneDialog).queryByRole('button', { name: '关闭' }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test('settings overlays move focus away from inert triggers and restore it on back', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderAccountModal();
|
||||
|
||||
const accountTrigger = screen.getByRole('button', { name: /账号信息/ });
|
||||
expect(document.activeElement).not.toBe(accountTrigger);
|
||||
|
||||
await user.click(accountTrigger);
|
||||
|
||||
const accountDialog = screen.getByRole('dialog', { name: '账号信息' });
|
||||
const accountBackButton = within(accountDialog).getByRole('button', {
|
||||
name: '返回',
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.activeElement).toBe(accountBackButton);
|
||||
});
|
||||
|
||||
const changePhoneTrigger = within(accountDialog).getByRole('button', {
|
||||
name: '更换手机号',
|
||||
});
|
||||
await user.click(changePhoneTrigger);
|
||||
|
||||
const changePhoneDialog = screen.getByRole('dialog', {
|
||||
name: '绑定新手机号',
|
||||
});
|
||||
const changePhoneBackButton = within(changePhoneDialog).getByRole('button', {
|
||||
name: '返回',
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.activeElement).toBe(changePhoneBackButton);
|
||||
});
|
||||
|
||||
await user.click(changePhoneBackButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.activeElement).toBe(changePhoneTrigger);
|
||||
});
|
||||
|
||||
await user.click(accountBackButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.activeElement).toBe(accountTrigger);
|
||||
});
|
||||
});
|
||||
|
||||
test('account panel includes merged security devices and audit sections', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderAccountModal({
|
||||
riskBlocks: [
|
||||
{
|
||||
scopeType: 'phone',
|
||||
title: '手机号保护',
|
||||
detail: '检测到异常验证行为,已开启保护。',
|
||||
remainingSeconds: 600,
|
||||
expiresAt: '2026-04-20T10:00:00.000Z',
|
||||
},
|
||||
],
|
||||
sessions: [
|
||||
{
|
||||
sessionId: 'session-1',
|
||||
clientType: 'mobile',
|
||||
clientRuntime: 'ios',
|
||||
clientPlatform: 'wechat',
|
||||
clientLabel: 'iPhone 15 Pro',
|
||||
deviceDisplayName: 'iPhone 15 Pro / 微信',
|
||||
miniProgramAppId: null,
|
||||
miniProgramEnv: null,
|
||||
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)',
|
||||
isCurrent: true,
|
||||
createdAt: '2026-04-20T07:30:00.000Z',
|
||||
lastSeenAt: '2026-04-20T09:00:00.000Z',
|
||||
expiresAt: '2026-04-27T09:00:00.000Z',
|
||||
ipMasked: '10.0.*.*',
|
||||
},
|
||||
],
|
||||
auditLogs: [
|
||||
{
|
||||
id: 'log-1',
|
||||
eventType: 'phone_login',
|
||||
title: '登录成功',
|
||||
detail: '通过手机号验证码完成登录。',
|
||||
createdAt: '2026-04-20T08:00:00.000Z',
|
||||
ipMasked: '10.0.*.*',
|
||||
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /账号信息/ }));
|
||||
|
||||
const accountDialog = screen.getByRole('dialog', { name: '账号信息' });
|
||||
expect(within(accountDialog).getByText('安全状态')).toBeTruthy();
|
||||
expect(within(accountDialog).getByText('登录设备')).toBeTruthy();
|
||||
expect(within(accountDialog).getByText('操作记录')).toBeTruthy();
|
||||
expect(within(accountDialog).getByText('手机号保护')).toBeTruthy();
|
||||
expect(within(accountDialog).getByText('iPhone 15 Pro')).toBeTruthy();
|
||||
expect(within(accountDialog).getByText('登录成功')).toBeTruthy();
|
||||
expect(
|
||||
within(accountDialog).getByRole('button', { name: '退出登录' }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(accountDialog).getByRole('button', { name: '退出全部设备' }),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('legacy nested section requests now open the merged account panel', () => {
|
||||
renderAccountModal({ initialSection: 'security' });
|
||||
|
||||
const accountDialog = screen.getByRole('dialog', { name: '账号信息' });
|
||||
expect(accountDialog).toBeTruthy();
|
||||
expect(within(accountDialog).getByText('安全状态')).toBeTruthy();
|
||||
expect(within(accountDialog).getByText('登录设备')).toBeTruthy();
|
||||
expect(within(accountDialog).getByText('操作记录')).toBeTruthy();
|
||||
});
|
||||
Reference in New Issue
Block a user