diff --git a/lib/main.dart b/lib/main.dart index 3855bb2..013e145 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,22 +1,25 @@ import 'package:flutter/material.dart'; import 'package:login_page/screens/home.dart'; +import 'package:login_page/screens/login.dart'; -void main() => runApp(MyApp()); +void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); - var title = "Login"; @override Widget build(BuildContext context) { return MaterialApp( - title: "$title", + title: "Login & Registration", debugShowCheckedModeBanner: false, - home: Home(), - + initialRoute: '/login', + routes: { + '/login': (context) => const LoginScreen(), + '/register': (context) => const Home(), + }, theme: ThemeData( primarySwatch: Colors.blue, ), - ); } } \ No newline at end of file diff --git a/lib/screens/home.dart b/lib/screens/home.dart index ebd7381..0b4a797 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -2,192 +2,234 @@ import 'package:flutter/material.dart'; import 'package:login_page/widgets/input_field.dart'; import 'package:login_page/widgets/membership.dart'; import 'package:login_page/widgets/gender.dart'; +import 'package:login_page/utils/validators.dart'; +class Home extends StatefulWidget { + const Home({Key? key}) : super(key: key); -class Home extends StatelessWidget { + @override + State createState() => _HomeState(); +} + +class _HomeState extends State { + final _formKey = GlobalKey(); + final _nameController = TextEditingController(); + final _dobController = TextEditingController(); + final _emailController = TextEditingController(); + final _mobileController = TextEditingController(); + final _customerIdController = TextEditingController(); + String _selectedGender = 'male'; + String _selectedMembership = 'classic'; + + @override + void dispose() { + _nameController.dispose(); + _dobController.dispose(); + _emailController.dispose(); + _mobileController.dispose(); + _customerIdController.dispose(); + super.dispose(); + } + + void _handleSave() { + if (_formKey.currentState!.validate()) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Registration successful!'), + backgroundColor: Colors.green, + ), + ); + } + } + + void _handleCancel() { + _formKey.currentState?.reset(); + _nameController.clear(); + _dobController.clear(); + _emailController.clear(); + _mobileController.clear(); + _customerIdController.clear(); + } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.blue[50], - body: Padding( - padding: EdgeInsets.only(top: 60.0, bottom: 60.0, left: 120.0, right: 120.0), - child: Card( - shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(40.0)), - elevation: 5.0, - child: Container( + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only( + top: 60.0, + bottom: 60.0, + left: 120.0, + right: 120.0, + ), + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40.0), + ), + elevation: 5.0, child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - width: MediaQuery.of(context).size.width /3.3, - height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width / 3.3, + constraints: const BoxConstraints(minHeight: 600), color: Colors.yellow[600], child: Padding( - padding: EdgeInsets.only(top: 70.0, right: 50.0, left: 50.0), - child: Align( - alignment: Alignment.centerRight, - child: Column( - children: [ - - Container( - child: CircleAvatar( - backgroundColor: Colors.black87, - backgroundImage: NetworkImage( - 'https://jideguru.github.io/static/img/profile.png', - ), - radius: 70.0, - ), + padding: const EdgeInsets.only( + top: 70.0, + right: 50.0, + left: 50.0, + ), + child: Column( + children: [ + const CircleAvatar( + backgroundColor: Colors.black87, + backgroundImage: NetworkImage( + 'https://jideguru.github.io/static/img/profile.png', ), - - - SizedBox(height: 60.0,), - - - Container( - padding: EdgeInsets.only( - top: 5.0, - bottom: 5.0 - ), - child: Text( - "Let's get you set up", - style: TextStyle( - fontSize: 30.0, - fontWeight: FontWeight.w900, - ), - ), + radius: 70.0, + ), + const SizedBox(height: 60.0), + Container( + padding: const EdgeInsets.only( + top: 5.0, + bottom: 5.0, ), - - SizedBox(height: 5.0,), - - - Container( - padding: EdgeInsets.only( - top: 5.0, - bottom: 5.0 - ), - child: Text( - "It should only take a couple of minutes to pair with your watch", - style: TextStyle( - fontSize: 18.0, - ), - textAlign: TextAlign.center, + child: const Text( + "Let's get you set up", + style: TextStyle( + fontSize: 30.0, + fontWeight: FontWeight.w900, ), ), - - SizedBox(height: 50.0,), - - - Container( - child: CircleAvatar( - backgroundColor: Colors.black87, - child: Text( - ">", - style: TextStyle(color: Colors.yellow), - ), + ), + const SizedBox(height: 5.0), + Container( + padding: const EdgeInsets.only( + top: 5.0, + bottom: 5.0, + ), + child: const Text( + "It should only take a couple of minutes to pair with your watch", + style: TextStyle( + fontSize: 18.0, ), + textAlign: TextAlign.center, ), - - ], - ), + ), + const SizedBox(height: 50.0), + const CircleAvatar( + backgroundColor: Colors.black87, + child: Text( + ">", + style: TextStyle(color: Colors.yellow), + ), + ), + ], ), ), ), - - - - - Container( - padding: EdgeInsets.only(top: 40.0, right: 70.0, left: 70.0, bottom: 40.0), - child: Column( - children: [ - - //InputField Widget from the widgets folder - InputField( - label: "Name", - content: "Name" - ), - - SizedBox(height: 20.0), - - //Gender Widget from the widgets folder - Gender(), - - SizedBox(height: 20.0), - - - //InputField Widget from the widgets folder - InputField( - label: "Date of birth", - content: "01/02/1986" - ), - - - SizedBox(height: 20.0), - - - //InputField Widget from the widgets folder - InputField( - label: "Email", - content: "yo@seethat.com" - ), - - SizedBox(height: 20.0), - - - - InputField( - label: "Mobile", - content: "+22994684468" - ), - - - SizedBox(height: 20.0), - - //InputField Widget from the widgets folder - InputField( - label: "Customer ID", - content: "22223311111" - ), - - - SizedBox(height: 40.0,), - - //Membership Widget from the widgets folder - Membership(), - - SizedBox(height: 40.0,), - - Row( + Expanded( + child: Container( + padding: const EdgeInsets.only( + top: 40.0, + right: 70.0, + left: 70.0, + bottom: 40.0, + ), + child: Form( + key: _formKey, + child: Column( children: [ - SizedBox(width: 170.0,), - FlatButton( - color: Colors.grey[200], - onPressed: (){}, - child: Text( - "Cancel" - ), + InputField( + label: "Name", + hintText: "Enter your name", + controller: _nameController, + validator: Validators.validateName, ), - - SizedBox(width: 20.0,), - - FlatButton( - color: Colors.greenAccent, - onPressed: (){}, - child: Text( - "Save", - style: TextStyle( - color: Colors.white + const SizedBox(height: 20.0), + Gender( + onChanged: (value) { + setState(() { + _selectedGender = value; + }); + }, + ), + const SizedBox(height: 20.0), + InputField( + label: "Date of birth", + hintText: "01/02/1986", + controller: _dobController, + validator: (value) => + Validators.validateRequired(value, 'Date of birth'), + ), + const SizedBox(height: 20.0), + InputField( + label: "Email", + hintText: "yo@seethat.com", + controller: _emailController, + validator: Validators.validateEmail, + keyboardType: TextInputType.emailAddress, + ), + const SizedBox(height: 20.0), + InputField( + label: "Mobile", + hintText: "+22994684468", + controller: _mobileController, + validator: Validators.validatePhone, + keyboardType: TextInputType.phone, + ), + const SizedBox(height: 20.0), + InputField( + label: "Customer ID", + hintText: "22223311111", + controller: _customerIdController, + validator: Validators.validateCustomerId, + ), + const SizedBox(height: 40.0), + Membership( + onChanged: (value) { + setState(() { + _selectedMembership = value; + }); + }, + ), + const SizedBox(height: 40.0), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + style: TextButton.styleFrom( + backgroundColor: Colors.grey[200], + padding: const EdgeInsets.symmetric( + horizontal: 30.0, + vertical: 15.0, + ), + ), + onPressed: _handleCancel, + child: const Text("Cancel"), ), - ), + const SizedBox(width: 20.0), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.greenAccent, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 30.0, + vertical: 15.0, + ), + ), + onPressed: _handleSave, + child: const Text("Save"), + ), + ], ), ], ), - - ], + ), ), - ), - ], ), ), @@ -195,5 +237,4 @@ class Home extends StatelessWidget { ), ); } - } diff --git a/lib/screens/login.dart b/lib/screens/login.dart new file mode 100644 index 0000000..633ec7b --- /dev/null +++ b/lib/screens/login.dart @@ -0,0 +1,202 @@ +import 'package:flutter/material.dart'; +import 'package:login_page/utils/validators.dart'; +import 'package:login_page/widgets/input_field.dart'; +import 'package:login_page/widgets/password_field.dart'; + +class LoginScreen extends StatefulWidget { + const LoginScreen({Key? key}) : super(key: key); + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + final _formKey = GlobalKey(); + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + bool _rememberMe = false; + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + void _handleLogin() { + if (_formKey.currentState!.validate()) { + // Login logic would go here + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Login successful!'), + backgroundColor: Colors.green, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.blue[50], + body: Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(40.0), + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20.0), + ), + elevation: 8.0, + child: Container( + constraints: const BoxConstraints(maxWidth: 500), + padding: const EdgeInsets.all(40.0), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Logo/Avatar + Center( + child: CircleAvatar( + backgroundColor: Colors.blue[600], + radius: 50.0, + child: const Icon( + Icons.person, + size: 50, + color: Colors.white, + ), + ), + ), + const SizedBox(height: 30.0), + + // Title + const Text( + 'Welcome Back', + style: TextStyle( + fontSize: 28.0, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 10.0), + Text( + 'Sign in to continue', + style: TextStyle( + fontSize: 16.0, + color: Colors.grey[600], + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 40.0), + + // Email Field + TextFormField( + controller: _emailController, + validator: Validators.validateEmail, + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration( + labelText: 'Email', + hintText: 'Enter your email', + prefixIcon: const Icon(Icons.email), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + ), + filled: true, + fillColor: Colors.blue[50], + ), + ), + const SizedBox(height: 20.0), + + // Password Field + TextFormField( + controller: _passwordController, + validator: Validators.validatePassword, + obscureText: true, + decoration: InputDecoration( + labelText: 'Password', + hintText: 'Enter your password', + prefixIcon: const Icon(Icons.lock), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + ), + filled: true, + fillColor: Colors.blue[50], + ), + ), + const SizedBox(height: 10.0), + + // Remember Me & Forgot Password + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Checkbox( + value: _rememberMe, + onChanged: (value) { + setState(() { + _rememberMe = value ?? false; + }); + }, + ), + const Text('Remember me'), + ], + ), + TextButton( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Forgot password clicked'), + ), + ); + }, + child: const Text('Forgot Password?'), + ), + ], + ), + const SizedBox(height: 20.0), + + // Login Button + ElevatedButton( + onPressed: _handleLogin, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue[600], + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 15.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + ), + child: const Text( + 'Login', + style: TextStyle(fontSize: 16.0), + ), + ), + const SizedBox(height: 20.0), + + // Sign Up Link + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("Don't have an account? "), + TextButton( + onPressed: () { + Navigator.pushReplacementNamed(context, '/register'); + }, + child: const Text('Sign Up'), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/utils/validators.dart b/lib/utils/validators.dart new file mode 100644 index 0000000..9674beb --- /dev/null +++ b/lib/utils/validators.dart @@ -0,0 +1,74 @@ +class Validators { + static String? validateEmail(String? value) { + if (value == null || value.isEmpty) { + return 'Email is required'; + } + + final emailRegex = RegExp( + r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', + ); + + if (!emailRegex.hasMatch(value)) { + return 'Please enter a valid email'; + } + + return null; + } + + static String? validatePassword(String? value) { + if (value == null || value.isEmpty) { + return 'Password is required'; + } + + if (value.length < 6) { + return 'Password must be at least 6 characters'; + } + + return null; + } + + static String? validatePhone(String? value) { + if (value == null || value.isEmpty) { + return 'Phone number is required'; + } + + final phoneRegex = RegExp(r'^\+?[0-9]{10,15}$'); + + if (!phoneRegex.hasMatch(value.replaceAll(RegExp(r'\s+'), ''))) { + return 'Please enter a valid phone number'; + } + + return null; + } + + static String? validateRequired(String? value, String fieldName) { + if (value == null || value.isEmpty) { + return '$fieldName is required'; + } + return null; + } + + static String? validateName(String? value) { + if (value == null || value.isEmpty) { + return 'Name is required'; + } + + if (value.length < 2) { + return 'Name must be at least 2 characters'; + } + + return null; + } + + static String? validateCustomerId(String? value) { + if (value == null || value.isEmpty) { + return 'Customer ID is required'; + } + + if (value.length < 5) { + return 'Customer ID must be at least 5 characters'; + } + + return null; + } +} diff --git a/lib/widgets/gender.dart b/lib/widgets/gender.dart index 18170ab..acc1a92 100644 --- a/lib/widgets/gender.dart +++ b/lib/widgets/gender.dart @@ -1,56 +1,94 @@ import 'package:flutter/material.dart'; -class Gender extends StatelessWidget { +class Gender extends StatefulWidget { + final Function(String)? onChanged; + final String? initialValue; + + const Gender({ + Key? key, + this.onChanged, + this.initialValue, + }) : super(key: key); + + @override + State createState() => _GenderState(); +} + +class _GenderState extends State { + String _selectedGender = 'male'; + + @override + void initState() { + super.initState(); + if (widget.initialValue != null) { + _selectedGender = widget.initialValue!; + } + } + @override Widget build(BuildContext context) { - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Row( - children: [ - Container( - width: 80.0, - child: Text( - "Gender", - textAlign: TextAlign.left, - ), - ), - SizedBox( - width: 40.0, - ), - CircleAvatar( - backgroundColor: Colors.blue[50], - child: Icon(Icons.face, color: Colors.grey), - ), - SizedBox( - width: 30.0, - ), - Container( - width: 70.0, - child: Text( - "Male", - textAlign: TextAlign.left, - ), - ), - CircleAvatar( - backgroundColor: Colors.blue[50], - child: Icon( - Icons.face, - color: Colors.grey, - ), - ), - SizedBox( - width: 30.0, + return Row( + children: [ + Container( + width: 80.0, + child: const Text( + "Gender", + textAlign: TextAlign.left, + ), + ), + const SizedBox(width: 40.0), + InkWell( + onTap: () { + setState(() { + _selectedGender = 'male'; + }); + widget.onChanged?.call('male'); + }, + child: CircleAvatar( + backgroundColor: _selectedGender == 'male' + ? Colors.blue[300] + : Colors.blue[50], + child: Icon( + Icons.face, + color: _selectedGender == 'male' ? Colors.white : Colors.grey, ), - Container( - width: 140.0, - child: Text( - "Female", - textAlign: TextAlign.left, - ), + ), + ), + const SizedBox(width: 10.0), + Container( + width: 70.0, + child: const Text( + "Male", + textAlign: TextAlign.left, + ), + ), + const SizedBox(width: 20.0), + InkWell( + onTap: () { + setState(() { + _selectedGender = 'female'; + }); + widget.onChanged?.call('female'); + }, + child: CircleAvatar( + backgroundColor: _selectedGender == 'female' + ? Colors.blue[300] + : Colors.blue[50], + child: Icon( + Icons.face, + color: _selectedGender == 'female' ? Colors.white : Colors.grey, ), - ], - ); - }, + ), + ), + const SizedBox(width: 10.0), + Container( + width: 140.0, + child: const Text( + "Female", + textAlign: TextAlign.left, + ), + ), + ], ); } } diff --git a/lib/widgets/input_field.dart b/lib/widgets/input_field.dart index a424814..d995448 100644 --- a/lib/widgets/input_field.dart +++ b/lib/widgets/input_field.dart @@ -2,55 +2,61 @@ import 'package:flutter/material.dart'; class InputField extends StatelessWidget { final String label; - final String content; + final String? hintText; + final TextEditingController? controller; + final String? Function(String?)? validator; + final TextInputType? keyboardType; - InputField({this.label, this.content}); + const InputField({ + Key? key, + required this.label, + this.hintText, + this.controller, + this.validator, + this.keyboardType, + }) : super(key: key); @override Widget build(BuildContext context) { - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Row( - children: [ - Container( - width: 80.0, - child: Text( - "$label", - textAlign: TextAlign.left, - ), - ), - SizedBox( - width: 40.0, + return Row( + children: [ + Container( + width: 80.0, + child: Text( + label, + textAlign: TextAlign.left, + ), + ), + const SizedBox(width: 40.0), + Expanded( + child: Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width / 3.7, ), - Container( - width: MediaQuery.of(context).size.width / 3.7, - color: Colors.blue[50], - child: TextField( - style: TextStyle( - fontSize: 15.0, + child: TextFormField( + controller: controller, + validator: validator, + keyboardType: keyboardType, + style: const TextStyle(fontSize: 15.0), + decoration: InputDecoration( + contentPadding: const EdgeInsets.all(10.0), + border: OutlineInputBorder( + borderSide: BorderSide(color: Colors.blue[50]!), + borderRadius: BorderRadius.circular(5.0), ), - decoration: InputDecoration( - contentPadding: EdgeInsets.all(10.0), - border: OutlineInputBorder( - borderSide: BorderSide( - color: Colors.blue[50], - ), - borderRadius: BorderRadius.circular(5.0), - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Colors.blue[50], - ), - borderRadius: BorderRadius.circular(5.0), - ), - hintText: "$content", - fillColor: Colors.blue[50], + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.blue[50]!), + borderRadius: BorderRadius.circular(5.0), ), + hintText: hintText, + filled: true, + fillColor: Colors.blue[50], ), ), - ], - ); - }, + ), + ), + ], ); } } + diff --git a/lib/widgets/membership.dart b/lib/widgets/membership.dart index c612a2a..83cefd0 100644 --- a/lib/widgets/membership.dart +++ b/lib/widgets/membership.dart @@ -1,70 +1,80 @@ import 'package:flutter/material.dart'; -class Membership extends StatelessWidget { +class Membership extends StatefulWidget { + final Function(String)? onChanged; + final String? initialValue; + + const Membership({ + Key? key, + this.onChanged, + this.initialValue, + }) : super(key: key); + @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Row( - children: [ - Container( - width: 90.0, - child: Text( - "Membership", - textAlign: TextAlign.left, - ), - ), - SizedBox( - width: 40.0, - ), - CircleAvatar( - backgroundColor: Colors.blue[50], - child: Icon(Icons.account_balance_wallet), - ), - SizedBox( - width: 10.0, - ), - Container( - width: 50.0, - child: Text( - "Classic", - textAlign: TextAlign.left, - ), - ), - CircleAvatar( - backgroundColor: Colors.blue[50], - child: Icon(Icons.account_balance_wallet), - ), - SizedBox( - width: 10.0, - ), - Container( - width: 50.0, - child: Text( - "Silver", - textAlign: TextAlign.left, - ), - ), - SizedBox( - width: 10.0, - ), - CircleAvatar( - backgroundColor: Colors.blue[50], - child: Icon(Icons.account_balance_wallet), - ), - SizedBox( - width: 10.0, - ), - Container( - width: 90.0, - child: Text( - "Gold", - textAlign: TextAlign.left, - ), + State createState() => _MembershipState(); +} + +class _MembershipState extends State { + String _selectedMembership = 'classic'; + + @override + void initState() { + super.initState(); + if (widget.initialValue != null) { + _selectedMembership = widget.initialValue!; + } + } + + Widget _buildMembershipOption(String type, String label) { + final isSelected = _selectedMembership == type; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () { + setState(() { + _selectedMembership = type; + }); + widget.onChanged?.call(type); + }, + child: CircleAvatar( + backgroundColor: isSelected ? Colors.blue[300] : Colors.blue[50], + child: Icon( + Icons.account_balance_wallet, + color: isSelected ? Colors.white : Colors.grey, ), - ], - ); - }, + ), + ), + const SizedBox(width: 10.0), + Container( + width: 50.0, + child: Text( + label, + textAlign: TextAlign.left, + ), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Container( + width: 90.0, + child: const Text( + "Membership", + textAlign: TextAlign.left, + ), + ), + const SizedBox(width: 40.0), + _buildMembershipOption('classic', 'Classic'), + const SizedBox(width: 10.0), + _buildMembershipOption('silver', 'Silver'), + const SizedBox(width: 10.0), + _buildMembershipOption('gold', 'Gold'), + ], ); } } diff --git a/lib/widgets/password_field.dart b/lib/widgets/password_field.dart new file mode 100644 index 0000000..11714f0 --- /dev/null +++ b/lib/widgets/password_field.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; + +class PasswordField extends StatefulWidget { + final String label; + final TextEditingController? controller; + final String? Function(String?)? validator; + + const PasswordField({ + Key? key, + required this.label, + this.controller, + this.validator, + }) : super(key: key); + + @override + State createState() => _PasswordFieldState(); +} + +class _PasswordFieldState extends State { + bool _obscureText = true; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Container( + width: 80.0, + child: Text( + widget.label, + textAlign: TextAlign.left, + ), + ), + const SizedBox(width: 40.0), + Expanded( + child: Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width / 3.7, + ), + child: TextFormField( + controller: widget.controller, + obscureText: _obscureText, + validator: widget.validator, + style: const TextStyle(fontSize: 15.0), + decoration: InputDecoration( + contentPadding: const EdgeInsets.all(10.0), + border: OutlineInputBorder( + borderSide: BorderSide(color: Colors.blue[50]!), + borderRadius: BorderRadius.circular(5.0), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.blue[50]!), + borderRadius: BorderRadius.circular(5.0), + ), + filled: true, + fillColor: Colors.blue[50], + suffixIcon: IconButton( + icon: Icon( + _obscureText ? Icons.visibility : Icons.visibility_off, + color: Colors.grey, + ), + onPressed: () { + setState(() { + _obscureText = !_obscureText; + }); + }, + ), + ), + ), + ), + ), + ], + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index d8444ff..f0a4081 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,16 +2,16 @@ name: login_page description: An app built using Flutter for web environment: - # You must be using Flutter >=1.5.0 or Dart >=2.3.0 - sdk: '>=2.3.0-dev.0.1 <3.0.0' - - -dev_dependencies: - pedantic: ^1.9.2 + sdk: '>=2.12.0 <4.0.0' dependencies: flutter: sdk: flutter +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + flutter: uses-material-design: true \ No newline at end of file diff --git a/test/screens/home_test.dart b/test/screens/home_test.dart new file mode 100644 index 0000000..6382f1b --- /dev/null +++ b/test/screens/home_test.dart @@ -0,0 +1,177 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:login_page/screens/home.dart'; + +void main() { + group('Home (Registration) Screen', () { + testWidgets('renders registration form elements', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Home(), + ), + ); + + expect(find.text("Let's get you set up"), findsOneWidget); + expect(find.text('Name'), findsOneWidget); + expect(find.text('Gender'), findsOneWidget); + expect(find.text('Date of birth'), findsOneWidget); + expect(find.text('Email'), findsOneWidget); + expect(find.text('Mobile'), findsOneWidget); + expect(find.text('Customer ID'), findsOneWidget); + expect(find.text('Membership'), findsOneWidget); + expect(find.widgetWithText(ElevatedButton, 'Save'), findsOneWidget); + expect(find.widgetWithText(TextButton, 'Cancel'), findsOneWidget); + }); + + testWidgets('validates empty name', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Home(), + ), + ); + + // Scroll to save button and tap + await tester.ensureVisible(find.widgetWithText(ElevatedButton, 'Save')); + await tester.tap(find.widgetWithText(ElevatedButton, 'Save')); + await tester.pump(); + + expect(find.text('Name is required'), findsOneWidget); + }); + + testWidgets('validates empty email', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Home(), + ), + ); + + // Scroll to save button and tap + await tester.ensureVisible(find.widgetWithText(ElevatedButton, 'Save')); + await tester.tap(find.widgetWithText(ElevatedButton, 'Save')); + await tester.pump(); + + expect(find.text('Email is required'), findsOneWidget); + }); + + testWidgets('validates invalid email format', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Home(), + ), + ); + + // Find email field and enter invalid email + final emailFields = find.byType(TextFormField); + await tester.enterText(emailFields.at(3), 'invalid-email'); + + // Scroll to save button and tap + await tester.ensureVisible(find.widgetWithText(ElevatedButton, 'Save')); + await tester.tap(find.widgetWithText(ElevatedButton, 'Save')); + await tester.pump(); + + expect(find.text('Please enter a valid email'), findsOneWidget); + }); + + testWidgets('validates empty phone', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Home(), + ), + ); + + // Scroll to save button and tap + await tester.ensureVisible(find.widgetWithText(ElevatedButton, 'Save')); + await tester.tap(find.widgetWithText(ElevatedButton, 'Save')); + await tester.pump(); + + expect(find.text('Phone number is required'), findsOneWidget); + }); + + testWidgets('validates empty customer ID', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Home(), + ), + ); + + // Scroll to save button and tap + await tester.ensureVisible(find.widgetWithText(ElevatedButton, 'Save')); + await tester.tap(find.widgetWithText(ElevatedButton, 'Save')); + await tester.pump(); + + expect(find.text('Customer ID is required'), findsOneWidget); + }); + + testWidgets('shows success message on valid registration', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Home(), + ), + ); + + // Fill in all fields + final textFields = find.byType(TextFormField); + await tester.enterText(textFields.at(0), 'John Doe'); + await tester.enterText(textFields.at(1), '01/01/1990'); + await tester.enterText(textFields.at(2), 'john@example.com'); + await tester.enterText(textFields.at(3), '+1234567890'); + await tester.enterText(textFields.at(4), '12345678'); + + // Scroll to save button and tap + await tester.ensureVisible(find.widgetWithText(ElevatedButton, 'Save')); + await tester.tap(find.widgetWithText(ElevatedButton, 'Save')); + await tester.pump(); + + expect(find.text('Registration successful!'), findsOneWidget); + }); + + testWidgets('cancel button clears form', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Home(), + ), + ); + + // Fill in name field + final textFields = find.byType(TextFormField); + await tester.enterText(textFields.first, 'John Doe'); + + // Verify text was entered + expect(find.text('John Doe'), findsOneWidget); + + // Scroll to cancel button and tap + await tester.ensureVisible(find.widgetWithText(TextButton, 'Cancel')); + await tester.tap(find.widgetWithText(TextButton, 'Cancel')); + await tester.pump(); + + // Form should be cleared (text should not be found in the field anymore) + final nameField = tester.widget(textFields.first); + expect(nameField.controller?.text, isEmpty); + }); + + testWidgets('gender selection works', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Home(), + ), + ); + + expect(find.text('Male'), findsOneWidget); + expect(find.text('Female'), findsOneWidget); + }); + + testWidgets('membership selection works', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Home(), + ), + ); + + expect(find.text('Classic'), findsOneWidget); + expect(find.text('Silver'), findsOneWidget); + expect(find.text('Gold'), findsOneWidget); + }); + }); +} diff --git a/test/screens/login_test.dart b/test/screens/login_test.dart new file mode 100644 index 0000000..d6eab66 --- /dev/null +++ b/test/screens/login_test.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:login_page/screens/login.dart'; + +void main() { + group('LoginScreen', () { + testWidgets('renders login form elements', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: LoginScreen(), + ), + ); + + expect(find.text('Welcome Back'), findsOneWidget); + expect(find.text('Sign in to continue'), findsOneWidget); + expect(find.text('Email'), findsOneWidget); + expect(find.text('Password'), findsOneWidget); + expect(find.text('Remember me'), findsOneWidget); + expect(find.text('Forgot Password?'), findsOneWidget); + expect(find.widgetWithText(ElevatedButton, 'Login'), findsOneWidget); + expect(find.text("Don't have an account? "), findsOneWidget); + }); + + testWidgets('validates empty email', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: LoginScreen(), + ), + ); + + // Tap login button without entering data + await tester.tap(find.widgetWithText(ElevatedButton, 'Login')); + await tester.pump(); + + expect(find.text('Email is required'), findsOneWidget); + }); + + testWidgets('validates empty password', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: LoginScreen(), + ), + ); + + // Enter email but not password + await tester.enterText( + find.widgetWithText(TextFormField, 'Enter your email'), + 'test@example.com', + ); + + // Tap login button + await tester.tap(find.widgetWithText(ElevatedButton, 'Login')); + await tester.pump(); + + expect(find.text('Password is required'), findsOneWidget); + }); + + testWidgets('validates invalid email format', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: LoginScreen(), + ), + ); + + // Enter invalid email + await tester.enterText( + find.widgetWithText(TextFormField, 'Enter your email'), + 'invalid-email', + ); + + // Tap login button + await tester.tap(find.widgetWithText(ElevatedButton, 'Login')); + await tester.pump(); + + expect(find.text('Please enter a valid email'), findsOneWidget); + }); + + testWidgets('validates short password', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: LoginScreen(), + ), + ); + + // Enter valid email + await tester.enterText( + find.widgetWithText(TextFormField, 'Enter your email'), + 'test@example.com', + ); + + // Enter short password + await tester.enterText( + find.widgetWithText(TextFormField, 'Enter your password'), + '12345', + ); + + // Tap login button + await tester.tap(find.widgetWithText(ElevatedButton, 'Login')); + await tester.pump(); + + expect( + find.text('Password must be at least 6 characters'), + findsOneWidget, + ); + }); + + testWidgets('shows success message on valid login', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: LoginScreen(), + ), + ); + + // Enter valid email + await tester.enterText( + find.widgetWithText(TextFormField, 'Enter your email'), + 'test@example.com', + ); + + // Enter valid password + await tester.enterText( + find.widgetWithText(TextFormField, 'Enter your password'), + 'password123', + ); + + // Tap login button + await tester.tap(find.widgetWithText(ElevatedButton, 'Login')); + await tester.pump(); + + expect(find.text('Login successful!'), findsOneWidget); + }); + + testWidgets('toggles remember me checkbox', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: LoginScreen(), + ), + ); + + final checkbox = find.byType(Checkbox); + + // Initially unchecked + expect(tester.widget(checkbox).value, false); + + // Tap checkbox + await tester.tap(checkbox); + await tester.pump(); + + // Now checked + expect(tester.widget(checkbox).value, true); + + // Tap again + await tester.tap(checkbox); + await tester.pump(); + + // Unchecked again + expect(tester.widget(checkbox).value, false); + }); + + testWidgets('forgot password button shows message', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: LoginScreen(), + ), + ); + + // Tap forgot password + await tester.tap(find.text('Forgot Password?')); + await tester.pump(); + + expect(find.text('Forgot password clicked'), findsOneWidget); + }); + }); +} diff --git a/test/utils/validators_test.dart b/test/utils/validators_test.dart new file mode 100644 index 0000000..1d6bb71 --- /dev/null +++ b/test/utils/validators_test.dart @@ -0,0 +1,149 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:login_page/utils/validators.dart'; + +void main() { + group('Validators', () { + group('validateEmail', () { + test('returns error when email is null', () { + expect(Validators.validateEmail(null), 'Email is required'); + }); + + test('returns error when email is empty', () { + expect(Validators.validateEmail(''), 'Email is required'); + }); + + test('returns error for invalid email format', () { + expect( + Validators.validateEmail('invalid-email'), + 'Please enter a valid email', + ); + expect( + Validators.validateEmail('test@'), + 'Please enter a valid email', + ); + expect( + Validators.validateEmail('@test.com'), + 'Please enter a valid email', + ); + }); + + test('returns null for valid email', () { + expect(Validators.validateEmail('test@example.com'), null); + expect(Validators.validateEmail('user.name@domain.co.uk'), null); + }); + }); + + group('validatePassword', () { + test('returns error when password is null', () { + expect(Validators.validatePassword(null), 'Password is required'); + }); + + test('returns error when password is empty', () { + expect(Validators.validatePassword(''), 'Password is required'); + }); + + test('returns error when password is too short', () { + expect( + Validators.validatePassword('12345'), + 'Password must be at least 6 characters', + ); + }); + + test('returns null for valid password', () { + expect(Validators.validatePassword('123456'), null); + expect(Validators.validatePassword('securePassword123'), null); + }); + }); + + group('validatePhone', () { + test('returns error when phone is null', () { + expect(Validators.validatePhone(null), 'Phone number is required'); + }); + + test('returns error when phone is empty', () { + expect(Validators.validatePhone(''), 'Phone number is required'); + }); + + test('returns error for invalid phone format', () { + expect( + Validators.validatePhone('123'), + 'Please enter a valid phone number', + ); + expect( + Validators.validatePhone('abc1234567890'), + 'Please enter a valid phone number', + ); + }); + + test('returns null for valid phone', () { + expect(Validators.validatePhone('1234567890'), null); + expect(Validators.validatePhone('+12345678901'), null); + expect(Validators.validatePhone('+1 234 567 8901'), null); + }); + }); + + group('validateName', () { + test('returns error when name is null', () { + expect(Validators.validateName(null), 'Name is required'); + }); + + test('returns error when name is empty', () { + expect(Validators.validateName(''), 'Name is required'); + }); + + test('returns error when name is too short', () { + expect( + Validators.validateName('A'), + 'Name must be at least 2 characters', + ); + }); + + test('returns null for valid name', () { + expect(Validators.validateName('John'), null); + expect(Validators.validateName('John Doe'), null); + }); + }); + + group('validateCustomerId', () { + test('returns error when customer ID is null', () { + expect(Validators.validateCustomerId(null), 'Customer ID is required'); + }); + + test('returns error when customer ID is empty', () { + expect(Validators.validateCustomerId(''), 'Customer ID is required'); + }); + + test('returns error when customer ID is too short', () { + expect( + Validators.validateCustomerId('1234'), + 'Customer ID must be at least 5 characters', + ); + }); + + test('returns null for valid customer ID', () { + expect(Validators.validateCustomerId('12345'), null); + expect(Validators.validateCustomerId('CUST123456'), null); + }); + }); + + group('validateRequired', () { + test('returns error when value is null', () { + expect( + Validators.validateRequired(null, 'Field'), + 'Field is required', + ); + }); + + test('returns error when value is empty', () { + expect( + Validators.validateRequired('', 'Field'), + 'Field is required', + ); + }); + + test('returns null for valid value', () { + expect(Validators.validateRequired('value', 'Field'), null); + }); + }); + }); +} diff --git a/test/widgets/gender_test.dart b/test/widgets/gender_test.dart new file mode 100644 index 0000000..14717ab --- /dev/null +++ b/test/widgets/gender_test.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:login_page/widgets/gender.dart'; + +void main() { + group('Gender Widget', () { + testWidgets('renders gender options', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Gender(), + ), + ), + ); + + expect(find.text('Gender'), findsOneWidget); + expect(find.text('Male'), findsOneWidget); + expect(find.text('Female'), findsOneWidget); + expect(find.byType(CircleAvatar), findsNWidgets(2)); + }); + + testWidgets('male is selected by default', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Gender(), + ), + ), + ); + + final avatars = tester.widgetList(find.byType(CircleAvatar)); + final firstAvatar = avatars.first; + + // First avatar (male) should have a different background color + expect(firstAvatar.backgroundColor, isNot(Colors.blue[50])); + }); + + testWidgets('can select female', (WidgetTester tester) async { + String? selectedGender; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Gender( + onChanged: (value) { + selectedGender = value; + }, + ), + ), + ), + ); + + // Tap on the second avatar (female) + final avatars = find.byType(CircleAvatar); + await tester.tap(avatars.at(1)); + await tester.pump(); + + expect(selectedGender, 'female'); + }); + + testWidgets('can select male', (WidgetTester tester) async { + String? selectedGender; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Gender( + initialValue: 'female', + onChanged: (value) { + selectedGender = value; + }, + ), + ), + ), + ); + + // Tap on the first avatar (male) + final avatars = find.byType(CircleAvatar); + await tester.tap(avatars.first); + await tester.pump(); + + expect(selectedGender, 'male'); + }); + + testWidgets('respects initial value', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Gender( + initialValue: 'female', + ), + ), + ), + ); + + await tester.pump(); + + // Widget should render without errors + expect(find.byType(Gender), findsOneWidget); + }); + }); +} diff --git a/test/widgets/input_field_test.dart b/test/widgets/input_field_test.dart new file mode 100644 index 0000000..6292fed --- /dev/null +++ b/test/widgets/input_field_test.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:login_page/widgets/input_field.dart'; + +void main() { + group('InputField Widget', () { + testWidgets('renders with label and hint text', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: InputField( + label: 'Test Label', + hintText: 'Test Hint', + ), + ), + ), + ); + + expect(find.text('Test Label'), findsOneWidget); + expect(find.byType(TextFormField), findsOneWidget); + }); + + testWidgets('accepts text input', (WidgetTester tester) async { + final controller = TextEditingController(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: InputField( + label: 'Name', + controller: controller, + ), + ), + ), + ); + + await tester.enterText(find.byType(TextFormField), 'John Doe'); + expect(controller.text, 'John Doe'); + + controller.dispose(); + }); + + testWidgets('validates input when validator is provided', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Form( + child: InputField( + label: 'Email', + validator: (value) { + if (value == null || value.isEmpty) { + return 'Email is required'; + } + return null; + }, + ), + ), + ), + ), + ); + + // Find the form and validate + final formState = tester.state(find.byType(Form)); + expect(formState.validate(), false); + }); + + testWidgets('uses correct keyboard type', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: InputField( + label: 'Email', + keyboardType: TextInputType.emailAddress, + ), + ), + ), + ); + + final textField = tester.widget(find.byType(TextFormField)); + expect(textField.keyboardType, TextInputType.emailAddress); + }); + }); +} diff --git a/test/widgets/membership_test.dart b/test/widgets/membership_test.dart new file mode 100644 index 0000000..1d4ca4d --- /dev/null +++ b/test/widgets/membership_test.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:login_page/widgets/membership.dart'; + +void main() { + group('Membership Widget', () { + testWidgets('renders membership options', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Membership(), + ), + ), + ); + + expect(find.text('Membership'), findsOneWidget); + expect(find.text('Classic'), findsOneWidget); + expect(find.text('Silver'), findsOneWidget); + expect(find.text('Gold'), findsOneWidget); + expect(find.byType(CircleAvatar), findsNWidgets(3)); + }); + + testWidgets('classic is selected by default', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Membership(), + ), + ), + ); + + final avatars = tester.widgetList(find.byType(CircleAvatar)); + final firstAvatar = avatars.first; + + // First avatar (classic) should have a different background color + expect(firstAvatar.backgroundColor, isNot(Colors.blue[50])); + }); + + testWidgets('can select silver', (WidgetTester tester) async { + String? selectedMembership; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Membership( + onChanged: (value) { + selectedMembership = value; + }, + ), + ), + ), + ); + + // Tap on the second avatar (silver) + final avatars = find.byType(CircleAvatar); + await tester.tap(avatars.at(1)); + await tester.pump(); + + expect(selectedMembership, 'silver'); + }); + + testWidgets('can select gold', (WidgetTester tester) async { + String? selectedMembership; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Membership( + onChanged: (value) { + selectedMembership = value; + }, + ), + ), + ), + ); + + // Tap on the third avatar (gold) + final avatars = find.byType(CircleAvatar); + await tester.tap(avatars.at(2)); + await tester.pump(); + + expect(selectedMembership, 'gold'); + }); + + testWidgets('respects initial value', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Membership( + initialValue: 'gold', + ), + ), + ), + ); + + await tester.pump(); + + // Widget should render without errors + expect(find.byType(Membership), findsOneWidget); + }); + + testWidgets('changes selection when tapped', (WidgetTester tester) async { + String? selectedMembership; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Membership( + onChanged: (value) { + selectedMembership = value; + }, + ), + ), + ), + ); + + // Initially classic + expect(selectedMembership, null); + + // Select silver + final avatars = find.byType(CircleAvatar); + await tester.tap(avatars.at(1)); + await tester.pump(); + expect(selectedMembership, 'silver'); + + // Select gold + await tester.tap(avatars.at(2)); + await tester.pump(); + expect(selectedMembership, 'gold'); + + // Select classic + await tester.tap(avatars.first); + await tester.pump(); + expect(selectedMembership, 'classic'); + }); + }); +} diff --git a/test/widgets/password_field_test.dart b/test/widgets/password_field_test.dart new file mode 100644 index 0000000..13f2d48 --- /dev/null +++ b/test/widgets/password_field_test.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:login_page/widgets/password_field.dart'; + +void main() { + group('PasswordField Widget', () { + testWidgets('renders with label', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PasswordField( + label: 'Password', + ), + ), + ), + ); + + expect(find.text('Password'), findsOneWidget); + expect(find.byType(TextFormField), findsOneWidget); + }); + + testWidgets('obscures text by default', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PasswordField( + label: 'Password', + ), + ), + ), + ); + + final textField = tester.widget(find.byType(TextFormField)); + expect(textField.obscureText, true); + }); + + testWidgets('toggles password visibility', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PasswordField( + label: 'Password', + ), + ), + ), + ); + + // Initially obscured + var textField = tester.widget(find.byType(TextFormField)); + expect(textField.obscureText, true); + + // Tap visibility icon + await tester.tap(find.byType(IconButton)); + await tester.pump(); + + // Now visible + textField = tester.widget(find.byType(TextFormField)); + expect(textField.obscureText, false); + + // Tap again to hide + await tester.tap(find.byType(IconButton)); + await tester.pump(); + + // Obscured again + textField = tester.widget(find.byType(TextFormField)); + expect(textField.obscureText, true); + }); + + testWidgets('accepts text input', (WidgetTester tester) async { + final controller = TextEditingController(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: PasswordField( + label: 'Password', + controller: controller, + ), + ), + ), + ); + + await tester.enterText(find.byType(TextFormField), 'secret123'); + expect(controller.text, 'secret123'); + + controller.dispose(); + }); + + testWidgets('validates input when validator is provided', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Form( + child: PasswordField( + label: 'Password', + validator: (value) { + if (value == null || value.isEmpty) { + return 'Password is required'; + } + return null; + }, + ), + ), + ), + ), + ); + + final formState = tester.state(find.byType(Form)); + expect(formState.validate(), false); + }); + }); +}