完善移动端登录密码必填校验
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// 功能描述:提供用户端手机号密码登录界面及本地输入校验。
|
||||
// 版本:1.1.0
|
||||
// 版本:1.2.0
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
@@ -30,9 +30,12 @@ class _LoginPageState extends State<LoginPage> {
|
||||
|
||||
final _phone = TextEditingController();
|
||||
final _password = TextEditingController();
|
||||
final _phoneFocus = FocusNode();
|
||||
final _passwordFocus = FocusNode();
|
||||
bool _submitting = false;
|
||||
String? _error;
|
||||
String? _phoneError;
|
||||
String? _passwordError;
|
||||
bool _obscurePassword = true;
|
||||
|
||||
@override
|
||||
@@ -47,23 +50,31 @@ class _LoginPageState extends State<LoginPage> {
|
||||
void dispose() {
|
||||
_phone.dispose();
|
||||
_password.dispose();
|
||||
_phoneFocus.dispose();
|
||||
_passwordFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 校验手机号并提交登录请求。
|
||||
/// 校验手机号和密码,通过后再提交登录请求。
|
||||
Future<void> _login() async {
|
||||
final phone = _phone.text.trim();
|
||||
if (!_phonePattern.hasMatch(phone)) {
|
||||
final phoneValid = _phonePattern.hasMatch(phone);
|
||||
final passwordValid = _password.text.isNotEmpty;
|
||||
if (!phoneValid || !passwordValid) {
|
||||
setState(() {
|
||||
_phoneError = '请输入正确的11位手机号';
|
||||
_phoneError = phoneValid ? null : '请输入正确的11位手机号';
|
||||
_passwordError = passwordValid ? null : '请输入密码';
|
||||
_error = null;
|
||||
});
|
||||
// 聚焦第一个无效字段,便于键盘和辅助技术用户立即修正。
|
||||
(phoneValid ? _passwordFocus : _phoneFocus).requestFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_submitting = true;
|
||||
_phoneError = null;
|
||||
_passwordError = null;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
@@ -123,10 +134,16 @@ class _LoginPageState extends State<LoginPage> {
|
||||
const SizedBox(height: 40),
|
||||
TextField(
|
||||
controller: _phone,
|
||||
focusNode: _phoneFocus,
|
||||
keyboardType: TextInputType.phone,
|
||||
autofillHints: const [AutofillHints.telephoneNumber],
|
||||
onChanged: (_) {
|
||||
if (_phoneError != null) setState(() => _phoneError = null);
|
||||
if (_phoneError != null || _error != null) {
|
||||
setState(() {
|
||||
_phoneError = null;
|
||||
_error = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: '手机号',
|
||||
@@ -137,12 +154,22 @@ class _LoginPageState extends State<LoginPage> {
|
||||
const SizedBox(height: 14),
|
||||
TextField(
|
||||
controller: _password,
|
||||
focusNode: _passwordFocus,
|
||||
obscureText: _obscurePassword,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
onChanged: (_) {
|
||||
if (_passwordError != null || _error != null) {
|
||||
setState(() {
|
||||
_passwordError = null;
|
||||
_error = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
onSubmitted: (_) => _submitting ? null : _login(),
|
||||
decoration: InputDecoration(
|
||||
labelText: '密码',
|
||||
prefixIcon: const Icon(Icons.lock_outline),
|
||||
errorText: _passwordError,
|
||||
suffixIcon: IconButton(
|
||||
tooltip: _obscurePassword ? '显示密码' : '隐藏密码',
|
||||
onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// 功能描述:验证用户端登录页字段、手机号校验和接口错误中文化。
|
||||
// 版本:1.1.0
|
||||
// 功能描述:验证用户端登录页字段、必填校验、提交行为和接口错误中文化。
|
||||
// 版本:1.2.0
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/app/dependencies.dart';
|
||||
@@ -7,6 +7,27 @@ import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/data/services/secure_session_store.dart';
|
||||
import 'package:user_app/ui/features/auth/login_page.dart';
|
||||
|
||||
/// 记录登录调用,验证本地校验是否正确阻止请求。
|
||||
class _RecordingUserSession extends UserSession {
|
||||
_RecordingUserSession() : super(SecureSessionStore());
|
||||
|
||||
int loginCallCount = 0;
|
||||
String? lastPhone;
|
||||
String? lastPassword;
|
||||
|
||||
@override
|
||||
Future<void> login({
|
||||
required String phone,
|
||||
required String password,
|
||||
String? verificationCode,
|
||||
String? requestIdentity,
|
||||
}) async {
|
||||
loginCallCount += 1;
|
||||
lastPhone = phone;
|
||||
lastPassword = password;
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证用户端登录界面的关键交互。
|
||||
void main() {
|
||||
testWidgets('login page exposes phone and password fields', (tester) async {
|
||||
@@ -18,8 +39,8 @@ void main() {
|
||||
expect(find.text('安全登录'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('手机号格式错误显示输入框中文提示', (tester) async {
|
||||
final session = UserSession(SecureSessionStore());
|
||||
testWidgets('手机号错误且密码为空时同时提示并阻止登录调用', (tester) async {
|
||||
final session = _RecordingUserSession();
|
||||
await tester.pumpWidget(MaterialApp(home: LoginPage(session: session)));
|
||||
|
||||
await tester.enterText(find.byType(TextField).first, '+8613800138000');
|
||||
@@ -27,7 +48,48 @@ void main() {
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('请输入正确的11位手机号'), findsOneWidget);
|
||||
expect(find.text('请输入密码'), findsOneWidget);
|
||||
expect(find.text('Invalid Argument'), findsNothing);
|
||||
expect(session.loginCallCount, 0);
|
||||
expect(tester.widget<TextField>(find.byType(TextField).first).focusNode?.hasFocus, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('合法手机号配空密码时提示密码必填并阻止登录调用', (tester) async {
|
||||
final session = _RecordingUserSession();
|
||||
await tester.pumpWidget(MaterialApp(home: LoginPage(session: session)));
|
||||
|
||||
await tester.enterText(find.byType(TextField).first, '13800138000');
|
||||
await tester.tap(find.text('安全登录'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('请输入正确的11位手机号'), findsNothing);
|
||||
expect(find.text('请输入密码'), findsOneWidget);
|
||||
expect(session.loginCallCount, 0);
|
||||
expect(tester.widget<TextField>(find.byType(TextField).at(1)).focusNode?.hasFocus, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('修改字段清除对应错误并由回车原样提交密码', (tester) async {
|
||||
final session = _RecordingUserSession();
|
||||
await tester.pumpWidget(MaterialApp(home: LoginPage(session: session)));
|
||||
|
||||
await tester.tap(find.text('安全登录'));
|
||||
await tester.pump();
|
||||
await tester.enterText(find.byType(TextField).first, '13800138000');
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('请输入正确的11位手机号'), findsNothing);
|
||||
expect(find.text('请输入密码'), findsOneWidget);
|
||||
|
||||
await tester.enterText(find.byType(TextField).at(1), ' 密码123 ');
|
||||
await tester.pump();
|
||||
expect(find.text('请输入密码'), findsNothing);
|
||||
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await tester.pump();
|
||||
|
||||
expect(session.loginCallCount, 1);
|
||||
expect(session.lastPhone, '13800138000');
|
||||
expect(session.lastPassword, ' 密码123 ');
|
||||
});
|
||||
|
||||
test('接口错误优先按错误码中文化并屏蔽未知英文', () {
|
||||
|
||||
Reference in New Issue
Block a user