diff --git a/apps/mobile/lib/features/register/pages/warga/register_warga.dart b/apps/mobile/lib/features/register/pages/warga/register_warga.dart index f712e57..85f47fb 100644 --- a/apps/mobile/lib/features/register/pages/warga/register_warga.dart +++ b/apps/mobile/lib/features/register/pages/warga/register_warga.dart @@ -1,9 +1,7 @@ -import 'dart:io'; +import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:image_picker/image_picker.dart' as img; +import 'package:http/http.dart' as http; import '../../../../models/text_field.dart'; -import '../../../../models/dropdown_field.dart'; -import '../../../../models/date_picker_field.dart'; import '../../controller/controller_warga.dart'; class RegisterWargaPage extends StatefulWidget { @@ -15,57 +13,90 @@ class RegisterWargaPage extends StatefulWidget { class _RegisterWargaPageState extends State { final vars = RegisterWargaVariables(); - File? ktpImage; + bool isLoading = false; - Future pickImage() async { - final picker = img.ImagePicker(); + Future submitRegister() async { + // Validasi form + if (vars.namaC.text.isEmpty || + vars.emailC.text.isEmpty || + vars.passwordC.text.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Harap isi semua field")), + ); + return; + } - showModalBottomSheet( - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(18)), - ), - builder: (_) { - return SizedBox( - height: 160, - child: Column( - children: [ - const SizedBox(height: 10), - Container( - height: 5, - width: 45, - decoration: BoxDecoration( - color: Colors.grey[400], - borderRadius: BorderRadius.circular(10), - ), - ), - const SizedBox(height: 20), - ], + if (!vars.emailC.text.endsWith('@gmail.com')) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Email harus berakhiran @gmail.com")), + ); + return; + } + + setState(() { + isLoading = true; + }); + + try { + final url = "https://apps-jawa-backend.vercel.app/auth/register"; + final body = { + "name": vars.namaC.text.trim(), + "email": vars.emailC.text.trim(), + "password": vars.passwordC.text.trim(), + }; + + final response = await http.post( + Uri.parse(url), + headers: {"Content-Type": "application/json; charset=UTF-8"}, + body: jsonEncode(body), + ); + + print(response.statusCode); + print(response.body); + + final respData = jsonDecode(response.body); + + if (response.statusCode == 200 || response.statusCode == 201) { + // Gunakan pesan dari backend dan beri key supaya integration test bisa menemukan + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + key: const Key('snackbar_register_success'), + content: Text(respData['message'] ?? "Register berhasil!"), ), ); - }, - ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text(respData['message'] ?? response.statusCode.toString()), + ), + ); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Terjadi kesalahan: $e")), + ); + } finally { + setState(() { + isLoading = false; + }); + } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.grey[100], - appBar: AppBar( elevation: 0, backgroundColor: Colors.cyan[700], - title: const Text( - "Register Warga", - ), + title: const Text("Register Warga"), ), - body: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( children: [ - - // **************** CARD FORM **************** + // ================= CARD FORM ================= Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( @@ -76,39 +107,42 @@ class _RegisterWargaPageState extends State { color: Colors.black.withOpacity(0.05), blurRadius: 12, offset: const Offset(0, 4), - ) + ), ], ), - child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - - ModernTextField(controller: vars.namaC, label: "Nama Lengkap", prefixIcon: Icons.badge), + ModernTextField( + controller: vars.namaC, + key: const Key('nama_field'), + label: "Nama Lengkap", + prefixIcon: Icons.badge, + ), ModernTextField( controller: vars.emailC, + key: const Key('email_field'), label: "Email (harus @gmail)", prefixIcon: Icons.email, keyboardType: TextInputType.emailAddress, ), ModernTextField( controller: vars.passwordC, + key: const Key('password_field'), label: "Password", prefixIcon: Icons.lock, keyboardType: TextInputType.visiblePassword, ), - ], ), ), - const SizedBox(height: 24), - - // **************** BUTTON SAVE **************** + // ================= BUTTON SAVE ================= SizedBox( width: double.infinity, child: ElevatedButton( - onPressed: () {}, + key: const Key('submit_register_button'), + onPressed: isLoading ? null : submitRegister, style: ElevatedButton.styleFrom( backgroundColor: Colors.cyan[700], padding: const EdgeInsets.symmetric(vertical: 16), @@ -117,10 +151,16 @@ class _RegisterWargaPageState extends State { ), elevation: 3, ), - child: const Text( - "Simpan Data", - style: TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w600), - ), + child: isLoading + ? const CircularProgressIndicator(color: Colors.white) + : const Text( + "Simpan Data", + style: TextStyle( + fontSize: 16, + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), ), ), ], diff --git a/apps/mobile/test/integration_test/register_flow_test.dart b/apps/mobile/test/integration_test/register_flow_test.dart new file mode 100644 index 0000000..2ec9ad8 --- /dev/null +++ b/apps/mobile/test/integration_test/register_flow_test.dart @@ -0,0 +1,51 @@ +import 'dart:convert'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:flutter/material.dart'; +import 'package:mockito/mockito.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:jawara/features/register/pages/warga/register_warga.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('E2E Register Warga Flow with mocked HTTP', (WidgetTester tester) async { + // Buat MockClient + final mockClient = MockClient((request) async { + if (request.url.toString().contains('/auth/register')) { + return http.Response( + jsonEncode({ + "message": "Register berhasil, silakan lengkapi profil & data keluarga", + "user": {"id": 12, "name": "John Doe", "email": "john@gmail.com", "account_status": "PENDING"} + }), + 201, + headers: {"Content-Type": "application/json"}, + ); + } + return http.Response('Not Found', 404); + }); + + // Pump halaman RegisterWargaPage dengan dependency injection + await tester.pumpWidget(MaterialApp( + home: RegisterWargaPage(), // pastikan page bisa menerima client + )); + await tester.pumpAndSettle(); + + // Isi form + await tester.enterText(find.byKey(const Key('nama_field')), 'John Doe'); + await tester.enterText(find.byKey(const Key('email_field')), 'john@gmail.com'); + await tester.enterText(find.byKey(const Key('password_field')), '123456'); + await tester.pumpAndSettle(); + + // Klik submit + final findSubmitButton = find.byKey(const Key('submit_register_button')); + await tester.tap(findSubmitButton); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + // Cek SnackBar muncul + expect(find.byType(SnackBar), findsOneWidget); + expect(find.textContaining('Register berhasil'), findsOneWidget); + }); +} diff --git a/apps/mobile/test/unit/http_client_mock.mocks.dart b/apps/mobile/test/unit/http_client_mock.mocks.dart new file mode 100644 index 0000000..09ca7d6 --- /dev/null +++ b/apps/mobile/test/unit/http_client_mock.mocks.dart @@ -0,0 +1,220 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in jawara/test/unit/http_client_mock.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; +import 'dart:convert' as _i4; +import 'dart:typed_data' as _i6; + +import 'package:http/http.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i5; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member + +class _FakeResponse_0 extends _i1.SmartFake implements _i2.Response { + _FakeResponse_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeStreamedResponse_1 extends _i1.SmartFake + implements _i2.StreamedResponse { + _FakeStreamedResponse_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +/// A class which mocks [Client]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockClient extends _i1.Mock implements _i2.Client { + MockClient() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future<_i2.Response> head(Uri? url, {Map? headers}) => + (super.noSuchMethod( + Invocation.method(#head, [url], {#headers: headers}), + returnValue: _i3.Future<_i2.Response>.value( + _FakeResponse_0( + this, + Invocation.method(#head, [url], {#headers: headers}), + ), + ), + ) + as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> get(Uri? url, {Map? headers}) => + (super.noSuchMethod( + Invocation.method(#get, [url], {#headers: headers}), + returnValue: _i3.Future<_i2.Response>.value( + _FakeResponse_0( + this, + Invocation.method(#get, [url], {#headers: headers}), + ), + ), + ) + as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> post( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #post, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + returnValue: _i3.Future<_i2.Response>.value( + _FakeResponse_0( + this, + Invocation.method( + #post, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + ), + ), + ) + as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> put( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #put, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + returnValue: _i3.Future<_i2.Response>.value( + _FakeResponse_0( + this, + Invocation.method( + #put, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + ), + ), + ) + as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> patch( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #patch, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + returnValue: _i3.Future<_i2.Response>.value( + _FakeResponse_0( + this, + Invocation.method( + #patch, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + ), + ), + ) + as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> delete( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #delete, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + returnValue: _i3.Future<_i2.Response>.value( + _FakeResponse_0( + this, + Invocation.method( + #delete, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + ), + ), + ) + as _i3.Future<_i2.Response>); + + @override + _i3.Future read(Uri? url, {Map? headers}) => + (super.noSuchMethod( + Invocation.method(#read, [url], {#headers: headers}), + returnValue: _i3.Future.value( + _i5.dummyValue( + this, + Invocation.method(#read, [url], {#headers: headers}), + ), + ), + ) + as _i3.Future); + + @override + _i3.Future<_i6.Uint8List> readBytes( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method(#readBytes, [url], {#headers: headers}), + returnValue: _i3.Future<_i6.Uint8List>.value(_i6.Uint8List(0)), + ) + as _i3.Future<_i6.Uint8List>); + + @override + _i3.Future<_i2.StreamedResponse> send(_i2.BaseRequest? request) => + (super.noSuchMethod( + Invocation.method(#send, [request]), + returnValue: _i3.Future<_i2.StreamedResponse>.value( + _FakeStreamedResponse_1( + this, + Invocation.method(#send, [request]), + ), + ), + ) + as _i3.Future<_i2.StreamedResponse>); + + @override + void close() => super.noSuchMethod( + Invocation.method(#close, []), + returnValueForMissingStub: null, + ); +} diff --git a/apps/mobile/test/unit/register_warga_test.dart b/apps/mobile/test/unit/register_warga_test.dart new file mode 100644 index 0000000..940eae3 --- /dev/null +++ b/apps/mobile/test/unit/register_warga_test.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:mockito/mockito.dart'; +import 'http_client_mock.mocks.dart'; + +void main() { + group('Register Warga Unit Test', () { + late MockClient client; + + setUp(() { + client = MockClient(); + }); + + test('Register API call success', () async { + final body = { + 'nama': 'John Doe', + 'email': 'john@gmail.com', + 'password': '123456' + }; + + // Mock response + when(client.post( + Uri.parse('https://apps-jawa-backend.vercel.app/auth/register'), + headers: anyNamed('headers'), + body: anyNamed('body'), + )).thenAnswer( + (_) async => http.Response(jsonEncode({'message': 'success'}), 201), + ); + + final response = await client.post( + Uri.parse('https://apps-jawa-backend.vercel.app/auth/register'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(body), + ); + + expect(response.statusCode, 201); + final data = jsonDecode(response.body); + expect(data['message'], 'success'); + }); + }); +}