From 8a96f341c8fadb708c4c97abb5a9bd1f5d403e98 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Mon, 19 Oct 2026 10:33:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E4=BC=98=E5=8C=96=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E9=A1=B5=E5=B8=83=E5=B1=80=E4=B8=8E=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E5=8F=8D=E9=A6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/l10n/app_en.arb | 1 + lib/l10n/app_id.arb | 1 + lib/l10n/app_localizations.dart | 6 + lib/l10n/app_localizations_en.dart | 4 + lib/l10n/app_localizations_id.dart | 4 + lib/l10n/app_localizations_zh.dart | 3 + lib/l10n/app_zh.arb | 1 + lib/pages/auth/auth_form_widgets.dart | 165 +++++++++++++++--------- lib/pages/auth/auth_logic.dart | 25 +++- lib/pages/auth/auth_view.dart | 175 +++++++++++++++++--------- lib/pages/auth/auth_widgets.dart | 4 +- test/auth/auth_k21_test.dart | 85 +++++++++++++ 12 files changed, 355 insertions(+), 119 deletions(-) create mode 100644 test/auth/auth_k21_test.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3dbf4e9..9a6c32b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -279,6 +279,7 @@ "authSignInTitle": "Welcome back", "authSignUpTitle": "Create your account", "authSubtitle": "Sign in with your email and password.", + "authSignUpSubtitle": "Create an account to keep your list, history, and progress.", "authBrandTagline": "Short dramas made for Indonesia", "authEmailLabel": "Email", "authPasswordLabel": "Password", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 3dd9f07..5e04b7c 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -279,6 +279,7 @@ "authSignInTitle": "Selamat datang kembali", "authSignUpTitle": "Buat akun kamu", "authSubtitle": "Masuk dengan email dan kata sandi.", + "authSignUpSubtitle": "Buat akun untuk menyimpan daftar, riwayat, dan progresmu.", "authBrandTagline": "Drama pendek untuk Indonesia", "authEmailLabel": "Email", "authPasswordLabel": "Kata sandi", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 00faecc..70ae96e 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1330,6 +1330,12 @@ abstract class AppLocalizations { /// **'Sign in with your email and password.'** String get authSubtitle; + /// No description provided for @authSignUpSubtitle. + /// + /// In en, this message translates to: + /// **'Create an account to keep your list, history, and progress.'** + String get authSignUpSubtitle; + /// No description provided for @authBrandTagline. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 99f84c8..48921e6 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -652,6 +652,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get authSubtitle => 'Sign in with your email and password.'; + @override + String get authSignUpSubtitle => + 'Create an account to keep your list, history, and progress.'; + @override String get authBrandTagline => 'Short dramas made for Indonesia'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 1bef0e3..5bb2fd7 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -655,6 +655,10 @@ class AppLocalizationsId extends AppLocalizations { @override String get authSubtitle => 'Masuk dengan email dan kata sandi.'; + @override + String get authSignUpSubtitle => + 'Buat akun untuk menyimpan daftar, riwayat, dan progresmu.'; + @override String get authBrandTagline => 'Drama pendek untuk Indonesia'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 2172b39..f3bd4f1 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -643,6 +643,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get authSubtitle => '用邮箱和密码登录。'; + @override + String get authSignUpSubtitle => '创建账号,保存你的片单、观看记录和进度。'; + @override String get authBrandTagline => '为印尼市场打造的短剧体验'; diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 052f09c..bd97d34 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -241,6 +241,7 @@ "authSignInTitle": "欢迎回来", "authSignUpTitle": "创建账号", "authSubtitle": "用邮箱和密码登录。", + "authSignUpSubtitle": "创建账号,保存你的片单、观看记录和进度。", "authBrandTagline": "为印尼市场打造的短剧体验", "authEmailLabel": "邮箱", "authPasswordLabel": "密码", diff --git a/lib/pages/auth/auth_form_widgets.dart b/lib/pages/auth/auth_form_widgets.dart index 90cc3c7..576da35 100644 --- a/lib/pages/auth/auth_form_widgets.dart +++ b/lib/pages/auth/auth_form_widgets.dart @@ -10,21 +10,40 @@ class _AuthFields extends StatelessWidget { @override Widget build(BuildContext context) { + final error = state.status.fieldError; + final emailError = + error == AuthFieldError.emailRequired || + error == AuthFieldError.emailInvalid; + final passwordError = + error == AuthFieldError.passwordRequired || + error == AuthFieldError.passwordLength || + error == AuthFieldError.passwordWeak; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _fieldLabel(state.l10n.authEmailLabel), const SizedBox(height: 8), - _emailField(), + _emailField(context, hasError: emailError), const SizedBox(height: 16), _fieldLabel(state.l10n.authPasswordLabel), const SizedBox(height: 8), - _passwordField(), + _passwordField(context, hasError: passwordError), + if (state.status.signUpMode) ...[ + const SizedBox(height: 8), + Text( + state.l10n.authPasswordHint, + key: const Key('auth-password-rule'), + style: const TextStyle( + color: AppColors.muted, + fontSize: AppDimens.caption, + ), + ), + ], ], ); } - Widget _emailField() { + Widget _emailField(BuildContext context, {required bool hasError}) { final enabled = !state.status.busy; return ListenableBuilder( listenable: state.fields.emailFocus, @@ -43,16 +62,18 @@ class _AuthFields extends StatelessWidget { onSubmitted: (_) => state.fields.passwordFocus.requestFocus(), style: const TextStyle(color: AppColors.onPrimary, fontSize: 15), decoration: _decoration( + context: context, hint: 'name@example.com', icon: Icons.mail_outline_rounded, focused: state.fields.emailFocus.hasFocus, + hasError: hasError, ), ), ), ); } - Widget _passwordField() { + Widget _passwordField(BuildContext context, {required bool hasError}) { final enabled = !state.status.busy; final l10n = state.l10n; return ListenableBuilder( @@ -72,9 +93,11 @@ class _AuthFields extends StatelessWidget { onSubmitted: (_) => callbacks.onSubmit(), style: const TextStyle(color: AppColors.onPrimary, fontSize: 15), decoration: _decoration( + context: context, hint: l10n.authPasswordHint, icon: Icons.lock_outline_rounded, focused: state.fields.passwordFocus.hasFocus, + hasError: hasError, suffix: IconButton( key: const Key('auth-password-visibility'), constraints: const BoxConstraints.tightFor( @@ -108,15 +131,22 @@ class _AuthFields extends StatelessWidget { ); InputDecoration _decoration({ + required BuildContext context, required String hint, required IconData icon, required bool focused, + required bool hasError, Widget? suffix, }) { final border = OutlineInputBorder( borderRadius: BorderRadius.circular(_fieldRadius), borderSide: BorderSide.none, ); + final errorColor = Theme.of(context).colorScheme.error; + final errorBorder = OutlineInputBorder( + borderRadius: BorderRadius.circular(_fieldRadius), + borderSide: BorderSide(color: errorColor, width: _fieldBorderWidth), + ); return InputDecoration( hintText: hint, hintStyle: const TextStyle(color: AppColors.muted, fontSize: 14), @@ -144,6 +174,8 @@ class _AuthFields extends StatelessWidget { width: _fieldBorderWidth, ), ), + errorBorder: errorBorder, + focusedErrorBorder: errorBorder, ); } } @@ -175,7 +207,10 @@ class _AuthSubmitButton extends StatelessWidget { return AppColors.blue; }), foregroundColor: const WidgetStatePropertyAll(AppColors.onPrimary), - elevation: const WidgetStatePropertyAll(0), + elevation: const WidgetStatePropertyAll(2), + shadowColor: WidgetStatePropertyAll( + AppColors.blue.withValues(alpha: 0.22), + ), padding: const WidgetStatePropertyAll(EdgeInsets.zero), shape: WidgetStatePropertyAll( RoundedRectangleBorder( @@ -267,65 +302,83 @@ class _ModeSegment extends StatelessWidget { } class _AuthBrand extends StatelessWidget { - const _AuthBrand({required this.tagline}); + const _AuthBrand({required this.tagline, required this.compact}); final String tagline; + final bool compact; @override Widget build(BuildContext context) { - return Row( + final copy = _brandCopy(context, centered: !compact); + if (compact) { + return Row( + key: const Key('auth-brand'), + children: [ + _brandMark(), + const SizedBox(width: 14), + Expanded(child: copy), + ], + ); + } + return Column( key: const Key('auth-brand'), - children: [ - Container( - key: const Key('auth-brand-mark'), - width: _brandMarkSize, - height: _brandMarkSize, - decoration: BoxDecoration( - color: AppColors.blue, - borderRadius: BorderRadius.circular(14), - boxShadow: [ - BoxShadow( - color: AppColors.blue.withValues(alpha: 0.32), - blurRadius: 24, - offset: const Offset(0, 10), - ), - ], - ), - child: const Icon( - Icons.movie_filter_rounded, - color: AppColors.onPrimary, - size: 26, - ), - ), - const SizedBox(width: 14), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - AppLocalizations.of(context)!.appName, - style: const TextStyle( - color: AppColors.onPrimary, - fontSize: 23, - fontWeight: FontWeight.w800, - height: 1.1, - ), - ), - const SizedBox(height: 4), - Text( - tagline, - style: const TextStyle( - color: AppColors.blueSoft, - fontSize: 13, - height: 1.35, - ), - ), - ], - ), - ), - ], + mainAxisSize: MainAxisSize.min, + children: [_brandMark(), const SizedBox(height: 14), copy], ); } + + Widget _brandMark() => Container( + key: const Key('auth-brand-mark'), + width: _brandMarkSize, + height: _brandMarkSize, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + boxShadow: [ + BoxShadow( + color: AppColors.blue.withValues(alpha: 0.32), + blurRadius: 24, + offset: const Offset(0, 10), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(14), + child: Image.asset( + 'assets/icon/splash_mark.png', + width: _brandMarkSize, + height: _brandMarkSize, + fit: BoxFit.cover, + ), + ), + ); + + Widget _brandCopy(BuildContext context, {required bool centered}) => Column( + crossAxisAlignment: centered + ? CrossAxisAlignment.center + : CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.appName, + textAlign: centered ? TextAlign.center : TextAlign.start, + style: const TextStyle( + color: AppColors.onPrimary, + fontSize: 26, + fontWeight: FontWeight.w800, + height: 1.1, + ), + ), + const SizedBox(height: 6), + Text( + tagline, + textAlign: centered ? TextAlign.center : TextAlign.start, + style: const TextStyle( + color: AppColors.blueSoft, + fontSize: 14, + height: 1.35, + ), + ), + ], + ); } class _AuthError extends StatelessWidget { diff --git a/lib/pages/auth/auth_logic.dart b/lib/pages/auth/auth_logic.dart index 8b9eb0e..91dd5db 100644 --- a/lib/pages/auth/auth_logic.dart +++ b/lib/pages/auth/auth_logic.dart @@ -8,29 +8,44 @@ class AuthLogic { String password = ''; bool busy = false; String? errorMessage; + AuthFieldError? fieldError; AuthLogic({this.signUpMode = false, this.errorMessage}); void toggleMode() { signUpMode = !signUpMode; errorMessage = null; + fieldError = null; } /// 返回 null 表示校验通过,否则返回错误文案(由视图用 l10n 映射) AuthFieldError? validate() { final mail = email.trim(); - if (mail.isEmpty) return AuthFieldError.emailRequired; - if (!_emailPattern.hasMatch(mail)) return AuthFieldError.emailInvalid; - if (password.isEmpty) return AuthFieldError.passwordRequired; + if (mail.isEmpty) return _record(AuthFieldError.emailRequired); + if (!_emailPattern.hasMatch(mail)) { + return _record(AuthFieldError.emailInvalid); + } + if (password.isEmpty) return _record(AuthFieldError.passwordRequired); if (password.length < 8 || password.length > 64) { - return AuthFieldError.passwordLength; + return _record(AuthFieldError.passwordLength); } if (!_hasLetter.hasMatch(password) || !_hasDigit.hasMatch(password)) { - return AuthFieldError.passwordWeak; + return _record(AuthFieldError.passwordWeak); } + fieldError = null; return null; } + /// 清除当前字段错误;输入变化时由页面调用。 + void clearFieldError() { + fieldError = null; + } + + AuthFieldError _record(AuthFieldError error) { + fieldError = error; + return error; + } + String get normalizedEmail => email.trim().toLowerCase(); static final _emailPattern = RegExp( diff --git a/lib/pages/auth/auth_view.dart b/lib/pages/auth/auth_view.dart index b645226..555d495 100644 --- a/lib/pages/auth/auth_view.dart +++ b/lib/pages/auth/auth_view.dart @@ -33,6 +33,13 @@ const double _glowOpacity = 0.18; const double _errorOpacity = 0.10; const double _errorBorderOpacity = 0.28; const double _enterSlideDistance = 16; +const double _topContentPadding = 64; +const double _bottomContentPadding = 28; +const double _compactTopContentPadding = 48; +const double _compactHeightBreakpoint = 720; +const double _compactBrandGap = 16; +const double _warmGlowSize = 260; +const double _warmGlowOpacity = 0.06; const Duration _enterDuration = Duration(milliseconds: 220); const Duration _segmentDuration = Duration(milliseconds: 160); @@ -89,6 +96,7 @@ class _AuthPageState extends State { } Future _submit() async { + if (_logic.busy) return; final l10n = AppLocalizations.of(context)!; final invalid = _logic.validate(); if (invalid != null) { @@ -117,8 +125,10 @@ class _AuthPageState extends State { if (!mounted) return; Navigator.of(context).maybePop(); } on ApiException catch (error) { + if (!mounted) return; setState(() => _logic.errorMessage = error.message); } catch (_) { + if (!mounted) return; setState(() => _logic.errorMessage = l10n.authNetworkError); } finally { if (mounted) setState(() => _logic.busy = false); @@ -158,76 +168,120 @@ class _AuthPageState extends State { ), ), ), - SafeArea( - child: AutofillGroup( - child: ListView( - key: const Key('auth-scroll'), - keyboardDismissBehavior: - ScrollViewKeyboardDismissBehavior.onDrag, - padding: EdgeInsets.fromLTRB( - AppDimens.pageHorizontal, - 12, - AppDimens.pageHorizontal, - 28 + MediaQuery.viewInsetsOf(context).bottom, + Positioned( + top: 72, + right: -80, + width: _warmGlowSize, + height: _warmGlowSize, + child: IgnorePointer( + child: Container( + key: const Key('auth-background-warm-glow'), + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + AppColors.gold.withValues(alpha: _warmGlowOpacity), + AppColors.gold.withValues(alpha: 0), + ], + ), ), - children: [ - Center( + ), + ), + ), + SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + final compact = + constraints.maxHeight < _compactHeightBreakpoint; + final top = compact + ? _compactTopContentPadding + : _topContentPadding; + final bottom = + _bottomContentPadding + + MediaQuery.viewInsetsOf(context).bottom; + final available = constraints.maxHeight - top - bottom; + final minHeight = available > 0 ? available : 0.0; + return AutofillGroup( + child: SingleChildScrollView( + key: const Key('auth-scroll'), + keyboardDismissBehavior: + ScrollViewKeyboardDismissBehavior.onDrag, + padding: EdgeInsets.fromLTRB( + AppDimens.pageHorizontal, + top, + AppDimens.pageHorizontal, + bottom, + ), child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: _contentMaxWidth, - ), - child: TweenAnimationBuilder( - key: const Key('auth-enter-animation'), - tween: Tween(begin: 0, end: 1), - duration: _enterDuration, - curve: Curves.easeOutCubic, - builder: (context, value, child) => Opacity( - opacity: value, - child: Transform.translate( - offset: Offset( - 0, - _enterSlideDistance * (1 - value), - ), - child: child, + constraints: BoxConstraints(minHeight: minHeight), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: _contentMaxWidth, ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (Navigator.of(context).canPop()) - Align( - alignment: Alignment.centerLeft, - child: IconButton( - key: const Key('auth-back'), - tooltip: MaterialLocalizations.of( - context, - ).backButtonTooltip, - onPressed: () => - Navigator.of(context).pop(), - icon: const Icon( - Icons.arrow_back_rounded, - color: AppColors.onPrimary, - ), - ), - ), - _AuthBrand(tagline: l10n.authBrandTagline), - const SizedBox(height: 28), - _authCard(l10n), - ], + child: _content(l10n, compact: compact), ), ), ), ), - ], - ), + ); + }, ), ), + if (Navigator.of(context).canPop()) + Positioned( + top: 0, + left: 0, + right: 0, + child: SafeArea( + child: Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.only(left: 4, top: 4), + child: IconButton( + key: const Key('auth-back'), + tooltip: MaterialLocalizations.of( + context, + ).backButtonTooltip, + onPressed: () => Navigator.of(context).pop(), + icon: const Icon( + Icons.arrow_back_rounded, + color: AppColors.onPrimary, + ), + ), + ), + ), + ), + ), ], ), ), ); } + Widget _content(AppLocalizations l10n, {required bool compact}) => + TweenAnimationBuilder( + key: const Key('auth-enter-animation'), + tween: Tween(begin: 0, end: 1), + duration: _enterDuration, + curve: Curves.easeOutCubic, + builder: (context, value, child) => Opacity( + opacity: value, + child: Transform.translate( + offset: Offset(0, _enterSlideDistance * (1 - value)), + child: child, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _AuthBrand(tagline: l10n.authBrandTagline, compact: compact), + SizedBox(height: compact ? _compactBrandGap : 28), + _authCard(l10n), + ], + ), + ); + Widget _authCard(AppLocalizations l10n) => _AuthCard( state: _AuthCardState( l10n: l10n, @@ -235,6 +289,7 @@ class _AuthPageState extends State { signUpMode: _logic.signUpMode, busy: _logic.busy, errorMessage: _logic.errorMessage, + fieldError: _logic.fieldError, obscurePassword: _obscurePassword, ), fields: _AuthFieldControllers( @@ -246,8 +301,14 @@ class _AuthPageState extends State { ), callbacks: _AuthCardCallbacks( onModeSelected: _selectMode, - onEmailChanged: (value) => _logic.email = value, - onPasswordChanged: (value) => _logic.password = value, + onEmailChanged: (value) => setState(() { + _logic.email = value; + _logic.clearFieldError(); + }), + onPasswordChanged: (value) => setState(() { + _logic.password = value; + _logic.clearFieldError(); + }), onTogglePasswordVisibility: () => setState(() => _obscurePassword = !_obscurePassword), onSubmit: _submit, diff --git a/lib/pages/auth/auth_widgets.dart b/lib/pages/auth/auth_widgets.dart index e4d7543..3934899 100644 --- a/lib/pages/auth/auth_widgets.dart +++ b/lib/pages/auth/auth_widgets.dart @@ -19,12 +19,14 @@ class _AuthFormStatus { required this.signUpMode, required this.busy, required this.errorMessage, + required this.fieldError, required this.obscurePassword, }); final bool signUpMode; final bool busy; final String? errorMessage; + final AuthFieldError? fieldError; final bool obscurePassword; } @@ -100,7 +102,7 @@ class _AuthCard extends StatelessWidget { ), const SizedBox(height: 6), Text( - l10n.authSubtitle, + status.signUpMode ? l10n.authSignUpSubtitle : l10n.authSubtitle, style: const TextStyle( color: AppColors.muted, fontSize: AppDimens.body, diff --git a/test/auth/auth_k21_test.dart b/test/auth/auth_k21_test.dart new file mode 100644 index 0000000..14fefef --- /dev/null +++ b/test/auth/auth_k21_test.dart @@ -0,0 +1,85 @@ +// Author: fruit.com.cn + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; + +import 'package:fruitshorts/core/data/remote/api_client.dart'; +import 'package:fruitshorts/core/data/remote/auth_api.dart'; +import 'package:fruitshorts/core/data/services/auth_controller.dart'; +import 'package:fruitshorts/core/data/services/auth_session_store.dart'; +import 'package:fruitshorts/core/platform/haptics.dart'; +import 'package:fruitshorts/l10n/app_localizations.dart'; +import 'package:fruitshorts/pages/auth/auth_view.dart'; + +import '../support/fake_haptics.dart'; +import '../support/fake_http_adapter.dart'; + +void main() { + setUp(() { + SharedPreferencesAsyncPlatform.instance = + InMemorySharedPreferencesAsync.empty(); + }); + + testWidgets('should_show_sign_up_subtitle_and_password_rule_when_switched', ( + tester, + ) async { + await _pumpAuth(tester); + final l10n = await AppLocalizations.delegate.load(const Locale('en')); + + expect(find.byKey(const Key('auth-password-rule')), findsNothing); + await tester.tap(find.byKey(const Key('auth-mode-sign-up'))); + await tester.pumpAndSettle(); + + expect(find.text(l10n.authSignUpSubtitle), findsOneWidget); + final rule = find.byKey(const Key('auth-password-rule')); + expect(rule, findsOneWidget); + expect(tester.widget(rule).data, l10n.authPasswordHint); + }); + + testWidgets('should_apply_semantic_error_border_when_email_is_invalid', ( + tester, + ) async { + await _pumpAuth(tester); + await tester.enterText(find.byKey(const Key('auth-email')), 'invalid'); + await tester.enterText(find.byKey(const Key('auth-password')), 'abcd1234'); + await tester.tap(find.byKey(const Key('auth-submit'))); + await tester.pumpAndSettle(); + + final emailFinder = find.byKey(const Key('auth-email')); + final email = tester.widget(emailFinder); + final errorBorder = email.decoration!.errorBorder! as OutlineInputBorder; + final scheme = Theme.of(tester.element(emailFinder)).colorScheme; + expect(errorBorder.borderSide.color, scheme.error); + expect(errorBorder.borderSide.width, 1.5); + }); +} + +Future _pumpAuth(WidgetTester tester) async { + final auth = AuthController( + AuthApi( + ApiClient( + baseUrl: 'http://127.0.0.1:1', + adapter: FakeHttpAdapter((_) async => FakeHttpResponse('{}', 500)), + ), + ), + AuthSessionStore(), + ); + await tester.pumpWidget( + MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: auth), + Provider.value(value: FakeHaptics()), + ], + child: MaterialApp( + locale: const Locale('en'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: const AuthPage(), + ), + ), + ); + await tester.pumpAndSettle(); +}