收口认证表单输入组件
迁移登录、重置密码、绑定手机号、邀请码和账号安全表单到 PlatformTextField 与 PlatformFieldLabel 补充认证表单公共输入断言和绑定手机号独立测试 更新 PlatformUiKit 文档和 Hermes 决策记录
This commit is contained in:
@@ -259,8 +259,14 @@ test('account actions open in independent panels instead of inline expansion', a
|
||||
const changePhoneDialog = screen.getByRole('dialog', {
|
||||
name: '绑定新手机号',
|
||||
});
|
||||
expect(within(changePhoneDialog).getByLabelText('新手机号')).toBeTruthy();
|
||||
expect(within(changePhoneDialog).getByLabelText('验证码')).toBeTruthy();
|
||||
const phoneInput = within(changePhoneDialog).getByLabelText(
|
||||
'新手机号',
|
||||
) as HTMLInputElement;
|
||||
const codeInput = within(changePhoneDialog).getByLabelText(
|
||||
'验证码',
|
||||
) as HTMLInputElement;
|
||||
expect(phoneInput.className).toContain('platform-text-field');
|
||||
expect(codeInput.className).toContain('platform-text-field');
|
||||
});
|
||||
|
||||
test('nested settings panels keep back navigation without an extra close action', async () => {
|
||||
|
||||
@@ -16,9 +16,11 @@ import type {
|
||||
AuthUser,
|
||||
} from '../../services/authService';
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
||||
import { PlatformPillBadge } from '../common/PlatformPillBadge';
|
||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import { PlatformSubpanel } from '../common/PlatformSubpanel';
|
||||
import { PlatformTextField } from '../common/PlatformTextField';
|
||||
import type { PlatformSettingsSection } from './AuthUiContext';
|
||||
import { CaptchaChallengeField } from './CaptchaChallengeField';
|
||||
|
||||
@@ -1010,10 +1012,12 @@ export function AccountModal({
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="grid gap-3">
|
||||
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||
<span>新手机号</span>
|
||||
<input
|
||||
className="platform-input h-11"
|
||||
<label className="grid gap-2">
|
||||
<PlatformFieldLabel variant="form" className="mb-0">
|
||||
新手机号
|
||||
</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
className="h-11"
|
||||
value={phone}
|
||||
inputMode="numeric"
|
||||
placeholder="13800000000"
|
||||
@@ -1021,11 +1025,13 @@ export function AccountModal({
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||
<span>验证码</span>
|
||||
<label className="grid gap-2">
|
||||
<PlatformFieldLabel variant="form" className="mb-0">
|
||||
验证码
|
||||
</PlatformFieldLabel>
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
className="platform-input h-11 min-w-0 flex-1"
|
||||
<PlatformTextField
|
||||
className="h-11 min-w-0 flex-1"
|
||||
value={code}
|
||||
inputMode="numeric"
|
||||
placeholder="输入验证码"
|
||||
@@ -1135,10 +1141,12 @@ export function AccountModal({
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="grid gap-3">
|
||||
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||
<span>当前密码</span>
|
||||
<input
|
||||
className="platform-input h-11"
|
||||
<label className="grid gap-2">
|
||||
<PlatformFieldLabel variant="form" className="mb-0">
|
||||
当前密码
|
||||
</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
className="h-11"
|
||||
value={currentPassword}
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
@@ -1148,10 +1156,12 @@ export function AccountModal({
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||
<span>新密码</span>
|
||||
<input
|
||||
className="platform-input h-11"
|
||||
<label className="grid gap-2">
|
||||
<PlatformFieldLabel variant="form" className="mb-0">
|
||||
新密码
|
||||
</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
className="h-11"
|
||||
value={newPassword}
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
|
||||
@@ -34,12 +34,20 @@ const authMocks = vi.hoisted(() => ({
|
||||
consumeAuthCallbackResult: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/apiClient', () => ({
|
||||
AUTH_STATE_EVENT: 'genarrative-auth-state-changed',
|
||||
ensureStoredAccessToken: authMocks.ensureStoredAccessToken,
|
||||
getStoredAccessToken: authMocks.getStoredAccessToken,
|
||||
refreshStoredAccessToken: authMocks.refreshStoredAccessToken,
|
||||
}));
|
||||
vi.mock('../../services/apiClient', async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import('../../services/apiClient')>(
|
||||
'../../services/apiClient',
|
||||
);
|
||||
|
||||
return {
|
||||
...actual,
|
||||
AUTH_STATE_EVENT: 'genarrative-auth-state-changed',
|
||||
ensureStoredAccessToken: authMocks.ensureStoredAccessToken,
|
||||
getStoredAccessToken: authMocks.getStoredAccessToken,
|
||||
refreshStoredAccessToken: authMocks.refreshStoredAccessToken,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../services/authService', () => ({
|
||||
authEntry: authMocks.authEntry,
|
||||
@@ -408,8 +416,17 @@ test('auth gate opens a login modal for protected actions and resumes after logi
|
||||
expect(dialog).toBeTruthy();
|
||||
expect(screen.queryByText('先登录账号,再同步你的冒险进度。')).toBeNull();
|
||||
|
||||
await user.type(within(dialog).getByLabelText('手机号'), '13800000000');
|
||||
await user.type(within(dialog).getByLabelText('验证码'), '123456');
|
||||
const phoneInput = within(dialog).getByLabelText(
|
||||
'手机号',
|
||||
) as HTMLInputElement;
|
||||
const codeInput = within(dialog).getByLabelText(
|
||||
'验证码',
|
||||
) as HTMLInputElement;
|
||||
expect(phoneInput.className).toContain('platform-text-field');
|
||||
expect(codeInput.className).toContain('platform-text-field');
|
||||
|
||||
await user.type(phoneInput, '13800000000');
|
||||
await user.type(codeInput, '123456');
|
||||
await acceptLegalConsent(user, dialog);
|
||||
await user.click(within(dialog).getByRole('button', { name: '登录' }));
|
||||
|
||||
@@ -592,9 +609,11 @@ test('auth gate hides register entry and opens invite modal for new sms account'
|
||||
const inviteDialog = await screen.findByRole('dialog', {
|
||||
name: '请填写邀请码',
|
||||
});
|
||||
expect(
|
||||
(within(inviteDialog).getByLabelText('邀请码') as HTMLInputElement).value,
|
||||
).toBe('SPRING2026');
|
||||
const inviteCodeInput = within(inviteDialog).getByLabelText(
|
||||
'邀请码',
|
||||
) as HTMLInputElement;
|
||||
expect(inviteCodeInput.value).toBe('SPRING2026');
|
||||
expect(inviteCodeInput.className).toContain('platform-text-field');
|
||||
expect(
|
||||
within(inviteDialog).getByRole('button', { name: '提交' }),
|
||||
).toBeTruthy();
|
||||
@@ -792,7 +811,11 @@ test('login modal resets draft state every time it is reopened', async () => {
|
||||
).toBeTruthy();
|
||||
await user.type(within(firstDialog).getByLabelText('验证码'), '123456');
|
||||
await user.click(within(firstDialog).getByRole('tab', { name: '密码登录' }));
|
||||
await user.type(within(firstDialog).getByLabelText('密码'), 'passw0rd');
|
||||
const passwordInput = within(firstDialog).getByLabelText(
|
||||
'密码',
|
||||
) as HTMLInputElement;
|
||||
expect(passwordInput.className).toContain('platform-text-field');
|
||||
await user.type(passwordInput, 'passw0rd');
|
||||
await user.click(
|
||||
within(firstDialog).getByRole('button', { name: '忘记密码' }),
|
||||
);
|
||||
|
||||
56
src/components/auth/BindPhoneScreen.test.tsx
Normal file
56
src/components/auth/BindPhoneScreen.test.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import type { AuthUser } from '../../services/authService';
|
||||
import { BindPhoneScreen } from './BindPhoneScreen';
|
||||
|
||||
const baseUser: AuthUser = {
|
||||
id: 'user-1',
|
||||
displayName: '微信旅人',
|
||||
avatarUrl: null,
|
||||
publicUserCode: 'user-bind-phone',
|
||||
phoneNumberMasked: null,
|
||||
loginMethod: 'wechat',
|
||||
bindingStatus: 'pending_bind_phone',
|
||||
wechatBound: true,
|
||||
};
|
||||
|
||||
test('绑定手机号表单复用平台输入和字段标题', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSubmit = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
render(
|
||||
<BindPhoneScreen
|
||||
user={baseUser}
|
||||
platformTheme="light"
|
||||
sendingCode={false}
|
||||
binding={false}
|
||||
error=""
|
||||
captchaChallenge={null}
|
||||
onSendCode={vi.fn().mockResolvedValue({
|
||||
cooldownSeconds: 60,
|
||||
expiresInSeconds: 300,
|
||||
})}
|
||||
onSubmit={onSubmit}
|
||||
onLogout={vi.fn().mockResolvedValue(undefined)}
|
||||
/>,
|
||||
);
|
||||
|
||||
const phoneInput = screen.getByLabelText('手机号') as HTMLInputElement;
|
||||
const codeInput = screen.getByLabelText('验证码') as HTMLInputElement;
|
||||
|
||||
expect(phoneInput.className).toContain('platform-text-field');
|
||||
expect(codeInput.className).toContain('platform-text-field');
|
||||
expect(screen.getByText('手机号').className).toContain(
|
||||
'text-[var(--platform-text-strong)]',
|
||||
);
|
||||
|
||||
await user.type(phoneInput, '13800000000');
|
||||
await user.type(codeInput, '123456');
|
||||
await user.click(screen.getByRole('button', { name: '绑定手机号并进入游戏' }));
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith('13800000000', '123456');
|
||||
});
|
||||
@@ -3,7 +3,9 @@ import { useEffect, useState } from 'react';
|
||||
import type { PlatformTheme } from '../../../packages/shared/src/contracts/runtime';
|
||||
import type { AuthCaptchaChallenge, AuthUser } from '../../services/authService';
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import { PlatformTextField } from '../common/PlatformTextField';
|
||||
import { CaptchaChallengeField } from './CaptchaChallengeField';
|
||||
|
||||
type BindPhoneScreenProps = {
|
||||
@@ -88,10 +90,11 @@ export function BindPhoneScreen({
|
||||
void onSubmit(phone, code);
|
||||
}}
|
||||
>
|
||||
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||
<span>手机号</span>
|
||||
<input
|
||||
className="platform-input"
|
||||
<label className="grid gap-2">
|
||||
<PlatformFieldLabel variant="form" className="mb-0">
|
||||
手机号
|
||||
</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
autoComplete="tel"
|
||||
inputMode="numeric"
|
||||
value={phone}
|
||||
@@ -100,11 +103,13 @@ export function BindPhoneScreen({
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||
<span>验证码</span>
|
||||
<label className="grid gap-2">
|
||||
<PlatformFieldLabel variant="form" className="mb-0">
|
||||
验证码
|
||||
</PlatformFieldLabel>
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
className="platform-input min-w-0 flex-1"
|
||||
<PlatformTextField
|
||||
className="min-w-0 flex-1"
|
||||
inputMode="numeric"
|
||||
value={code}
|
||||
onChange={(event) => setCode(event.target.value)}
|
||||
|
||||
@@ -16,10 +16,12 @@ import {
|
||||
readStoredLegalConsent,
|
||||
} from '../common/legalDocuments';
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
||||
import { PlatformModalCloseButton } from '../common/PlatformModalCloseButton';
|
||||
import { PlatformSegmentedTabs } from '../common/PlatformSegmentedTabs';
|
||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import { PlatformSubpanel } from '../common/PlatformSubpanel';
|
||||
import { PlatformTextField } from '../common/PlatformTextField';
|
||||
import { CaptchaChallengeField } from './CaptchaChallengeField';
|
||||
|
||||
type SmsScene = 'login' | 'reset_password';
|
||||
@@ -273,10 +275,11 @@ export function LoginScreen({
|
||||
void onPasswordSubmit(phone, password);
|
||||
}}
|
||||
>
|
||||
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||
<span>手机号</span>
|
||||
<input
|
||||
className="platform-input"
|
||||
<label className="grid gap-2">
|
||||
<PlatformFieldLabel variant="form" className="mb-0">
|
||||
手机号
|
||||
</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
autoComplete="tel"
|
||||
inputMode="numeric"
|
||||
value={phone}
|
||||
@@ -284,10 +287,11 @@ export function LoginScreen({
|
||||
placeholder="13800000000"
|
||||
/>
|
||||
</label>
|
||||
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||
<span>密码</span>
|
||||
<input
|
||||
className="platform-input"
|
||||
<label className="grid gap-2">
|
||||
<PlatformFieldLabel variant="form" className="mb-0">
|
||||
密码
|
||||
</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
autoComplete="current-password"
|
||||
type="password"
|
||||
value={password}
|
||||
@@ -516,10 +520,11 @@ function PhoneCodeForm({
|
||||
}}
|
||||
>
|
||||
{showPhoneField ? (
|
||||
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||
<span>手机号</span>
|
||||
<input
|
||||
className="platform-input"
|
||||
<label className="grid gap-2">
|
||||
<PlatformFieldLabel variant="form" className="mb-0">
|
||||
手机号
|
||||
</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
autoComplete="tel"
|
||||
inputMode="numeric"
|
||||
value={phone}
|
||||
@@ -529,11 +534,13 @@ function PhoneCodeForm({
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||
<span>验证码</span>
|
||||
<label className="grid gap-2">
|
||||
<PlatformFieldLabel variant="form" className="mb-0">
|
||||
验证码
|
||||
</PlatformFieldLabel>
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
className="platform-input min-w-0 flex-1"
|
||||
<PlatformTextField
|
||||
className="min-w-0 flex-1"
|
||||
inputMode="numeric"
|
||||
value={code}
|
||||
onChange={(event) => onCodeChange(event.target.value)}
|
||||
@@ -609,10 +616,11 @@ function PasswordResetPanel({
|
||||
void onSubmit();
|
||||
}}
|
||||
>
|
||||
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||
<span>手机号</span>
|
||||
<input
|
||||
className="platform-input"
|
||||
<label className="grid gap-2">
|
||||
<PlatformFieldLabel variant="form" className="mb-0">
|
||||
手机号
|
||||
</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
autoComplete="tel"
|
||||
inputMode="numeric"
|
||||
value={phone}
|
||||
@@ -620,11 +628,13 @@ function PasswordResetPanel({
|
||||
placeholder="13800000000"
|
||||
/>
|
||||
</label>
|
||||
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||
<span>验证码</span>
|
||||
<label className="grid gap-2">
|
||||
<PlatformFieldLabel variant="form" className="mb-0">
|
||||
验证码
|
||||
</PlatformFieldLabel>
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
className="platform-input min-w-0 flex-1"
|
||||
<PlatformTextField
|
||||
className="min-w-0 flex-1"
|
||||
inputMode="numeric"
|
||||
value={code}
|
||||
onChange={(event) => onCodeChange(event.target.value)}
|
||||
@@ -645,10 +655,11 @@ function PasswordResetPanel({
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
</label>
|
||||
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||
<span>新密码</span>
|
||||
<input
|
||||
className="platform-input"
|
||||
<label className="grid gap-2">
|
||||
<PlatformFieldLabel variant="form" className="mb-0">
|
||||
新密码
|
||||
</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
autoComplete="new-password"
|
||||
type="password"
|
||||
value={password}
|
||||
|
||||
@@ -2,8 +2,10 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import type { PlatformTheme } from '../../../packages/shared/src/contracts/runtime';
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
||||
import { PlatformModalCloseButton } from '../common/PlatformModalCloseButton';
|
||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import { PlatformTextField } from '../common/PlatformTextField';
|
||||
|
||||
type RegistrationInviteModalProps = {
|
||||
isOpen: boolean;
|
||||
@@ -84,10 +86,11 @@ export function RegistrationInviteModal({
|
||||
void onSubmit(normalizedInviteCode);
|
||||
}}
|
||||
>
|
||||
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||
<span>邀请码</span>
|
||||
<input
|
||||
className="platform-input"
|
||||
<label className="grid gap-2">
|
||||
<PlatformFieldLabel variant="form" className="mb-0">
|
||||
邀请码
|
||||
</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
autoComplete="off"
|
||||
value={inviteCode}
|
||||
onChange={(event) => setInviteCode(event.target.value)}
|
||||
|
||||
Reference in New Issue
Block a user