diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4b8980cbbfc8130d30a97caab2d260e5ae7ce461..5f22d9a5476526119d4480cafcc364e70e0cca40 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -26,5 +26,7 @@ + diff --git a/android/settings_aar.gradle b/android/settings_aar.gradle new file mode 100644 index 0000000000000000000000000000000000000000..e7b4def49cb53d9aa04228dd3edb14c9e635e003 --- /dev/null +++ b/android/settings_aar.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/assets/logo/google.png b/assets/logo/google.png new file mode 100644 index 0000000000000000000000000000000000000000..2ef869d64c542bfa652312d35e2fea1757456558 Binary files /dev/null and b/assets/logo/google.png differ diff --git a/assets/logo/google.svg b/assets/logo/google.svg new file mode 100644 index 0000000000000000000000000000000000000000..7f1d5e497be41367280aece388d69ab4c999aab1 --- /dev/null +++ b/assets/logo/google.svg @@ -0,0 +1,37 @@ + + + + + + + + + + +]> + + + + + + + + + + + + + + + diff --git a/lib/main.dart b/lib/main.dart index 4ed6580765258a9ac737ab7948dde4e5fe17c9bc..1b5188ac51e22610224a3546c9d183a4cabd50cd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'flavor/flavor.dart'; +import 'page/login/login.dart'; +import 'page/registrasi/registrasi.dart'; void main() { ApiFlavor.flavor = BuildFlavor.production.toString(); @@ -88,10 +90,28 @@ class _MyHomePageState extends State { Text( 'Hello World', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 40) ), + RaisedButton( + onPressed: () => _navigateToLogin(context), + child: Text('Login'), + ), + RaisedButton( + onPressed: () => _navigateToRegistrasi(context), + child: Text('Registrasi'), + ), ], ), ), ); } + + void _navigateToLogin(BuildContext context) { + final route = MaterialPageRoute(builder: (_) => Login()); + Navigator.of(context).push(route); + } + + void _navigateToRegistrasi(BuildContext context) { + final route = MaterialPageRoute(builder: (_) => Registrasi()); + Navigator.of(context).push(route); + } } diff --git a/lib/network/cookies_interface.dart b/lib/network/cookies_interface.dart deleted file mode 100644 index 5d597734c317a996cbabb13446d1ead6e8565e4c..0000000000000000000000000000000000000000 --- a/lib/network/cookies_interface.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import '../config/strings.dart'; -import 'package:path_provider/path_provider.dart'; - -class CookiesInterface { - - static Future checkCookieFileAvailability() async { - Directory dir; - await getApplicationDocumentsDirectory().then( - (Directory directory) { - dir = directory; - } - ); - File cookieFile = File(dir.path + "/cookies.json"); - bool cookiesExist = cookieFile.existsSync(); - - return cookiesExist; - } - - static Future createCookieFile(Map responseHeaders) async { - try { - String setCookie; - String csrfToken; - String sessionId; - String userKey; - List cookiesList; - Directory dir; - - await getApplicationDocumentsDirectory().then( - (Directory directory) { - dir = directory; - } - ); - File cookieFile = new File(dir.path + "/cookies.json"); - cookieFile.createSync(); - - setCookie = responseHeaders["set-cookie"]; - if (setCookie != null) { - csrfToken = setCookie.split(";")[0].split("=")[1]; - sessionId = setCookie.split(";")[4].split(",")[1].split("=")[1]; - userKey = KEY; - } - - cookiesList = [ - csrfToken, - sessionId, - userKey, - ]; - - cookieFile.writeAsStringSync(json.encode(cookiesList)); - return; - } on Exception catch (e) { - print(e.toString()); - throw e; - } - } - - static Future> getCookieFile() async { - Directory dir; - await getApplicationDocumentsDirectory().then( - (Directory directory) { - dir = directory; - } - ); - File file = File(dir.path + "/cookies.json"); - - dynamic res = file.readAsStringSync(); - return json.decode(res); - } -} \ No newline at end of file diff --git a/lib/network/data/network_model.dart b/lib/network/data/network_model.dart deleted file mode 100644 index 0b49f62ae5e64f811b2e0622b4444ef5843ec64a..0000000000000000000000000000000000000000 --- a/lib/network/data/network_model.dart +++ /dev/null @@ -1,11 +0,0 @@ -class NetworkModel { - var response; - int statusCode; - String errorMessage; - - NetworkModel({ - this.response, - this.statusCode, - this.errorMessage, - }); -} diff --git a/lib/network/network_interface.dart b/lib/network/network_interface.dart deleted file mode 100644 index 698fcbcad74721fedacedca274c1754647acef32..0000000000000000000000000000000000000000 --- a/lib/network/network_interface.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'dart:convert'; -import 'package:ppl_disabilitas/flavor/flavor.dart'; -import 'data/network_model.dart'; -import 'package:http/http.dart' as http; -import '../config/strings.dart'; -import 'cookies_interface.dart'; - -class NetworkInterface { - String url = ApiFlavor.getBaseUrl(); - String key = KEY; - - // POST request - Future post({ - String path, - dynamic bodyParams, - bool isLogin, - }) async { - Map headersJson = await _buildRequestHeader(isLogin); - NetworkModel model; - try { - model = await http.post( - "$url$path", - body: json.encode(bodyParams), - headers: headersJson, - ).then((response) async { - Map responseBody = json.decode(response.body); - if (!isLogin) { - if (responseBody.containsKey("key")) { - setKey(responseBody["key"]); - } - await CookiesInterface.createCookieFile(response.headers); - } - return NetworkModel( - statusCode: response.statusCode, - response: responseBody, - ); - }); - if (model.statusCode >= 400) { - throw Exception(); - } - } on Exception catch (e) { - NetworkModel errorModel = NetworkModel( - statusCode: model.statusCode, - errorMessage: e.toString(), - response: model.response); - model = errorModel; - } - - return model; - } - - // GET request - Future get({ - String path, - bool isLogin, - }) async { - Map headersJson = await _buildRequestHeader(isLogin); - NetworkModel model; - try { - model = await http - .get( - "$url$path", - headers: headersJson, - ) - .then((response) { - dynamic responseBody = json.decode(response.body); - return NetworkModel( - statusCode: response.statusCode, - response: responseBody, - ); - }); - if (model.statusCode >= 400) { - throw Exception(); - } - } on Exception catch (e) { - NetworkModel errorModel = NetworkModel( - statusCode: model.statusCode, - errorMessage: e.toString(), - response: model.response); - model = errorModel; - } - - return model; - } - - // PATCH request - Future patch({ - String path, - dynamic bodyParams, - bool isLogin, - }) async { - NetworkModel model; - Map headersJson = await _buildRequestHeader(isLogin); - try { - model = await http - .patch( - "$url$path", - body: json.encode(bodyParams), - headers: headersJson, - ) - .then((response) { - print(response.body.toString()); - return NetworkModel( - statusCode: response.statusCode, - response: jsonDecode(response.body), - ); - }); - if (model.statusCode >= 400) { - throw Exception(); - } - } on Exception catch (e) { - NetworkModel errorModel = NetworkModel( - statusCode: model.statusCode, - errorMessage: e.toString(), - response: model.response); - model = errorModel; - } - - return model; - } - - Future> _buildRequestHeader(bool isLogin) async { - Map headers = Map(); - headers.putIfAbsent("Content-Type", () => "application/json"); - if (isLogin) { - List cookieFile = await CookiesInterface.getCookieFile(); - setKey(cookieFile[2]); - key = cookieFile[2]; - headers.putIfAbsent("Authorization", () => 'Token $key'); - headers.putIfAbsent("X-CSRFToken", () => cookieFile[0]); - headers.putIfAbsent("Cookie", - () => "csrftoken=${cookieFile[0]};sessionid=${cookieFile[1]}"); - return headers; - } - } -} \ No newline at end of file diff --git a/lib/page/login/login.dart b/lib/page/login/login.dart new file mode 100644 index 0000000000000000000000000000000000000000..b1183db79ea5de4060251adce44fae517b7226cf --- /dev/null +++ b/lib/page/login/login.dart @@ -0,0 +1,96 @@ +import 'dart:async'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:ppl_disabilitas/config/styles.dart'; +import 'package:ppl_disabilitas/utils/customButton.dart'; +import 'package:ppl_disabilitas/utils/customTextField.dart'; +import 'package:ppl_disabilitas/utils/validator.dart'; + +class Login extends StatefulWidget{ + + LoginState createState() => LoginState(); +} + +class LoginState extends State { + final GlobalKey _formKey = GlobalKey(); + String _accountCredential; + String _password; + + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('bisaGO'), + centerTitle: true, + backgroundColor: Color(0xff3A903A), + ), + body: SingleChildScrollView( + child: Form( + key: _formKey, + child: Center( + child: Container( + margin: EdgeInsets.symmetric(horizontal: 20.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: 25), + Text('Masuk ke Akun', + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 25, + ), + ), + SizedBox(height: 20), + CustomTextField( + title: 'Nomor Telepon atau Email', + key: Key('Text Field Akun'), + onSaved: (input) { + _accountCredential = input; + }, + validator: FieldValidator.validateEmail, + ), + CustomTextField( + title: 'Password', + key: Key('Text Field Password'), + obsecure: true, + onSaved: (input) { + _password = input; + }, + validator: FieldValidator.validatePassword, + ), + Container( + margin: EdgeInsets.fromLTRB(0, 30, 0, 10), + alignment: Alignment.center, + child: ButtonTheme( + minWidth: double.infinity, + height: 40, + child: submitButton( + 'Masuk', Color(0xff3A903A), Colors.white, + Color(0xff3A903A), Colors.white, _validateLoginInput), + ) + ), + Container( + child: separator('Atau masuk dengan'), + ), + Container( + margin: EdgeInsets.fromLTRB(0, 10, 0, 30), + alignment: Alignment.center, + child: googleButton(), + ), + ], + ), + ), + ) + ) + ) + ); + } + + void _validateLoginInput() async { + final FormState form = _formKey.currentState; + if(_formKey.currentState.validate()) { + form.save(); + } + } +} \ No newline at end of file diff --git a/lib/page/registrasi/registrasi.dart b/lib/page/registrasi/registrasi.dart new file mode 100644 index 0000000000000000000000000000000000000000..c4e413412dee23d91d25d2cb5f8c58d3390b0a6c --- /dev/null +++ b/lib/page/registrasi/registrasi.dart @@ -0,0 +1,97 @@ +import 'dart:async'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:ppl_disabilitas/config/styles.dart'; +import 'package:ppl_disabilitas/utils/customButton.dart'; +import 'package:ppl_disabilitas/utils/customTextField.dart'; + +class Registrasi extends StatefulWidget { + RegistrasiState createState() => RegistrasiState(); +} + +class RegistrasiState extends State { + final GlobalKey _formKey = GlobalKey(); + + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('bisaGO'), + centerTitle: true, + backgroundColor: Color(0xff3A903A), + ), + body: SingleChildScrollView( + child: Form( + key: _formKey, + child: Center( + child: Container( + margin: EdgeInsets.symmetric(horizontal: 20.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: 25), + Text('Daftar Akun Baru', + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 25, + ), + ), + SizedBox(height: 20), + CustomTextField( + title: 'Nama Lengkap', + key: Key('Text Field Nama'), + validator: (input) => input.isEmpty ? "*Wajib diisi" : null, + ), + CustomTextField( + title: 'Nomor Telepon', + key: Key('Text Field Nomor Telepon'), + validator: (input) => input.isEmpty ? "*Wajib diisi" : null, + ), + CustomTextField( + title: 'Email (Opsional)', + key: Key('Text Field Email'), + ), + CustomTextField( + title: 'Password', + key: Key('Text Field Password'), + validator: (input) => input.isEmpty ? "*Wajib diisi" : null, + ), + CustomTextField( + title: 'Konfirmasi Password', + key: Key('Text Field Konfirmasi Password'), + validator: (input) => input.isEmpty ? "*Wajib diisi" : null, + ), + Container( + margin: EdgeInsets.fromLTRB(0, 30, 0, 10), + alignment: Alignment.center, + child: ButtonTheme( + minWidth: double.infinity, + height: 40, + child: submitButton('Daftar', Color(0xff3A903A), Colors.white, + Color(0xff3A903A), Colors.white, _validateLoginInput), + ) + ), + Container( + child: separator('Atau daftar dengan'), + ), + Container( + margin: EdgeInsets.fromLTRB(0, 10, 0, 30), + alignment: Alignment.center, + child: googleButton(), + ), + ], + ), + ), + ), + ), + ), + ); + } + void _validateLoginInput() async { + final FormState form = _formKey.currentState; + if (_formKey.currentState.validate()) { + form.save(); + } + } +} \ No newline at end of file diff --git a/lib/utils/customButton.dart b/lib/utils/customButton.dart new file mode 100644 index 0000000000000000000000000000000000000000..b5d46eefc4b87f88280896bbce2d5b71920ebab3 --- /dev/null +++ b/lib/utils/customButton.dart @@ -0,0 +1,109 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +Widget submitButton(String text, Color splashColor, Color highlightColor, +Color fillColor, Color textColor, void function()) { + return RaisedButton( + padding: EdgeInsets.symmetric(vertical: 15), + highlightElevation: 0.0, + splashColor: splashColor, + highlightColor: highlightColor, + elevation: 0.0, + color: fillColor, + shape: RoundedRectangleBorder( + borderRadius: new BorderRadius.all(Radius.circular(10)), + ), + child: Text( + text, + style: TextStyle(fontSize: 20, color: textColor, fontWeight: FontWeight.bold), + ), + onPressed: () { + function(); + }, + ); +} + +Widget separator(String text) { + return Row( + children: [ + SizedBox( + width: 20, + ), + Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: Divider( + thickness: 1, + ), + ), + ), + Text(text), + Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: Divider( + thickness: 1, + ), + ), + ), + SizedBox( + width: 20, + ), + ], + ); +} + +Widget googleButton() { + return InkWell( + onTap: () { + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(10)), + border: Border.all(), + boxShadow: [ + BoxShadow( + color: Colors.grey.shade200, + offset: Offset(2, 4), + blurRadius: 5, + spreadRadius: 2) + ], + color: Colors.white, + ), + child: Row( + children: [ + Expanded( + flex: 1, + child: Container( + padding: EdgeInsets.all(15), + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(10), + topLeft: Radius.circular(10)), + ), + alignment: Alignment.center, + child: Image( + image: NetworkImage('https://upload.wikimedia.org/wikipedia/commons/thumb/5/53/Google_%22G%22_Logo.svg/512px-Google_%22G%22_Logo.svg.png'), + ), + ), + ), + Expanded( + flex: 6, + child: Container( + padding: EdgeInsets.symmetric(vertical: 15), + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + bottomRight: Radius.circular(10), + topRight: Radius.circular(10)), + ), + alignment: Alignment.center, + child: Text('Akun Google', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold) + ), + ), + ), + ], + ), + ), + ); +} \ No newline at end of file diff --git a/lib/utils/customTextField.dart b/lib/utils/customTextField.dart new file mode 100644 index 0000000000000000000000000000000000000000..971a1edf05bdcde6801e518afe11cb3d0ba7fed5 --- /dev/null +++ b/lib/utils/customTextField.dart @@ -0,0 +1,66 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class CustomTextField extends StatelessWidget { + CustomTextField( + { this.title, + this.hint, + this.obsecure = false, + this.key, + this.validator, + this.onSaved}); + final String title; + final Key key; + final FormFieldSetter onSaved; + final String hint; + final bool obsecure; + final FormFieldValidator validator; + + @override + Widget build(BuildContext context) { + key: key; + return Container( + margin: EdgeInsets.symmetric(vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle(fontSize: 18), + ), + SizedBox( + height: 10, + ), + TextFormField( + onSaved: onSaved, + validator: validator, + autofocus: true, + obscureText: obsecure, + style: TextStyle( + fontSize: 15, + ), + decoration: InputDecoration( + hintStyle: TextStyle(fontWeight: FontWeight.bold, fontSize: 15), + hintText: hint, + contentPadding: EdgeInsets.all(8.0), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: Theme.of(context).primaryColor, + width: 1, + ), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: Theme.of(context).primaryColor, + width: 1, + ), + ), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/utils/validator.dart b/lib/utils/validator.dart new file mode 100644 index 0000000000000000000000000000000000000000..97649872915f4a95cac43e39946ccf77f509f871 --- /dev/null +++ b/lib/utils/validator.dart @@ -0,0 +1,20 @@ +class FieldValidator { + static String validateEmail(String value) { + Pattern pattern = + r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$'; + RegExp regex = new RegExp(pattern); + if (value.isEmpty) return '*Wajib diisi'; + if (!regex.hasMatch(value)) + return '*Masukkan email yang valid'; + else + return null; + } + + static String validatePassword(String value) { + if (value.isEmpty) return '*Wajib diisi'; + + if (value.length < 7) return '*Password harus lebih dari 6 karakter'; + else + return null; + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 1b41f5ac096ca76a352a87fae0127243843e3ef9..a82dc000b772bc61dea6fcb1028b9306048d3613 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + image_test_utils: ^1.0.0 # For information on the generic Dart part of this file, see the @@ -45,9 +46,7 @@ flutter: # To add assets to your application, add an assets section, like this: # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - + # - assets/logo/google.png # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. @@ -108,6 +107,5 @@ flutter: weight: 700 - asset: assets/fonts/Comfortaa-Bold.ttf weight: 800 - # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages diff --git a/test/login_test.dart b/test/login_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..90b1c685d25b0c6bc006b199f093e1b47f670cb3 --- /dev/null +++ b/test/login_test.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_test_utils/image_test_utils.dart'; + +import 'package:ppl_disabilitas/utils/validator.dart'; +import 'package:ppl_disabilitas/page/login/login.dart'; + + +void main() { + testWidgets('Find Username Text Field', (WidgetTester tester) async { + provideMockedNetworkImages(() async { + final textFieldKey = Key("Text Field Akun"); + await tester.pumpWidget(MaterialApp(home: Login())); + expect(find.byKey(textFieldKey), findsOneWidget); + }); + }); + + testWidgets('Find Password Text Field', (WidgetTester tester) async { + provideMockedNetworkImages(() async { + final textFieldKey = Key("Text Field Password"); + await tester.pumpWidget(MaterialApp(home: Login())); + expect(find.byKey(textFieldKey), findsOneWidget); + }); + }); + + + test('Empty Email Test', () { + var result = FieldValidator.validateEmail(''); + expect(result, '*Wajib diisi'); + }); + + test('Invalid Email Test', () { + var result = FieldValidator.validateEmail('dummy'); + expect(result, '*Masukkan email yang valid'); + }); + + test('Valid Email Test', () { + var result = FieldValidator.validateEmail('dummy@test.com'); + expect(result, null); + }); + + test('Empty Password Test', () { + var result = FieldValidator.validatePassword(''); + expect(result, '*Wajib diisi'); + }); + + test('Invalid Password Test', () { + var result = FieldValidator.validatePassword('ab456'); + expect(result, '*Password harus lebih dari 6 karakter'); + }); + + test('Valid Password Test', () { + var result = FieldValidator.validatePassword('abcd1234'); + expect(result, null); + }); +} \ No newline at end of file diff --git a/test/registrasi_test.dart b/test/registrasi_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..d0abba49410b0263856fcf93184b594cda22c379 --- /dev/null +++ b/test/registrasi_test.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_test_utils/image_test_utils.dart'; + +import 'package:ppl_disabilitas/utils/validator.dart'; +import 'package:ppl_disabilitas/page/registrasi/registrasi.dart'; + + +void main() { + testWidgets('Find Name Text Field', (WidgetTester tester) async { + provideMockedNetworkImages(() async { + final textFieldKey = Key("Text Field Nama"); + await tester.pumpWidget(MaterialApp(home: Registrasi())); + expect(find.byKey(textFieldKey), findsOneWidget); + }); + }); + + testWidgets('Find Phone Number Text Field', (WidgetTester tester) async { + provideMockedNetworkImages(() async { + final textFieldKey = Key("Text Field Nomor Telepon"); + await tester.pumpWidget(MaterialApp(home: Registrasi())); + expect(find.byKey(textFieldKey), findsOneWidget); + }); + }); + + testWidgets('Find Email Text Field', (WidgetTester tester) async { + provideMockedNetworkImages(() async { + final textFieldKey = Key("Text Field Email"); + await tester.pumpWidget(MaterialApp(home: Registrasi())); + expect(find.byKey(textFieldKey), findsOneWidget); + }); + }); + + testWidgets('Find Password Text Field', (WidgetTester tester) async { + provideMockedNetworkImages(() async { + final textFieldKey = Key("Text Field Password"); + await tester.pumpWidget(MaterialApp(home: Registrasi())); + expect(find.byKey(textFieldKey), findsOneWidget); + }); + }); + + testWidgets('Find Password Confirmation Text Field', (WidgetTester tester) async { + provideMockedNetworkImages(() async { + final textFieldKey = Key("Text Field Konfirmasi Password"); + await tester.pumpWidget(MaterialApp(home: Registrasi())); + expect(find.byKey(textFieldKey), findsOneWidget); + }); + }); +} \ No newline at end of file