feat(flutter): 修复登录输入并升级登录页

This commit is contained in:
Zhou Weidong committed 2026-10-20 16:21:06 +08:00
1 parent f72cb41670
commit 81bb1543bb
5 files changed
+233 -141

No files matched your search

+29 -42
View File
@@ -22,11 +22,11 @@ class _AuthFields extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_fieldLabel(state.l10n.authEmailLabel), _fieldLabel(state.l10n.authEmailLabel),
const SizedBox(height: 8), const SizedBox(height: 6),
_emailField(context, hasError: emailError), _emailField(context, hasError: emailError),
const SizedBox(height: 16), const SizedBox(height: 14),
_fieldLabel(state.l10n.authPasswordLabel), _fieldLabel(state.l10n.authPasswordLabel),
const SizedBox(height: 8), const SizedBox(height: 6),
_passwordField(context, hasError: passwordError), _passwordField(context, hasError: passwordError),
if (state.status.signUpMode) ...[ if (state.status.signUpMode) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -60,7 +60,8 @@ class _AuthFields extends StatelessWidget {
autofillHints: const [AutofillHints.email], autofillHints: const [AutofillHints.email],
onChanged: callbacks.onEmailChanged, onChanged: callbacks.onEmailChanged,
onSubmitted: (_) => state.fields.passwordFocus.requestFocus(), onSubmitted: (_) => state.fields.passwordFocus.requestFocus(),
style: const TextStyle(color: AppColors.onPrimary, fontSize: 15), style: const TextStyle(color: AppColors.onSurface, fontSize: 15),
cursorColor: AppColors.blue,
decoration: _decoration( decoration: _decoration(
context: context, context: context,
hint: 'name@example.com', hint: 'name@example.com',
@@ -91,7 +92,8 @@ class _AuthFields extends StatelessWidget {
autofillHints: const [AutofillHints.password], autofillHints: const [AutofillHints.password],
onChanged: callbacks.onPasswordChanged, onChanged: callbacks.onPasswordChanged,
onSubmitted: (_) => callbacks.onSubmit(), onSubmitted: (_) => callbacks.onSubmit(),
style: const TextStyle(color: AppColors.onPrimary, fontSize: 15), style: const TextStyle(color: AppColors.onSurface, fontSize: 15),
cursorColor: AppColors.blue,
decoration: _decoration( decoration: _decoration(
context: context, context: context,
hint: l10n.authPasswordHint, hint: l10n.authPasswordHint,
@@ -125,8 +127,8 @@ class _AuthFields extends StatelessWidget {
label, label,
style: const TextStyle( style: const TextStyle(
color: AppColors.muted, color: AppColors.muted,
fontSize: 13, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w700,
), ),
); );
@@ -140,7 +142,10 @@ class _AuthFields extends StatelessWidget {
}) { }) {
final border = OutlineInputBorder( final border = OutlineInputBorder(
borderRadius: BorderRadius.circular(_fieldRadius), borderRadius: BorderRadius.circular(_fieldRadius),
borderSide: BorderSide.none, borderSide: const BorderSide(
color: AppColors.border,
width: _fieldBorderWidth,
),
); );
final errorColor = Theme.of(context).colorScheme.error; final errorColor = Theme.of(context).colorScheme.error;
final errorBorder = OutlineInputBorder( final errorBorder = OutlineInputBorder(
@@ -149,11 +154,11 @@ class _AuthFields extends StatelessWidget {
); );
return InputDecoration( return InputDecoration(
hintText: hint, hintText: hint,
hintStyle: const TextStyle(color: AppColors.muted, fontSize: 14), hintStyle: const TextStyle(color: AppColors.subtle, fontSize: 14),
prefixIcon: Icon( prefixIcon: Icon(
icon, icon,
size: 21, size: 21,
color: focused ? AppColors.blueSoft : AppColors.muted, color: focused ? AppColors.blue : AppColors.muted,
), ),
prefixIconConstraints: const BoxConstraints( prefixIconConstraints: const BoxConstraints(
minWidth: _minTapTarget, minWidth: _minTapTarget,
@@ -161,7 +166,7 @@ class _AuthFields extends StatelessWidget {
), ),
suffixIcon: suffix, suffixIcon: suffix,
filled: true, filled: true,
fillColor: AppColors.bar, fillColor: AppColors.card,
isDense: true, isDense: true,
contentPadding: const EdgeInsets.only(right: 16), contentPadding: const EdgeInsets.only(right: 16),
border: border, border: border,
@@ -229,8 +234,8 @@ class _AuthSubmitButton extends StatelessWidget {
: Text( : Text(
status.signUpMode ? l10n.signUp : l10n.signIn, status.signUpMode ? l10n.signUp : l10n.signIn,
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 15,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w800,
), ),
), ),
), ),
@@ -273,7 +278,7 @@ class _ModeSegment extends StatelessWidget {
child: Text( child: Text(
label, label,
style: TextStyle( style: TextStyle(
color: selected ? AppColors.onPrimary : AppColors.muted, color: selected ? AppColors.onPrimary : AppColors.onSurface,
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
@@ -327,29 +332,11 @@ class _AuthBrand extends StatelessWidget {
); );
} }
Widget _brandMark() => Container( Widget _brandMark() => _brandMarkWidget(
key: const Key('auth-brand-mark'), key: const Key('auth-brand-mark'),
width: _brandMarkSize, size: compact ? _compactBrandMarkSize : _brandMarkSize,
height: _brandMarkSize, radius: compact ? _compactBrandMarkRadius : _brandMarkRadius,
decoration: BoxDecoration( iconSize: compact ? _compactBrandIconSize : _brandIconSize,
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( Widget _brandCopy(BuildContext context, {required bool centered}) => Column(
@@ -361,9 +348,9 @@ class _AuthBrand extends StatelessWidget {
AppLocalizations.of(context)!.appName, AppLocalizations.of(context)!.appName,
textAlign: centered ? TextAlign.center : TextAlign.start, textAlign: centered ? TextAlign.center : TextAlign.start,
style: const TextStyle( style: const TextStyle(
color: AppColors.onPrimary, color: AppColors.onSurface,
fontSize: 26, fontSize: 24,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w900,
height: 1.1, height: 1.1,
), ),
), ),
@@ -372,8 +359,8 @@ class _AuthBrand extends StatelessWidget {
tagline, tagline,
textAlign: centered ? TextAlign.center : TextAlign.start, textAlign: centered ? TextAlign.center : TextAlign.start,
style: const TextStyle( style: const TextStyle(
color: AppColors.blueSoft, color: AppColors.muted,
fontSize: 14, fontSize: 13,
height: 1.35, height: 1.35,
), ),
), ),
@@ -406,7 +393,7 @@ class _AuthError extends StatelessWidget {
child: Text( child: Text(
message, message,
key: const Key('auth-error'), key: const Key('auth-error'),
style: TextStyle(color: color, fontSize: 13, height: 1.35), style: TextStyle(color: color, fontSize: 12, height: 1.35),
), ),
), ),
], ],
+103 -71
View File
@@ -11,6 +11,7 @@ import '../../core/platform/haptics.dart';
import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors.dart';
import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_dimens.dart';
import '../../core/theme/app_theme.dart'; import '../../core/theme/app_theme.dart';
import '../../core/widgets/subpage_header.dart';
import '../../l10n/app_localizations.dart'; import '../../l10n/app_localizations.dart';
import 'auth_logic.dart'; import 'auth_logic.dart';
import 'phone_auth_sheet.dart'; import 'phone_auth_sheet.dart';
@@ -19,18 +20,21 @@ part 'auth_widgets.dart';
part 'auth_form_widgets.dart'; part 'auth_form_widgets.dart';
const double _minTapTarget = 44; const double _minTapTarget = 44;
const double _brandMarkSize = 48; const double _brandMarkSize = 72;
const double _compactBrandMarkSize = 48;
const double _brandMarkRadius = 20;
const double _compactBrandMarkRadius = 14;
const double _brandIconSize = 36;
const double _compactBrandIconSize = 26;
const double _contentMaxWidth = 440; const double _contentMaxWidth = 440;
// Auth keeps its approved 20px card silhouette. // Auth keeps its approved 20px card silhouette.
const double _cardRadius = AppDimens.authCardRadius; const double _cardRadius = AppDimens.authCardRadius;
const double _cardPadding = AppDimens.cardPadding; const double _cardPadding = 20;
const double _cardBorderWidth = 1;
const double _fieldHeight = 52; const double _fieldHeight = 52;
const double _fieldRadius = 14; const double _fieldRadius = 14;
const double _fieldBorderWidth = 1.5; const double _fieldBorderWidth = 1;
const double _buttonHeight = 52; const double _buttonHeight = 50;
const double _buttonRadius = 26; const double _buttonRadius = 25;
const double _glowOpacity = 0.18;
const double _errorOpacity = 0.10; const double _errorOpacity = 0.10;
const double _errorBorderOpacity = 0.28; const double _errorBorderOpacity = 0.28;
const double _enterSlideDistance = 16; const double _enterSlideDistance = 16;
@@ -39,11 +43,48 @@ const double _bottomContentPadding = 28;
const double _compactTopContentPadding = 48; const double _compactTopContentPadding = 48;
const double _compactHeightBreakpoint = 720; const double _compactHeightBreakpoint = 720;
const double _compactBrandGap = 16; const double _compactBrandGap = 16;
const double _warmGlowSize = 260; const double _glowSize = 280;
const double _warmGlowOpacity = 0.06; const double _warmGlowOpacity = 0.18;
const double _entryButtonHeight = 50;
const double _entryButtonRadius = 25;
const double _entryButtonFontSize = 15;
const double _entryIconSize = 20;
const Duration _enterDuration = Duration(milliseconds: 220); const Duration _enterDuration = Duration(milliseconds: 220);
const Duration _segmentDuration = Duration(milliseconds: 160); const Duration _segmentDuration = Duration(milliseconds: 160);
Widget _brandMarkWidget({
Key? key,
required double size,
required double radius,
required double iconSize,
}) {
return Container(
key: key,
width: size,
height: size,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(radius),
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AppColors.blue, AppColors.secondary],
),
boxShadow: [
BoxShadow(
color: AppColors.blue.withValues(alpha: 0.24),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Icon(
Icons.play_arrow_rounded,
color: AppColors.onPrimary,
size: iconSize,
),
);
}
/// 登录 / 注册页:邮箱 + 密码。 /// 登录 / 注册页:邮箱 + 密码。
class AuthPage extends StatefulWidget { class AuthPage extends StatefulWidget {
const AuthPage({super.key, this.signUpMode = false}); const AuthPage({super.key, this.signUpMode = false});
@@ -147,34 +188,39 @@ class _AuthPageState extends State<AuthPage> {
backgroundColor: AppColors.ink, backgroundColor: AppColors.ink,
body: Stack( body: Stack(
children: [ children: [
const Positioned.fill(
child: IgnorePointer(
child: DecoratedBox(
key: Key('auth-page-background'),
decoration: BoxDecoration(gradient: AppTheme.pageGradient),
),
),
),
Positioned( Positioned(
top: 0, top: -90,
left: 0, right: -70,
right: 0, width: _glowSize,
height: 420, height: _glowSize,
child: IgnorePointer( child: IgnorePointer(
child: Container( child: Container(
key: const Key('auth-background-glow'), key: const Key('auth-background-glow'),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( shape: BoxShape.circle,
begin: Alignment.topCenter, gradient: RadialGradient(
end: Alignment.bottomCenter,
colors: [ colors: [
AppColors.blue.withValues(alpha: _glowOpacity), AppColors.surfaceTop,
AppColors.blue.withValues(alpha: 0.05), AppColors.surfaceTop.withValues(alpha: 0),
AppColors.ink.withValues(alpha: 0),
], ],
stops: const [0, 0.55, 1],
), ),
), ),
), ),
), ),
), ),
Positioned( Positioned(
top: 72, left: -110,
right: -80, bottom: -90,
width: _warmGlowSize, width: _glowSize,
height: _warmGlowSize, height: _glowSize,
child: IgnorePointer( child: IgnorePointer(
child: Container( child: Container(
key: const Key('auth-background-warm-glow'), key: const Key('auth-background-warm-glow'),
@@ -239,23 +285,16 @@ class _AuthPageState extends State<AuthPage> {
child: Align( child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 4, top: 4), padding: const EdgeInsets.only(left: 14, top: 8),
child: IconButton( child: AppSubpageBackButton(
key: const Key('auth-back'), backKey: const Key('auth-back'),
tooltip: MaterialLocalizations.of( onBack: () {
context,
).backButtonTooltip,
onPressed: () {
if (_emailEntryOpen) { if (_emailEntryOpen) {
setState(() => _emailEntryOpen = false); setState(() => _emailEntryOpen = false);
} else { } else {
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
}, },
icon: const Icon(
Icons.arrow_back_rounded,
color: AppColors.onPrimary,
),
), ),
), ),
), ),
@@ -298,58 +337,44 @@ class _AuthPageState extends State<AuthPage> {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Center( Center(
child: Container( child: _brandMarkWidget(
key: const Key('auth-brand-logo'), key: const Key('auth-brand-logo'),
width: 64, size: _brandMarkSize,
height: 64, radius: _brandMarkRadius,
decoration: BoxDecoration( iconSize: _brandIconSize,
borderRadius: BorderRadius.circular(18),
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AppColors.blue, AppColors.secondary],
),
),
child: const Icon(
Icons.play_arrow_rounded,
color: AppColors.onPrimary,
size: 32,
),
), ),
), ),
const SizedBox(height: 14), const SizedBox(height: 16),
Text( Text(
l10n.fruitShortsBrand, l10n.fruitShortsBrand,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: const TextStyle(
fontFamily: 'Outfit', fontFamily: 'Outfit',
color: AppColors.onSurface, color: AppColors.onSurface,
fontSize: 20, fontSize: 24,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w900,
), ),
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
Text( Text(
l10n.authLandingSubtitle, l10n.authLandingSubtitle,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle(color: AppColors.subtle, fontSize: 11), style: const TextStyle(color: AppColors.muted, fontSize: 13),
), ),
const SizedBox(height: 22), const SizedBox(height: 28),
_AuthEntryButton( _AuthEntryButton(
key: const Key('auth-entry-phone'), key: const Key('auth-entry-phone'),
label: l10n.authPhoneSignIn, label: l10n.authPhoneSignIn,
icon: Icons.phone_android_rounded, icon: Icons.phone_android_rounded,
background: AppColors.input, primary: true,
foreground: AppColors.onSurface,
onTap: _openPhoneSheet, onTap: _openPhoneSheet,
), ),
const SizedBox(height: 11), const SizedBox(height: 12),
_AuthEntryButton( _AuthEntryButton(
key: const Key('auth-entry-email'), key: const Key('auth-entry-email'),
label: l10n.authEmailSignIn, label: l10n.authEmailSignIn,
icon: Icons.mail_outline_rounded, icon: Icons.mail_outline_rounded,
background: AppColors.input, primary: false,
foreground: AppColors.onSurface,
onTap: () => setState(() => _emailEntryOpen = true), onTap: () => setState(() => _emailEntryOpen = true),
), ),
const SizedBox(height: 18), const SizedBox(height: 18),
@@ -417,33 +442,40 @@ class _AuthEntryButton extends StatelessWidget {
const _AuthEntryButton({ const _AuthEntryButton({
super.key, super.key,
required this.label, required this.label,
required this.background, required this.primary,
required this.foreground,
required this.onTap, required this.onTap,
this.icon, this.icon,
}); });
final String label; final String label;
final Color background; final bool primary;
final Color foreground;
final VoidCallback onTap; final VoidCallback onTap;
final IconData? icon; final IconData? icon;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final background = primary ? AppColors.blue : AppColors.card;
final foreground = primary ? AppColors.onPrimary : AppColors.onSurface;
return Material( return Material(
color: background, color: background,
borderRadius: BorderRadius.circular(9), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(_entryButtonRadius),
side: primary
? BorderSide.none
: const BorderSide(color: AppColors.border),
),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(_entryButtonRadius),
child: SizedBox( child: SizedBox(
height: 44, height: _entryButtonHeight,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (icon != null) Icon(icon, size: 16, color: foreground), if (icon != null)
if (icon != null) const SizedBox(width: 7), Icon(icon, size: _entryIconSize, color: foreground),
if (icon != null) const SizedBox(width: 8),
Flexible( Flexible(
child: Text( child: Text(
label, label,
@@ -451,8 +483,8 @@ class _AuthEntryButton extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
color: foreground, color: foreground,
fontSize: 13, fontSize: _entryButtonFontSize,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w800,
), ),
), ),
), ),
+6 -10
View File
@@ -76,10 +76,7 @@ class _AuthCard extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.card, color: AppColors.card,
borderRadius: BorderRadius.circular(_cardRadius), borderRadius: BorderRadius.circular(_cardRadius),
border: Border.all( boxShadow: const [AppTheme.cardShadow],
color: AppColors.outlineVariantDark,
width: _cardBorderWidth,
),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -94,7 +91,7 @@ class _AuthCard extends StatelessWidget {
Text( Text(
status.signUpMode ? l10n.authSignUpTitle : l10n.authSignInTitle, status.signUpMode ? l10n.authSignUpTitle : l10n.authSignInTitle,
style: const TextStyle( style: const TextStyle(
color: AppColors.onPrimary, color: AppColors.onSurface,
fontSize: 25, fontSize: 25,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
height: 1.2, height: 1.2,
@@ -149,12 +146,11 @@ class _AuthModeSwitch extends StatelessWidget {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(22)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(22)),
), ),
child: Container( child: Container(
height: _minTapTarget, height: 36,
padding: const EdgeInsets.all(3), padding: const EdgeInsets.all(2),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.bar, color: AppColors.raised,
borderRadius: BorderRadius.circular(22), borderRadius: BorderRadius.circular(18),
border: Border.all(color: AppColors.outlineVariantDark),
), ),
child: Row( child: Row(
children: [ children: [
+1 -1
View File
@@ -53,7 +53,7 @@ void main() {
final errorBorder = email.decoration!.errorBorder! as OutlineInputBorder; final errorBorder = email.decoration!.errorBorder! as OutlineInputBorder;
final scheme = Theme.of(tester.element(emailFinder)).colorScheme; final scheme = Theme.of(tester.element(emailFinder)).colorScheme;
expect(errorBorder.borderSide.color, scheme.error); expect(errorBorder.borderSide.color, scheme.error);
expect(errorBorder.borderSide.width, 1.5); expect(errorBorder.borderSide.width, 1);
}); });
} }
+94 -17
View File
@@ -13,6 +13,7 @@ import 'package:fruitshorts/core/data/services/auth_controller.dart';
import 'package:fruitshorts/core/data/services/auth_session_store.dart'; import 'package:fruitshorts/core/data/services/auth_session_store.dart';
import 'package:fruitshorts/core/platform/haptics.dart'; import 'package:fruitshorts/core/platform/haptics.dart';
import 'package:fruitshorts/core/theme/app_colors.dart'; import 'package:fruitshorts/core/theme/app_colors.dart';
import 'package:fruitshorts/core/theme/app_theme.dart';
import 'package:fruitshorts/l10n/app_localizations.dart'; import 'package:fruitshorts/l10n/app_localizations.dart';
import 'package:fruitshorts/pages/auth/auth_view.dart'; import 'package:fruitshorts/pages/auth/auth_view.dart';
@@ -30,6 +31,7 @@ void main() {
AuthApi? api, AuthApi? api,
FakeHaptics? haptics, FakeHaptics? haptics,
bool openEmail = true, bool openEmail = true,
bool asRoute = false,
}) async { }) async {
final auth = AuthController( final auth = AuthController(
api ?? api ??
@@ -53,11 +55,15 @@ void main() {
locale: const Locale('en'), locale: const Locale('en'),
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
home: const AuthPage(), home: asRoute ? const _AuthRouteHost() : const AuthPage(),
), ),
), ),
); );
await tester.pump(const Duration(milliseconds: 220)); await tester.pump(const Duration(milliseconds: 220));
if (asRoute) {
await tester.tap(find.byKey(const Key('auth-open')));
await tester.pumpAndSettle();
}
if (openEmail) { if (openEmail) {
await tester.ensureVisible(find.byKey(const Key('auth-entry-email'))); await tester.ensureVisible(find.byKey(const Key('auth-entry-email')));
await tester.tap(find.byKey(const Key('auth-entry-email'))); await tester.tap(find.byKey(const Key('auth-entry-email')));
@@ -78,21 +84,46 @@ void main() {
); );
expect( expect(
tester.getSize(find.byKey(const Key('auth-brand-logo'))), tester.getSize(find.byKey(const Key('auth-brand-logo'))),
const Size(64, 64), const Size(72, 72),
); );
expect(find.byKey(const Key('auth-entry-google')), findsNothing); expect(find.byKey(const Key('auth-entry-google')), findsNothing);
expect(find.byKey(const Key('auth-entry-phone')), findsOneWidget); expect(find.byKey(const Key('auth-entry-phone')), findsOneWidget);
expect(find.byKey(const Key('auth-entry-email')), findsOneWidget); expect(find.byKey(const Key('auth-entry-email')), findsOneWidget);
expect(find.byKey(const Key('auth-legal')), findsOneWidget); expect(find.byKey(const Key('auth-legal')), findsOneWidget);
expect(
tester.getSize(find.byKey(const Key('auth-entry-phone'))).height,
50,
);
expect(
tester.getSize(find.byKey(const Key('auth-entry-email'))).height,
50,
);
final phoneButton = tester.widget<Material>(
find.descendant(
of: find.byKey(const Key('auth-entry-phone')),
matching: find.byType(Material),
),
);
expect(phoneButton.color, AppColors.blue);
final emailButton = tester.widget<Material>(
find.descendant(
of: find.byKey(const Key('auth-entry-email')),
matching: find.byType(Material),
),
);
expect(emailButton.color, AppColors.card);
expect(
(emailButton.shape! as RoundedRectangleBorder).side.color,
AppColors.border,
);
final glow = tester.widget<Container>( final glow = tester.widget<Container>(
find.byKey(const Key('auth-background-glow')), find.byKey(const Key('auth-background-glow')),
); );
final glowDecoration = glow.decoration! as BoxDecoration; final glowDecoration = glow.decoration! as BoxDecoration;
final gradient = glowDecoration.gradient! as LinearGradient; final gradient = glowDecoration.gradient! as RadialGradient;
for (final color in gradient.colors) { expect(gradient.colors.first, AppColors.surfaceTop);
expect(color.a, lessThanOrEqualTo(0.18)); expect(gradient.colors.last.a, 0);
}
await tester.tap(find.byKey(const Key('auth-entry-email'))); await tester.tap(find.byKey(const Key('auth-entry-email')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
@@ -101,19 +132,28 @@ void main() {
final cardDecoration = card.decoration! as BoxDecoration; final cardDecoration = card.decoration! as BoxDecoration;
expect(cardDecoration.color, AppColors.card); expect(cardDecoration.color, AppColors.card);
expect(cardDecoration.borderRadius, BorderRadius.circular(20)); expect(cardDecoration.borderRadius, BorderRadius.circular(20));
expect( expect(cardDecoration.boxShadow, contains(AppTheme.cardShadow));
(cardDecoration.border! as Border).top.color, expect(card.padding, const EdgeInsets.all(20));
AppColors.outlineVariantDark,
);
expect((cardDecoration.border! as Border).top.width, 1);
final email = tester.widget<TextField>( final email = tester.widget<TextField>(
find.byKey(const Key('auth-email')), find.byKey(const Key('auth-email')),
); );
expect(email.decoration?.fillColor, AppColors.bar); expect(email.decoration?.fillColor, AppColors.card);
expect(email.style?.color, AppColors.onSurface);
expect(email.style?.fontSize, 15);
expect(email.cursorColor, AppColors.blue);
expect(tester.getSize(find.byKey(const Key('auth-email'))).height, 52); expect(tester.getSize(find.byKey(const Key('auth-email'))).height, 52);
final emailBorder = email.decoration!.border! as OutlineInputBorder; final emailBorder =
email.decoration!.enabledBorder! as OutlineInputBorder;
expect(emailBorder.borderRadius, BorderRadius.circular(14)); expect(emailBorder.borderRadius, BorderRadius.circular(14));
expect(emailBorder.borderSide.color, AppColors.border);
expect(emailBorder.borderSide.width, 1);
final password = tester.widget<TextField>(
find.byKey(const Key('auth-password')),
);
expect(password.style?.color, AppColors.onSurface);
expect(password.style?.fontSize, 15);
expect(password.cursorColor, AppColors.blue);
final submit = tester.widget<FilledButton>( final submit = tester.widget<FilledButton>(
find.byKey(const Key('auth-submit')), find.byKey(const Key('auth-submit')),
@@ -123,11 +163,11 @@ void main() {
submit.style!.backgroundColor!.resolve(const {WidgetState.pressed}), submit.style!.backgroundColor!.resolve(const {WidgetState.pressed}),
AppColors.bluePressed, AppColors.bluePressed,
); );
expect(tester.getSize(find.byKey(const Key('auth-submit'))).height, 52); expect(tester.getSize(find.byKey(const Key('auth-submit'))).height, 50);
expect( expect(
(submit.style!.shape!.resolve(const {})! as RoundedRectangleBorder) (submit.style!.shape!.resolve(const {})! as RoundedRectangleBorder)
.borderRadius, .borderRadius,
BorderRadius.circular(26), BorderRadius.circular(25),
); );
expect(find.text('Sign In'), findsWidgets); expect(find.text('Sign In'), findsWidgets);
@@ -139,6 +179,23 @@ void main() {
}, },
); );
testWidgets('should_render_40px_back_button_when_page_is_pushed', (
tester,
) async {
await pumpAuthPage(tester, openEmail: false, asRoute: true);
final back = find.byKey(const Key('auth-back'));
expect(tester.getSize(back), const Size(40, 40));
expect(
tester
.widget<Material>(
find.descendant(of: back, matching: find.byType(Material)),
)
.color,
AppColors.card,
);
});
testWidgets('should_fade_and_slide_content_in_within_220ms', (tester) async { testWidgets('should_fade_and_slide_content_in_within_220ms', (tester) async {
await pumpAuthPage(tester); await pumpAuthPage(tester);
@@ -198,10 +255,10 @@ void main() {
final focusedBorder = final focusedBorder =
email.decoration!.focusedBorder! as OutlineInputBorder; email.decoration!.focusedBorder! as OutlineInputBorder;
expect(focusedBorder.borderSide.color, AppColors.blue); expect(focusedBorder.borderSide.color, AppColors.blue);
expect(focusedBorder.borderSide.width, 1.5); expect(focusedBorder.borderSide.width, 1);
expect( expect(
tester.widget<Icon>(find.byIcon(Icons.mail_outline_rounded)).color, tester.widget<Icon>(find.byIcon(Icons.mail_outline_rounded)).color,
AppColors.blueSoft, AppColors.blue,
); );
await tester.tap(find.byKey(const Key('auth-password-visibility'))); await tester.tap(find.byKey(const Key('auth-password-visibility')));
@@ -280,6 +337,7 @@ void main() {
final error = tester.widget<Text>(errorFinder); final error = tester.widget<Text>(errorFinder);
final scheme = Theme.of(tester.element(errorFinder)).colorScheme; final scheme = Theme.of(tester.element(errorFinder)).colorScheme;
expect(error.style?.color, scheme.error); expect(error.style?.color, scheme.error);
expect(error.style?.fontSize, 12);
expect( expect(
find.descendant( find.descendant(
of: capsuleFinder, of: capsuleFinder,
@@ -325,3 +383,22 @@ class _PendingAuthApi extends AuthApi {
throw const ApiException(500, 'pending failure'); throw const ApiException(500, 'pending failure');
} }
} }
class _AuthRouteHost extends StatelessWidget {
const _AuthRouteHost();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: TextButton(
key: const Key('auth-open'),
onPressed: () => Navigator.of(
context,
).push<void>(MaterialPageRoute(builder: (_) => const AuthPage())),
child: const Text('Open auth'),
),
),
);
}
}