From fa0b9a7c69b7084e9515c3bdd7c43f12d9088451 Mon Sep 17 00:00:00 2001 From: mariam Date: Wed, 4 Mar 2026 16:34:00 +0200 Subject: [PATCH 1/9] feat(SCRUM-98): integrate Google Maps and add location page with custom markers with static points Initially --- .metadata | 12 +-- android/app/src/main/AndroidManifest.xml | 2 + ios/Runner/AppDelegate.swift | 2 + lib/app/core/router/app_router.dart | 6 ++ lib/app/core/router/route_names.dart | 1 + .../presentation/pages/location_page.dart | 98 +++++++++++++++++++ .../widgets/custom_marker_widget.dart | 27 +++++ pubspec.lock | 32 ++++-- pubspec.yaml | 3 +- 9 files changed, 168 insertions(+), 15 deletions(-) create mode 100644 lib/features/driver_orders_details/presentation/pages/location_page.dart create mode 100644 lib/features/driver_orders_details/presentation/widgets/custom_marker_widget.dart diff --git a/.metadata b/.metadata index 3bfa89d..0691157 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "bd7a4a6b5576630823ca344e3e684c53aa1a0f46" + revision: "9f455d2486bcb28cad87b062475f42edc959f636" channel: "stable" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 - base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 - - platform: web - create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 - base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 + create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + - platform: android + create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 # User provided section diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index eb848a5..6da2591 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,8 @@ android:label="tracking_app" android:name="${applicationName}" android:icon="@mipmap/ic_launcher"> + Bool { + GMSServices.provideAPIKey("AIzaSyBRplvYc2qNr0KuGUndmcJQHiVdBLIO1IA") GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } diff --git a/lib/app/core/router/app_router.dart b/lib/app/core/router/app_router.dart index 4f7f329..f71535d 100644 --- a/lib/app/core/router/app_router.dart +++ b/lib/app/core/router/app_router.dart @@ -5,6 +5,7 @@ import 'package:tracking_app/app/core/router/route_names.dart'; import 'package:tracking_app/features/Onboarding/presentation/pages/onboardingScreen.dart'; import 'package:tracking_app/features/app_sections/presentation/pages/app_sections.dart'; import 'package:tracking_app/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/pages/location_page.dart'; import 'package:tracking_app/features/profile/data/models/driver_model.dart'; import 'package:tracking_app/features/profile/presentation/pages/edit_driver_profile_page.dart'; import 'package:tracking_app/features/profile/presentation/pages/edit_vehicle_page.dart'; @@ -110,5 +111,10 @@ final GoRouter appRouter = GoRouter( return OrderDetailsPage(order: order); }, ), + + GoRoute( + path: RouteNames.locationPage, + builder: (context, state) => LocationPage(), + ), ], ); diff --git a/lib/app/core/router/route_names.dart b/lib/app/core/router/route_names.dart index c435505..36005a4 100644 --- a/lib/app/core/router/route_names.dart +++ b/lib/app/core/router/route_names.dart @@ -16,4 +16,5 @@ abstract class RouteNames { static const ordersDetailsPage = "/ordersDetails"; static const myOrders = "/myOrders"; static const orderDetails = "/orderDetails"; + static const locationPage = "/locationPage"; } diff --git a/lib/features/driver_orders_details/presentation/pages/location_page.dart b/lib/features/driver_orders_details/presentation/pages/location_page.dart new file mode 100644 index 0000000..a235b99 --- /dev/null +++ b/lib/features/driver_orders_details/presentation/pages/location_page.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_marker_widgets/google_maps_marker_widgets.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/widgets/custom_marker_widget.dart'; + +class LocationPage extends StatefulWidget { + const LocationPage({super.key}); + + @override + State createState() => _LocationPageState(); +} + +class _LocationPageState extends State { + final MarkerWidgetsController _controller = MarkerWidgetsController(); + + final LatLng myLocation = LatLng(31.2515108, 29.9842777); + final LatLng destination = LatLng(31.1923215, 29.9162657); + + Set polylines = { + Polyline( + polylineId: PolylineId("route"), + points: [LatLng(31.2515108, 29.9842777), LatLng(31.1923215, 29.9162657)], + color: Colors.pink, + width: 2, + ), + }; + @override + void initState() { + super.initState(); + _addMarkers(); + } + + void _addMarkers() { + _controller.addMarkerWidget( + markerWidget: MarkerWidget( + markerId: const MarkerId("driver_location"), + child: customMarker( + "Your location", + const Icon( + Icons.location_on_outlined, + color: AppColors.pink, + size: 20, + ), + ), + ), + marker: Marker( + markerId: const MarkerId("driver_location"), + position: myLocation, + ), + ); + + _controller.addMarkerWidget( + markerWidget: MarkerWidget( + markerId: const MarkerId("user_location"), + child: customMarker( + "User", + const Icon(Icons.home_outlined, color: AppColors.pink, size: 20), + ), + ), + marker: Marker( + markerId: const MarkerId("user_location"), + position: destination, + ), + ); + } + + CameraPosition routeCameraPosition = const CameraPosition( + target: LatLng(31.2515108, 29.9842777), + zoom: 12, + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Location')), + body: Scaffold( + body: Column( + children: [ + Expanded( + child: MarkerWidgets( + markerWidgetsController: _controller, + builder: (BuildContext context, Set markers) { + return GoogleMap( + initialCameraPosition: routeCameraPosition, + mapType: MapType.normal, + markers: markers, + polylines: polylines, + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/driver_orders_details/presentation/widgets/custom_marker_widget.dart b/lib/features/driver_orders_details/presentation/widgets/custom_marker_widget.dart new file mode 100644 index 0000000..9f90e09 --- /dev/null +++ b/lib/features/driver_orders_details/presentation/widgets/custom_marker_widget.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; + +Widget customMarker(String text, Icon icon) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + decoration: BoxDecoration( + color: AppColors.pink, + borderRadius: BorderRadius.circular(30), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + CircleAvatar(backgroundColor: AppColors.white, radius: 16, child: icon), + const SizedBox(width: 6), + Text( + text, + style: const TextStyle( + color: AppColors.white, + fontWeight: FontWeight.w400, + fontSize: 12, + ), + ), + ], + ), + ); +} diff --git a/pubspec.lock b/pubspec.lock index 4a204ad..0aecf4a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -725,6 +725,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.14+3" + google_maps_marker_widgets: + dependency: "direct main" + description: + name: google_maps_marker_widgets + sha256: "2d1bd2862ba16554cad0425a96ce185718010d0d9e36087bb9df912bd5129c34" + url: "https://pub.dev" + source: hosted + version: "1.1.1" graphs: dependency: transitive description: @@ -977,10 +985,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.16.0" mime: dependency: transitive description: @@ -1245,6 +1253,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + screenshot: + dependency: transitive + description: + name: screenshot + sha256: "63817697a7835e6ce82add4228e15d233b74d42975c143ad8cfe07009fab866b" + url: "https://pub.dev" + source: hosted + version: "3.0.0" shared_preferences: dependency: "direct main" description: @@ -1486,26 +1502,26 @@ packages: dependency: transitive description: name: test - sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" url: "https://pub.dev" source: hosted - version: "1.26.3" + version: "1.26.2" test_api: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.6" test_core: dependency: transitive description: name: test_core - sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" url: "https://pub.dev" source: hosted - version: "0.6.12" + version: "0.6.11" timezone: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1266188..8994fba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,7 +38,8 @@ dependencies: flutter_local_notifications: ^20.0.0 firebase_crashlytics: ^5.0.7 cloud_firestore: ^6.1.2 - cached_network_image: ^3.3.1 + cached_network_image: ^3.3.1 + google_maps_marker_widgets: ^1.1.1 dev_dependencies: bloc_test: ^10.0.0 From b9a7ffc8e7d5a26aad7c3778fd4bb2143ccea5a0 Mon Sep 17 00:00:00 2001 From: mariam Date: Wed, 4 Mar 2026 17:21:46 +0200 Subject: [PATCH 2/9] feat(SCRUM-98): update initial route to location page and implement real-time route fetching with polylines using open street map --- .../presentation/pages/location_page.dart | 46 +++++++++++++++---- pubspec.lock | 10 +++- pubspec.yaml | 3 ++ 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/lib/features/driver_orders_details/presentation/pages/location_page.dart b/lib/features/driver_orders_details/presentation/pages/location_page.dart index a235b99..ab432b6 100644 --- a/lib/features/driver_orders_details/presentation/pages/location_page.dart +++ b/lib/features/driver_orders_details/presentation/pages/location_page.dart @@ -3,6 +3,9 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:google_maps_marker_widgets/google_maps_marker_widgets.dart'; import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; import 'package:tracking_app/features/driver_orders_details/presentation/widgets/custom_marker_widget.dart'; +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:flutter_polyline_points/flutter_polyline_points.dart'; class LocationPage extends StatefulWidget { const LocationPage({super.key}); @@ -17,18 +20,45 @@ class _LocationPageState extends State { final LatLng myLocation = LatLng(31.2515108, 29.9842777); final LatLng destination = LatLng(31.1923215, 29.9162657); - Set polylines = { - Polyline( - polylineId: PolylineId("route"), - points: [LatLng(31.2515108, 29.9842777), LatLng(31.1923215, 29.9162657)], - color: Colors.pink, - width: 2, - ), - }; + Set polylines = {}; + @override void initState() { super.initState(); _addMarkers(); + _getRealRoute(); + } + + Future _getRealRoute() async { + final url = + 'https://router.project-osrm.org/route/v1/driving/${myLocation.longitude},${myLocation.latitude};${destination.longitude},${destination.latitude}?overview=full&geometries=polyline'; + final response = await http.get(Uri.parse(url)); + if (response.statusCode == 200) { + final data = json.decode(response.body); + + if (data['code'] == 'Ok') { + String encodedPolyline = data['routes'][0]['geometry']; + + List result = PolylinePoints.decodePolyline( + encodedPolyline, + ); + + List polylineCoordinates = result + .map((point) => LatLng(point.latitude, point.longitude)) + .toList(); + + setState(() { + polylines = { + Polyline( + polylineId: const PolylineId("real_route"), + color: Colors.pink, + width: 5, + points: polylineCoordinates, + ), + }; + }); + } + } } void _addMarkers() { diff --git a/pubspec.lock b/pubspec.lock index 0aecf4a..b07c9ab 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -563,6 +563,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.33" + flutter_polyline_points: + dependency: "direct main" + description: + name: flutter_polyline_points + sha256: c775fe59fbcf1f925d611c039555c7f58ed6d9411747b7a2915bbd9c5e730a51 + url: "https://pub.dev" + source: hosted + version: "3.1.0" flutter_svg: dependency: "direct main" description: @@ -766,7 +774,7 @@ packages: source: hosted version: "0.15.6" http: - dependency: transitive + dependency: "direct main" description: name: http sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" diff --git a/pubspec.yaml b/pubspec.yaml index 8994fba..e8bf142 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,6 +40,9 @@ dependencies: cloud_firestore: ^6.1.2 cached_network_image: ^3.3.1 google_maps_marker_widgets: ^1.1.1 + http: ^1.1.0 + flutter_polyline_points: ^3.1.0 + dev_dependencies: bloc_test: ^10.0.0 From 932163ed4b03d753e4141513892f2bb0fecd3fb7 Mon Sep 17 00:00:00 2001 From: mariam Date: Wed, 4 Mar 2026 21:46:41 +0200 Subject: [PATCH 3/9] feat(SCRUM-98): enhance location page with dynamic destination setting and real-time route fetching --- .../presentation/pages/location_page.dart | 114 +++++++++++------- 1 file changed, 73 insertions(+), 41 deletions(-) diff --git a/lib/features/driver_orders_details/presentation/pages/location_page.dart b/lib/features/driver_orders_details/presentation/pages/location_page.dart index ab432b6..48a5e16 100644 --- a/lib/features/driver_orders_details/presentation/pages/location_page.dart +++ b/lib/features/driver_orders_details/presentation/pages/location_page.dart @@ -18,31 +18,95 @@ class _LocationPageState extends State { final MarkerWidgetsController _controller = MarkerWidgetsController(); final LatLng myLocation = LatLng(31.2515108, 29.9842777); - final LatLng destination = LatLng(31.1923215, 29.9162657); + LatLng? destination; + final String myAddress = "City Centre Alexandria"; Set polylines = {}; @override void initState() { super.initState(); - _addMarkers(); - _getRealRoute(); + _addMyMarker(); + _setDestinationFromAddress(); + } + + void _addMyMarker() { + _controller.addMarkerWidget( + markerWidget: MarkerWidget( + markerId: const MarkerId("driver_location"), + child: customMarker( + "Your location", + const Icon( + Icons.location_on_outlined, + color: AppColors.pink, + size: 20, + ), + ), + ), + marker: Marker( + markerId: const MarkerId("driver_location"), + position: myLocation, + ), + ); + } + + Future _setDestinationFromAddress() async { + LatLng? result = await getLatLngFromAddress(myAddress); + if (result != null) { + destination = result; + _controller.addMarkerWidget( + markerWidget: MarkerWidget( + markerId: const MarkerId("destination_location"), + child: customMarker( + "Destination", + const Icon(Icons.home_outlined, color: AppColors.pink, size: 20), + ), + ), + marker: Marker( + markerId: const MarkerId("destination_location"), + position: result, + ), + ); + await _getRealRoute(); + } + } + + Future getLatLngFromAddress(String address) async { + final url = + "https://nominatim.openstreetmap.org/search?q=${Uri.encodeComponent("$address, Egypt")}&format=json&limit=1&addressdetails=1"; + + final response = await http.get( + Uri.parse(url), + headers: {"User-Agent": "tracking_app"}, + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + print("<<<<<<<< Geocode response: $data"); + + if (data.isNotEmpty) { + double lat = double.parse(data[0]['lat']); + double lon = double.parse(data[0]['lon']); + return LatLng(lat, lon); + } + } + + return null; } Future _getRealRoute() async { + if (destination == null) return; final url = - 'https://router.project-osrm.org/route/v1/driving/${myLocation.longitude},${myLocation.latitude};${destination.longitude},${destination.latitude}?overview=full&geometries=polyline'; + 'https://router.project-osrm.org/route/v1/driving/${myLocation.longitude},${myLocation.latitude};${destination!.longitude},${destination!.latitude}?overview=full&geometries=polyline'; final response = await http.get(Uri.parse(url)); if (response.statusCode == 200) { final data = json.decode(response.body); - + print('<<<<<<<<< OSRM data: $data'); if (data['code'] == 'Ok') { String encodedPolyline = data['routes'][0]['geometry']; - List result = PolylinePoints.decodePolyline( encodedPolyline, ); - List polylineCoordinates = result .map((point) => LatLng(point.latitude, point.longitude)) .toList(); @@ -57,44 +121,12 @@ class _LocationPageState extends State { ), }; }); + } else { + print("OSRM Error: ${data['code']}"); } } } - void _addMarkers() { - _controller.addMarkerWidget( - markerWidget: MarkerWidget( - markerId: const MarkerId("driver_location"), - child: customMarker( - "Your location", - const Icon( - Icons.location_on_outlined, - color: AppColors.pink, - size: 20, - ), - ), - ), - marker: Marker( - markerId: const MarkerId("driver_location"), - position: myLocation, - ), - ); - - _controller.addMarkerWidget( - markerWidget: MarkerWidget( - markerId: const MarkerId("user_location"), - child: customMarker( - "User", - const Icon(Icons.home_outlined, color: AppColors.pink, size: 20), - ), - ), - marker: Marker( - markerId: const MarkerId("user_location"), - position: destination, - ), - ); - } - CameraPosition routeCameraPosition = const CameraPosition( target: LatLng(31.2515108, 29.9842777), zoom: 12, From ff9ff1056c5bd8d81cef15b01c55bc5e1c86a56d Mon Sep 17 00:00:00 2001 From: mariam Date: Sun, 8 Mar 2026 00:37:00 +0200 Subject: [PATCH 4/9] feat(SCRUM-98): refactor methods and markers in google maps --- assets/images/driver_location.png | Bin 0 -> 4720 bytes assets/images/flowery_location.png | Bin 0 -> 5877 bytes assets/images/user_location.png | Bin 0 -> 4184 bytes lib/app/config/di/di.config.dart | 32 +- lib/app/core/router/app_router.dart | 7 +- lib/app/core/ui_helper/assets/images.dart | 3 + .../order_details_remote_datasource_impl.dart | 95 ++++- .../order_details_remote_datasource.dart | 10 + .../data/mapper/drivers_dto_mapper.dart | 20 + .../data/models/drivers_dto.dart | 55 +++ .../data/repos/order_details_repo_impl.dart | 41 ++- .../domain/models/drivers_model.dart | 22 ++ .../domain/repos/order_details_repo.dart | 12 +- .../usecases/get_driver_data_usecase.dart | 13 + .../usecases/get_order_details_usecase.dart | 3 +- .../domain/usecases/location_usecase.dart | 22 ++ .../manager/order_details_cubit.dart | 101 +++-- .../manager/order_details_states.dart | 26 +- .../pages/drivers_orders_details_page.dart | 29 +- .../presentation/pages/location_page.dart | 344 ++++++++++++------ .../widgets/custom_marker_widget.dart | 27 -- pubspec.lock | 18 +- pubspec.yaml | 2 - 23 files changed, 654 insertions(+), 228 deletions(-) create mode 100644 assets/images/driver_location.png create mode 100644 assets/images/flowery_location.png create mode 100644 assets/images/user_location.png create mode 100644 lib/features/driver_orders_details/data/mapper/drivers_dto_mapper.dart create mode 100644 lib/features/driver_orders_details/data/models/drivers_dto.dart create mode 100644 lib/features/driver_orders_details/domain/models/drivers_model.dart create mode 100644 lib/features/driver_orders_details/domain/usecases/get_driver_data_usecase.dart create mode 100644 lib/features/driver_orders_details/domain/usecases/location_usecase.dart delete mode 100644 lib/features/driver_orders_details/presentation/widgets/custom_marker_widget.dart diff --git a/assets/images/driver_location.png b/assets/images/driver_location.png new file mode 100644 index 0000000000000000000000000000000000000000..4925894ffd987725eed17a8f11324ee883872670 GIT binary patch literal 4720 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 LNkvXXu0mjf=>#6; literal 0 HcmV?d00001 diff --git a/assets/images/user_location.png b/assets/images/user_location.png new file mode 100644 index 0000000000000000000000000000000000000000..1bca739a76870221c5cc3c21e999e0366afaffc0 GIT binary patch literal 4184 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: 'firestore'), ), ); - gh.lazySingleton<_i697.ProfileLocalDataSource>( - () => _i495.ProfileLocalDataSourceImpl(gh<_i603.AuthStorage>()), - ); gh.factory<_i114.OrderDetailsRemoteDatasource>( () => _i860.OrderDetailsRemoteDatasourceImpl( firestore: gh<_i974.FirebaseFirestore>(), + dio: gh<_i361.Dio>(), ), ); + gh.lazySingleton<_i697.ProfileLocalDataSource>( + () => _i495.ProfileLocalDataSourceImpl(gh<_i603.AuthStorage>()), + ); gh.lazySingleton<_i890.ApiClient>( () => networkModule.authApiClient(gh<_i361.Dio>()), ); @@ -158,7 +163,13 @@ extension GetItInjectableX on _i174.GetIt { () => _i583.MyOrdersRemoteDataSourceImp(gh<_i890.ApiClient>()), ); gh.factory<_i313.OrderDetailsRepo>( - () => _i55.OrderDetailsRepoImpl(gh<_i114.OrderDetailsRemoteDatasource>()), + () => _i55.OrderDetailsRepoImpl( + gh<_i114.OrderDetailsRemoteDatasource>(), + gh<_i603.AuthStorage>(), + ), + ); + gh.factory<_i449.LocationUsecase>( + () => _i449.LocationUsecase(gh<_i313.OrderDetailsRepo>()), ); gh.factory<_i919.MyOrdersRepo>( () => _i754.MyOrdersRepoImpl(gh<_i466.MyOrdersRemoteDataSource>()), @@ -166,6 +177,9 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i335.GetOrderUseCase>( () => _i335.GetOrderUseCase(gh<_i919.MyOrdersRepo>()), ); + gh.factory<_i883.GetDriverDataUsecase>( + () => _i883.GetDriverDataUsecase(repo: gh<_i313.OrderDetailsRepo>()), + ); gh.factory<_i1045.GetOrderDetailsUsecase>( () => _i1045.GetOrderDetailsUsecase(repo: gh<_i313.OrderDetailsRepo>()), ); @@ -178,12 +192,16 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i708.AuthRemoteDataSource>( () => _i777.AuthRemoteDataSourceImpl(gh<_i890.ApiClient>()), ); + gh.factory<_i375.OrderDetailsCubit>( + () => _i375.OrderDetailsCubit( + gh<_i1045.GetOrderDetailsUsecase>(), + gh<_i883.GetDriverDataUsecase>(), + gh<_i449.LocationUsecase>(), + ), + ); gh.factory<_i712.AuthRepo>( () => _i566.AuthRepoImpl(gh<_i708.AuthRemoteDataSource>()), ); - gh.factory<_i375.OrderDetailsCubit>( - () => _i375.OrderDetailsCubit(gh<_i1045.GetOrderDetailsUsecase>()), - ); gh.factory<_i991.ChangePasswordUsecase>( () => _i991.ChangePasswordUsecase(gh<_i712.AuthRepo>()), ); diff --git a/lib/app/core/router/app_router.dart b/lib/app/core/router/app_router.dart index f71535d..3fae5ef 100644 --- a/lib/app/core/router/app_router.dart +++ b/lib/app/core/router/app_router.dart @@ -23,7 +23,7 @@ import 'package:tracking_app/features/auth/presentation/verify_reset/manger/cubi import 'package:tracking_app/features/auth/presentation/verify_reset/pages/verify_reset_page.dart'; final GoRouter appRouter = GoRouter( - initialLocation: RouteNames.onboarding, + initialLocation: RouteNames.ordersDetailsPage, routes: [ GoRoute( path: RouteNames.changePassword, @@ -114,7 +114,10 @@ final GoRouter appRouter = GoRouter( GoRoute( path: RouteNames.locationPage, - builder: (context, state) => LocationPage(), + builder: (context, state) { + final locationType = state.extra as String; + return LocationPage(locationType: locationType); + }, ), ], ); diff --git a/lib/app/core/ui_helper/assets/images.dart b/lib/app/core/ui_helper/assets/images.dart index 8b1029b..2bb488a 100644 --- a/lib/app/core/ui_helper/assets/images.dart +++ b/lib/app/core/ui_helper/assets/images.dart @@ -13,4 +13,7 @@ class Assets { static const String imagesFilter = "assets/images/filter.png"; static const String imagesFlower = "assets/images/Flower.svg"; static const String delete = "assets/images/delete.png"; + static const String driverLocation = "assets/images/driver_location.png"; + static const String userLocation = "assets/images/user_location.png"; + static const String floweryLocation = "assets/images/flowery_location.png"; } diff --git a/lib/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart b/lib/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart index f893869..46484dc 100644 --- a/lib/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart +++ b/lib/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart @@ -1,14 +1,21 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_polyline_points/flutter_polyline_points.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:injectable/injectable.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/drivers_dto.dart'; import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; @Injectable(as: OrderDetailsRemoteDatasource) class OrderDetailsRemoteDatasourceImpl implements OrderDetailsRemoteDatasource { final FirebaseFirestore _firestore; - OrderDetailsRemoteDatasourceImpl({required FirebaseFirestore firestore}) - : _firestore = firestore; + final Dio dio; + OrderDetailsRemoteDatasourceImpl({ + required FirebaseFirestore firestore, + required this.dio, + }) : _firestore = firestore; @override ApiResult> getOrderStream(String orderId) { @@ -29,4 +36,88 @@ class OrderDetailsRemoteDatasourceImpl implements OrderDetailsRemoteDatasource { return ErrorApiResult>(error: e.toString()); } } + + @override + ApiResult> getDriverData(String driverId) { + try { + final stream = _firestore + .collection('drivers') + .doc(driverId) + .snapshots() + .where((snapshot) => snapshot.exists && snapshot.data() != null) + .map((snapshot) { + return DriverDataDto.fromJson( + snapshot.data() as Map, + ); + }); + return SuccessApiResult>(data: stream); + } catch (e) { + return ErrorApiResult>(error: e.toString()); + } + } + + @override + Future> getLatLngFromAddress(String address) async { + try { + final response = await dio.get( + "https://nominatim.openstreetmap.org/search", + queryParameters: { + "q": "$address, Egypt", + "format": "json", + "limit": 1, + "addressdetails": 1, + }, + options: Options(headers: {"User-Agent": "tracking_app"}), + ); + + final data = response.data; + + print("<<<<<<<< Geocode response: $data"); + + if (response.statusCode == 200 && data != null && data.isNotEmpty) { + double lat = double.parse(data[0]['lat']); + double lon = double.parse(data[0]['lon']); + + return SuccessApiResult(data: LatLng(lat, lon)); + } + return SuccessApiResult(data: null); + } catch (e) { + return ErrorApiResult(error: e.toString()); + } + } + + @override + Future>> getRealRoute( + LatLng myLocation, + LatLng destination, + ) async { + try { + final response = await dio.get( + "https://router.project-osrm.org/route/v1/driving/" + "${myLocation.longitude},${myLocation.latitude};" + "${destination.longitude},${destination.latitude}", + queryParameters: {"overview": "full", "geometries": "polyline"}, + ); + + final data = response.data; + + if (response.statusCode == 200 && data['code'] == 'Ok') { + String encodedPolyline = data['routes'][0]['geometry']; + + List result = PolylinePoints.decodePolyline( + encodedPolyline, + ); + + List polylineCoordinates = result + .map((point) => LatLng(point.latitude, point.longitude)) + .toList(); + + return SuccessApiResult>(data: polylineCoordinates); + } + + return ErrorApiResult>(error: 'No route found'); + } catch (e) { + return ErrorApiResult>(error: e.toString()); + } + } } diff --git a/lib/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart b/lib/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart index 49bbd41..a161c43 100644 --- a/lib/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart +++ b/lib/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart @@ -1,6 +1,16 @@ +import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/drivers_dto.dart'; import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; abstract class OrderDetailsRemoteDatasource { ApiResult> getOrderStream(String orderId); + ApiResult> getDriverData(String driverId); + + Future> getLatLngFromAddress(String address); + + Future>> getRealRoute( + LatLng myLocation, + LatLng destination, + ); } diff --git a/lib/features/driver_orders_details/data/mapper/drivers_dto_mapper.dart b/lib/features/driver_orders_details/data/mapper/drivers_dto_mapper.dart new file mode 100644 index 0000000..5d63e20 --- /dev/null +++ b/lib/features/driver_orders_details/data/mapper/drivers_dto_mapper.dart @@ -0,0 +1,20 @@ +import 'package:tracking_app/features/driver_orders_details/data/models/drivers_dto.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; + +extension DriversDtoMapper on DriverDataDto { + DriverDataModel toDriversModel() { + return DriverDataModel( + name: name, + phone: phone, + id: id, + deviceToken: deviceToken, + currentLocation: currentLocation.toDriverLocationModel(), + ); + } +} + +extension DriverLocationDtoMapper on DriverLocationDto { + DriverLocationModel toDriverLocationModel() { + return DriverLocationModel(lat: lat, lng: lng); + } +} diff --git a/lib/features/driver_orders_details/data/models/drivers_dto.dart b/lib/features/driver_orders_details/data/models/drivers_dto.dart new file mode 100644 index 0000000..bdde436 --- /dev/null +++ b/lib/features/driver_orders_details/data/models/drivers_dto.dart @@ -0,0 +1,55 @@ +class DriverDataDto { + final String id; + final String name; + final String phone; + final String deviceToken; + final DriverLocationDto currentLocation; + + DriverDataDto({ + required this.id, + required this.name, + required this.phone, + required this.deviceToken, + required this.currentLocation, + }); + + factory DriverDataDto.fromJson(Map json) { + return DriverDataDto( + id: json['id'] ?? '', + name: json['name'] ?? '', + phone: json['phone'] ?? '', + deviceToken: json['deviceToken'] ?? '', + currentLocation: DriverLocationDto.fromJson( + json['currentLocation'] ?? {}, + ), + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'phone': phone, + 'deviceToken': deviceToken, + 'currentLocation': currentLocation.toJson(), + }; + } +} + +class DriverLocationDto { + final double lat; + final double lng; + + DriverLocationDto({required this.lat, required this.lng}); + + factory DriverLocationDto.fromJson(Map json) { + return DriverLocationDto( + lat: (json['lat'] ?? 0).toDouble(), + lng: (json['lng'] ?? 0).toDouble(), + ); + } + + Map toJson() { + return {'lat': lat, 'lng': lng}; + } +} diff --git a/lib/features/driver_orders_details/data/repos/order_details_repo_impl.dart b/lib/features/driver_orders_details/data/repos/order_details_repo_impl.dart index 37251f2..5c8d8cb 100644 --- a/lib/features/driver_orders_details/data/repos/order_details_repo_impl.dart +++ b/lib/features/driver_orders_details/data/repos/order_details_repo_impl.dart @@ -1,18 +1,28 @@ +import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart'; +import 'package:tracking_app/features/driver_orders_details/data/mapper/drivers_dto_mapper.dart'; import 'package:tracking_app/features/driver_orders_details/data/mapper/order_dto_mapper.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/drivers_dto.dart'; import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; import 'package:tracking_app/features/driver_orders_details/domain/repos/order_details_repo.dart'; @Injectable(as: OrderDetailsRepo) class OrderDetailsRepoImpl implements OrderDetailsRepo { final OrderDetailsRemoteDatasource _remoteDataSource; - OrderDetailsRepoImpl(this._remoteDataSource); + final AuthStorage _authStorage; + OrderDetailsRepoImpl(this._remoteDataSource, this._authStorage); @override - ApiResult> getOrderDetails(String orderId) { + Future>> getOrderDetails() async { + final orderId = await _authStorage.getOrderId(); + if (orderId == null) { + return ErrorApiResult>(error: "No order ID found"); + } final result = _remoteDataSource.getOrderStream(orderId); switch (result) { @@ -24,4 +34,31 @@ class OrderDetailsRepoImpl implements OrderDetailsRepo { return ErrorApiResult>(error: result.error); } } + + @override + ApiResult> getDriverData(String driverId) { + final result = _remoteDataSource.getDriverData(driverId); + + switch (result) { + case SuccessApiResult>(): + return SuccessApiResult>( + data: result.data.map((dto) => dto.toDriversModel()), + ); + case ErrorApiResult>(): + return ErrorApiResult>(error: result.error); + } + } + + @override + Future> getLatLngFromAddress(String address) { + return _remoteDataSource.getLatLngFromAddress(address); + } + + @override + Future>> getRealRoute( + LatLng myLocation, + LatLng destination, + ) { + return _remoteDataSource.getRealRoute(myLocation, destination); + } } diff --git a/lib/features/driver_orders_details/domain/models/drivers_model.dart b/lib/features/driver_orders_details/domain/models/drivers_model.dart new file mode 100644 index 0000000..e8657cd --- /dev/null +++ b/lib/features/driver_orders_details/domain/models/drivers_model.dart @@ -0,0 +1,22 @@ +class DriverDataModel { + final String id; + final String name; + final String phone; + final String deviceToken; + final DriverLocationModel currentLocation; + + DriverDataModel({ + required this.id, + required this.name, + required this.phone, + required this.deviceToken, + required this.currentLocation, + }); +} + +class DriverLocationModel { + final double lat; + final double lng; + + DriverLocationModel({required this.lat, required this.lng}); +} diff --git a/lib/features/driver_orders_details/domain/repos/order_details_repo.dart b/lib/features/driver_orders_details/domain/repos/order_details_repo.dart index 942beaa..1e33a3c 100644 --- a/lib/features/driver_orders_details/domain/repos/order_details_repo.dart +++ b/lib/features/driver_orders_details/domain/repos/order_details_repo.dart @@ -1,6 +1,16 @@ +import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; abstract class OrderDetailsRepo { - ApiResult> getOrderDetails(String orderId); + Future>> getOrderDetails(); + ApiResult> getDriverData(String driverId); + + Future> getLatLngFromAddress(String address); + + Future>> getRealRoute( + LatLng myLocation, + LatLng destination, + ); } diff --git a/lib/features/driver_orders_details/domain/usecases/get_driver_data_usecase.dart b/lib/features/driver_orders_details/domain/usecases/get_driver_data_usecase.dart new file mode 100644 index 0000000..0680a58 --- /dev/null +++ b/lib/features/driver_orders_details/domain/usecases/get_driver_data_usecase.dart @@ -0,0 +1,13 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/repos/order_details_repo.dart'; + +@injectable +class GetDriverDataUsecase { + OrderDetailsRepo _repo; + GetDriverDataUsecase({required OrderDetailsRepo repo}) : _repo = repo; + + ApiResult> call(String driverId) => + _repo.getDriverData(driverId); +} diff --git a/lib/features/driver_orders_details/domain/usecases/get_order_details_usecase.dart b/lib/features/driver_orders_details/domain/usecases/get_order_details_usecase.dart index e3253c1..37fe21e 100644 --- a/lib/features/driver_orders_details/domain/usecases/get_order_details_usecase.dart +++ b/lib/features/driver_orders_details/domain/usecases/get_order_details_usecase.dart @@ -8,6 +8,5 @@ class GetOrderDetailsUsecase { OrderDetailsRepo _repo; GetOrderDetailsUsecase({required OrderDetailsRepo repo}) : _repo = repo; - ApiResult> call(String orderId) => - _repo.getOrderDetails(orderId); + Future>> call() => _repo.getOrderDetails(); } diff --git a/lib/features/driver_orders_details/domain/usecases/location_usecase.dart b/lib/features/driver_orders_details/domain/usecases/location_usecase.dart new file mode 100644 index 0000000..c881b2c --- /dev/null +++ b/lib/features/driver_orders_details/domain/usecases/location_usecase.dart @@ -0,0 +1,22 @@ +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/repos/order_details_repo.dart'; + +@injectable +class LocationUsecase { + final OrderDetailsRepo _repo; + + LocationUsecase(this._repo); + + Future> getAddress(String address) { + return _repo.getLatLngFromAddress(address); + } + + Future>> getRealRoute( + LatLng driverLocation, + LatLng destination, + ) { + return _repo.getRealRoute(driverLocation, destination); + } +} diff --git a/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart b/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart index 224458f..e6f3bd0 100644 --- a/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart +++ b/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart @@ -1,58 +1,93 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:injectable/injectable.dart'; -import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; import 'package:tracking_app/app/config/base_state/base_state.dart'; -import 'package:tracking_app/app/config/di/di.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/usecases/get_driver_data_usecase.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/usecases/location_usecase.dart'; import '../../domain/usecases/get_order_details_usecase.dart'; import 'order_details_states.dart'; @injectable class OrderDetailsCubit extends Cubit { final GetOrderDetailsUsecase _getOrderDetailsUsecase; - StreamSubscription? _subscription; - final _authStorage = getIt(); + final GetDriverDataUsecase _getDriverDataUsecase; + final LocationUsecase _locationUsecase; + StreamSubscription? _orderSubscription; + StreamSubscription? _driverSubscription; - OrderDetailsCubit(this._getOrderDetailsUsecase) : super(OrderDetailsStates()); + OrderDetailsCubit( + this._getOrderDetailsUsecase, + this._getDriverDataUsecase, + this._locationUsecase, + ) : super(OrderDetailsStates()); void getOrderDetails() async { emit(state.copyWith(data: Resource.loading())); - _subscription?.cancel(); - - try { - final orderId = await _authStorage.getOrderId(); - if (orderId == null || orderId.isEmpty) { - emit(state.copyWith(data: Resource.error('Order ID not found'))); - return; - } - final result = _getOrderDetailsUsecase.call(orderId); - - if (result is SuccessApiResult>) { - _subscription = result.data.listen( - (order) { - emit(state.copyWith(data: Resource.success(order))); - }, - onError: (error) { - emit(state.copyWith(data: Resource.error(error.toString()))); - }, - ); - } else if (result is ErrorApiResult>) { - emit(state.copyWith(data: Resource.error(result.error))); - } - } catch (e) { - emit( - state.copyWith( - data: Resource.error("Error retrieving order details: $e"), - ), + _orderSubscription?.cancel(); + + final result = await _getOrderDetailsUsecase.call(); + + if (result is SuccessApiResult>) { + _orderSubscription = result.data.listen( + (order) { + emit(state.copyWith(data: Resource.success(order))); + if (order.driverId.isNotEmpty) { + getDriverData(order.driverId); + } + }, + onError: (error) { + emit(state.copyWith(data: Resource.error(error.toString()))); + }, ); + } else if (result is ErrorApiResult>) { + emit(state.copyWith(data: Resource.error(result.error))); + } + } + + void getDriverData(String driverId) async { + emit(state.copyWith(driverData: Resource.loading())); + _driverSubscription?.cancel(); + final result = _getDriverDataUsecase.call(driverId); + if (result is SuccessApiResult>) { + _driverSubscription = result.data.listen((driver) async { + emit(state.copyWith(driverData: Resource.success(driver))); + }); + } else if (result is ErrorApiResult>) { + emit(state.copyWith(driverData: Resource.error(result.error))); + } + } + + Future setDestinationFromAddress( + String address, + LatLng driverLocation, + ) async { + final result = await _locationUsecase.getAddress(address); + if (result is SuccessApiResult && result.data != null) { + emit(state.copyWith(destination: result.data)); + await getRoute(driverLocation); + } + } + + Future getRoute(LatLng driverLocation) async { + if (state.destination == null) return; + + final result = await _locationUsecase.getRealRoute( + driverLocation, + state.destination!, + ); + if (result is SuccessApiResult>) { + emit(state.copyWith(polylines: result.data)); } } @override Future close() { - _subscription?.cancel(); + _orderSubscription?.cancel(); + _driverSubscription?.cancel(); return super.close(); } } diff --git a/lib/features/driver_orders_details/presentation/manager/order_details_states.dart b/lib/features/driver_orders_details/presentation/manager/order_details_states.dart index 267a1ca..8adc425 100644 --- a/lib/features/driver_orders_details/presentation/manager/order_details_states.dart +++ b/lib/features/driver_orders_details/presentation/manager/order_details_states.dart @@ -1,11 +1,31 @@ +import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; class OrderDetailsStates { final Resource? data; - const OrderDetailsStates({this.data}); + final Resource? driverData; + final LatLng? destination; + final List? polylines; + const OrderDetailsStates({ + this.data, + this.driverData, + this.destination, + this.polylines, + }); - OrderDetailsStates copyWith({Resource? data}) { - return OrderDetailsStates(data: data ?? this.data); + OrderDetailsStates copyWith({ + Resource? data, + Resource? driverData, + LatLng? destination, + List? polylines, + }) { + return OrderDetailsStates( + data: data ?? this.data, + driverData: driverData ?? this.driverData, + destination: destination ?? this.destination, + polylines: polylines ?? this.polylines, + ); } } diff --git a/lib/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart b/lib/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart index aa8ba57..6623515 100644 --- a/lib/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart +++ b/lib/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart @@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:tracking_app/app/config/base_state/base_state.dart'; import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/app/core/router/route_names.dart'; import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; import 'package:tracking_app/app/core/values/paths.dart'; import 'package:tracking_app/app/core/widgets/custom_button.dart'; @@ -111,18 +112,28 @@ class DriversOrdersDetailsPage extends StatelessWidget { const SizedBox(height: 24), SectionTitle(title: LocaleKeys.pickupAddress.tr()), - AddressCard( - title: order?.orderDetails.pickupAddress.name ?? '', - address: order?.orderDetails.pickupAddress.address ?? '', - imagePath: AppPaths.flowerLogo, + InkWell( + onTap: () => context.push( + RouteNames.locationPage, + extra: 'pickup', + ), + child: AddressCard( + title: order?.orderDetails.pickupAddress.name ?? '', + address: + order?.orderDetails.pickupAddress.address ?? '', + imagePath: AppPaths.flowerLogo, + ), ), const SizedBox(height: 16), SectionTitle(title: LocaleKeys.userAddress.tr()), - - AddressCard( - title: order?.userAddress.name ?? '', - address: order?.userAddress.address ?? '', - imagePath: AppPaths.flowerLogo, + InkWell( + onTap: () => + context.push(RouteNames.locationPage, extra: 'user'), + child: AddressCard( + title: order?.userAddress.name ?? '', + address: order?.userAddress.address ?? '', + imagePath: AppPaths.flowerLogo, + ), ), const SizedBox(height: 24), diff --git a/lib/features/driver_orders_details/presentation/pages/location_page.dart b/lib/features/driver_orders_details/presentation/pages/location_page.dart index 48a5e16..2f5f9f9 100644 --- a/lib/features/driver_orders_details/presentation/pages/location_page.dart +++ b/lib/features/driver_orders_details/presentation/pages/location_page.dart @@ -1,158 +1,260 @@ +import 'dart:typed_data'; +import 'package:flutter/services.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; -import 'package:google_maps_marker_widgets/google_maps_marker_widgets.dart'; +import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/app/core/ui_helper/assets/images.dart'; import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; -import 'package:tracking_app/features/driver_orders_details/presentation/widgets/custom_marker_widget.dart'; -import 'dart:convert'; -import 'package:http/http.dart' as http; -import 'package:flutter_polyline_points/flutter_polyline_points.dart'; +import 'package:tracking_app/app/core/values/paths.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_cubit.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_states.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/widgets/address_card.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/widgets/section_title.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; class LocationPage extends StatefulWidget { - const LocationPage({super.key}); + final String locationType; + const LocationPage({super.key, required this.locationType}); @override State createState() => _LocationPageState(); } class _LocationPageState extends State { - final MarkerWidgetsController _controller = MarkerWidgetsController(); + late OrderDetailsCubit cubit; - final LatLng myLocation = LatLng(31.2515108, 29.9842777); LatLng? destination; - final String myAddress = "City Centre Alexandria"; - Set polylines = {}; + Set markers = {}; + BitmapDescriptor? driverIcon; + BitmapDescriptor? destinationIcon; + @override void initState() { super.initState(); - _addMyMarker(); - _setDestinationFromAddress(); + cubit = getIt(); + cubit.getOrderDetails(); + loadMarkerIcons(); } - void _addMyMarker() { - _controller.addMarkerWidget( - markerWidget: MarkerWidget( - markerId: const MarkerId("driver_location"), - child: customMarker( - "Your location", - const Icon( - Icons.location_on_outlined, - color: AppColors.pink, - size: 20, - ), - ), - ), - marker: Marker( + Future getMarkerIcon(String path) async { + final ByteData data = await DefaultAssetBundle.of(context).load(path); + final Uint8List bytes = data.buffer.asUint8List(); + return BitmapDescriptor.fromBytes(bytes); + } + + Future loadMarkerIcons() async { + driverIcon = await getMarkerIcon(Assets.driverLocation); + + destinationIcon = await getMarkerIcon( + widget.locationType == 'pickup' + ? Assets.floweryLocation + : Assets.userLocation, + ); + setState(() {}); + } + + void driverMarker(LatLng driverLocation) { + markers.add( + Marker( markerId: const MarkerId("driver_location"), - position: myLocation, + position: driverLocation, + icon: driverIcon ?? BitmapDescriptor.defaultMarker, + infoWindow: const InfoWindow(title: "Your location"), ), ); } - Future _setDestinationFromAddress() async { - LatLng? result = await getLatLngFromAddress(myAddress); - if (result != null) { - destination = result; - _controller.addMarkerWidget( - markerWidget: MarkerWidget( - markerId: const MarkerId("destination_location"), - child: customMarker( - "Destination", - const Icon(Icons.home_outlined, color: AppColors.pink, size: 20), - ), - ), - marker: Marker( - markerId: const MarkerId("destination_location"), - position: result, - ), - ); - await _getRealRoute(); - } + void destinationMarker(LatLng destinationLocation) { + markers.add( + Marker( + markerId: const MarkerId("destination_location"), + position: destinationLocation, + icon: destinationIcon ?? BitmapDescriptor.defaultMarker, + infoWindow: const InfoWindow(title: "Destination"), + ), + ); } - Future getLatLngFromAddress(String address) async { - final url = - "https://nominatim.openstreetmap.org/search?q=${Uri.encodeComponent("$address, Egypt")}&format=json&limit=1&addressdetails=1"; + @override + Widget build(BuildContext context) { + return Scaffold( + body: BlocProvider( + create: (context) => cubit, + child: BlocConsumer( + listener: (context, state) { + final driver = state.driverData?.data; + final order = state.data?.data; + if (driver == null || order == null) return; - final response = await http.get( - Uri.parse(url), - headers: {"User-Agent": "tracking_app"}, - ); + final driverLocation = LatLng( + driver.currentLocation.lat, + driver.currentLocation.lng, + ); + String address; - if (response.statusCode == 200) { - final data = json.decode(response.body); - print("<<<<<<<< Geocode response: $data"); + if (widget.locationType == 'pickup') { + address = order.orderDetails.pickupAddress.address; + } else { + address = order.userAddress.address; + } - if (data.isNotEmpty) { - double lat = double.parse(data[0]['lat']); - double lon = double.parse(data[0]['lon']); - return LatLng(lat, lon); - } - } + print( + '<<<<<<< driver $driver, order $order, ${state.destination}, ${state.polylines}', + ); - return null; - } + cubit.setDestinationFromAddress(address, driverLocation); - Future _getRealRoute() async { - if (destination == null) return; - final url = - 'https://router.project-osrm.org/route/v1/driving/${myLocation.longitude},${myLocation.latitude};${destination!.longitude},${destination!.latitude}?overview=full&geometries=polyline'; - final response = await http.get(Uri.parse(url)); - if (response.statusCode == 200) { - final data = json.decode(response.body); - print('<<<<<<<<< OSRM data: $data'); - if (data['code'] == 'Ok') { - String encodedPolyline = data['routes'][0]['geometry']; - List result = PolylinePoints.decodePolyline( - encodedPolyline, - ); - List polylineCoordinates = result - .map((point) => LatLng(point.latitude, point.longitude)) - .toList(); - - setState(() { - polylines = { - Polyline( - polylineId: const PolylineId("real_route"), - color: Colors.pink, - width: 5, - points: polylineCoordinates, - ), - }; - }); - } else { - print("OSRM Error: ${data['code']}"); - } - } - } + driverMarker(driverLocation); - CameraPosition routeCameraPosition = const CameraPosition( - target: LatLng(31.2515108, 29.9842777), - zoom: 12, - ); + if (state.destination == null || state.polylines == null) return; + destinationMarker(state.destination!); - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Location')), - body: Scaffold( - body: Column( - children: [ - Expanded( - child: MarkerWidgets( - markerWidgetsController: _controller, - builder: (BuildContext context, Set markers) { - return GoogleMap( - initialCameraPosition: routeCameraPosition, - mapType: MapType.normal, - markers: markers, - polylines: polylines, - ); - }, - ), - ), - ], + if (state.polylines != null) { + polylines = { + Polyline( + polylineId: const PolylineId("real_route"), + color: AppColors.pink, + width: 5, + points: state.polylines ?? [], + ), + }; + } + setState(() {}); + + print( + '<<<<<<<<< driverLocation ${driverLocation.latitude}, ${driverLocation.longitude}', + ); + print( + '<<<<<<<<< pickupAddress ${state.data?.data?.orderDetails.pickupAddress.address}', + ); + print( + '<<<<<<<<< userAddress ${state.data?.data?.userAddress.address.toString()}', + ); + }, + + builder: (context, state) { + final driver = state.driverData?.data; + if (driver == null) { + return const Center(child: CircularProgressIndicator()); + } + + final driverLocation = LatLng( + driver.currentLocation.lat, + driver.currentLocation.lng, + ); + + return Stack( + alignment: Alignment.topLeft, + + children: [ + Column( + children: [ + Expanded( + child: GoogleMap( + initialCameraPosition: CameraPosition( + target: driverLocation, + zoom: 18, + ), + mapType: MapType.normal, + markers: markers, + polylines: polylines, + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.locationType == 'pickup') ...[ + SectionTitle(title: LocaleKeys.pickupAddress.tr()), + AddressCard( + title: + state + .data + ?.data + ?.orderDetails + .pickupAddress + .name ?? + '', + address: + state + .data + ?.data + ?.orderDetails + .pickupAddress + .address ?? + '', + imagePath: AppPaths.flowerLogo, + ), + const SizedBox(height: 16), + SectionTitle(title: LocaleKeys.userAddress.tr()), + + AddressCard( + title: state.data?.data?.userAddress.name ?? '', + address: + state.data?.data?.userAddress.address ?? '', + imagePath: AppPaths.flowerLogo, + ), + ] else ...[ + SectionTitle(title: LocaleKeys.userAddress.tr()), + AddressCard( + title: state.data?.data?.userAddress.name ?? '', + address: + state.data?.data?.userAddress.address ?? '', + imagePath: AppPaths.flowerLogo, + ), + const SizedBox(height: 16), + SectionTitle(title: LocaleKeys.pickupAddress.tr()), + AddressCard( + title: + state + .data + ?.data + ?.orderDetails + .pickupAddress + .name ?? + '', + address: + state + .data + ?.data + ?.orderDetails + .pickupAddress + .address ?? + '', + imagePath: AppPaths.flowerLogo, + ), + ], + ], + ), + ), + ], + ), + + Positioned( + top: 40, + left: 16, + child: InkWell( + onTap: () => context.pop(), + child: CircleAvatar( + backgroundColor: AppColors.pink, + child: Center( + child: Icon( + Icons.arrow_back_ios_new, + color: AppColors.white, + ), + ), + ), + ), + ), + ], + ); + }, ), ), ); diff --git a/lib/features/driver_orders_details/presentation/widgets/custom_marker_widget.dart b/lib/features/driver_orders_details/presentation/widgets/custom_marker_widget.dart deleted file mode 100644 index 9f90e09..0000000 --- a/lib/features/driver_orders_details/presentation/widgets/custom_marker_widget.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; - -Widget customMarker(String text, Icon icon) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), - decoration: BoxDecoration( - color: AppColors.pink, - borderRadius: BorderRadius.circular(30), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - CircleAvatar(backgroundColor: AppColors.white, radius: 16, child: icon), - const SizedBox(width: 6), - Text( - text, - style: const TextStyle( - color: AppColors.white, - fontWeight: FontWeight.w400, - fontSize: 12, - ), - ), - ], - ), - ); -} diff --git a/pubspec.lock b/pubspec.lock index b07c9ab..013c624 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -733,14 +733,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.14+3" - google_maps_marker_widgets: - dependency: "direct main" - description: - name: google_maps_marker_widgets - sha256: "2d1bd2862ba16554cad0425a96ce185718010d0d9e36087bb9df912bd5129c34" - url: "https://pub.dev" - source: hosted - version: "1.1.1" graphs: dependency: transitive description: @@ -774,7 +766,7 @@ packages: source: hosted version: "0.15.6" http: - dependency: "direct main" + dependency: transitive description: name: http sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" @@ -1261,14 +1253,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" - screenshot: - dependency: transitive - description: - name: screenshot - sha256: "63817697a7835e6ce82add4228e15d233b74d42975c143ad8cfe07009fab866b" - url: "https://pub.dev" - source: hosted - version: "3.0.0" shared_preferences: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index e8bf142..aa0a93f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,8 +39,6 @@ dependencies: firebase_crashlytics: ^5.0.7 cloud_firestore: ^6.1.2 cached_network_image: ^3.3.1 - google_maps_marker_widgets: ^1.1.1 - http: ^1.1.0 flutter_polyline_points: ^3.1.0 From 1cc4d58ddb7cceccddf62947be146bacb6284fac Mon Sep 17 00:00:00 2001 From: mariam Date: Sun, 8 Mar 2026 02:05:53 +0200 Subject: [PATCH 5/9] feat(SCRUM-98): add phone and whats app launcher --- lib/app/core/utils/app_launcher.dart | 20 +++++++++++++++++++ .../pages/drivers_orders_details_page.dart | 2 ++ .../presentation/pages/location_page.dart | 8 ++++++++ .../presentation/widgets/address_card.dart | 7 +++++-- 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 lib/app/core/utils/app_launcher.dart diff --git a/lib/app/core/utils/app_launcher.dart b/lib/app/core/utils/app_launcher.dart new file mode 100644 index 0000000..0468568 --- /dev/null +++ b/lib/app/core/utils/app_launcher.dart @@ -0,0 +1,20 @@ +import 'package:url_launcher/url_launcher.dart'; + +class AppLauncher { + static void launchPhone(String phoneNumber) async { + final Uri url = Uri(scheme: 'tel', path: phoneNumber); + if (await canLaunchUrl(url)) { + await launchUrl(url); + } + } + + static void launchWhatsApp(String phoneNumber) async { + String formattedPhone = phoneNumber.replaceAll(RegExp(r'[^0-9]'), ''); + + if (formattedPhone.startsWith('0')) { + formattedPhone = '20${formattedPhone.substring(1)}'; + } + final Uri url = Uri.parse("whatsapp://send?phone=$formattedPhone"); + await launchUrl(url, mode: LaunchMode.externalApplication); + } +} diff --git a/lib/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart b/lib/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart index 6623515..44316d3 100644 --- a/lib/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart +++ b/lib/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart @@ -122,6 +122,7 @@ class DriversOrdersDetailsPage extends StatelessWidget { address: order?.orderDetails.pickupAddress.address ?? '', imagePath: AppPaths.flowerLogo, + phoneNumber: (state.driverData?.data?.phone).toString(), ), ), const SizedBox(height: 16), @@ -133,6 +134,7 @@ class DriversOrdersDetailsPage extends StatelessWidget { title: order?.userAddress.name ?? '', address: order?.userAddress.address ?? '', imagePath: AppPaths.flowerLogo, + phoneNumber: (state.driverData?.data?.phone).toString(), ), ), const SizedBox(height: 24), diff --git a/lib/features/driver_orders_details/presentation/pages/location_page.dart b/lib/features/driver_orders_details/presentation/pages/location_page.dart index 2f5f9f9..254fd04 100644 --- a/lib/features/driver_orders_details/presentation/pages/location_page.dart +++ b/lib/features/driver_orders_details/presentation/pages/location_page.dart @@ -190,6 +190,8 @@ class _LocationPageState extends State { .address ?? '', imagePath: AppPaths.flowerLogo, + phoneNumber: (state.driverData?.data?.phone) + .toString(), ), const SizedBox(height: 16), SectionTitle(title: LocaleKeys.userAddress.tr()), @@ -199,6 +201,8 @@ class _LocationPageState extends State { address: state.data?.data?.userAddress.address ?? '', imagePath: AppPaths.flowerLogo, + phoneNumber: (state.driverData?.data?.phone) + .toString(), ), ] else ...[ SectionTitle(title: LocaleKeys.userAddress.tr()), @@ -207,6 +211,8 @@ class _LocationPageState extends State { address: state.data?.data?.userAddress.address ?? '', imagePath: AppPaths.flowerLogo, + phoneNumber: (state.driverData?.data?.phone) + .toString(), ), const SizedBox(height: 16), SectionTitle(title: LocaleKeys.pickupAddress.tr()), @@ -228,6 +234,8 @@ class _LocationPageState extends State { .address ?? '', imagePath: AppPaths.flowerLogo, + phoneNumber: (state.driverData?.data?.phone) + .toString(), ), ], ], diff --git a/lib/features/driver_orders_details/presentation/widgets/address_card.dart b/lib/features/driver_orders_details/presentation/widgets/address_card.dart index 6b211c2..0cfca02 100644 --- a/lib/features/driver_orders_details/presentation/widgets/address_card.dart +++ b/lib/features/driver_orders_details/presentation/widgets/address_card.dart @@ -1,17 +1,20 @@ import 'package:flutter/material.dart'; import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/app/core/utils/app_launcher.dart'; import 'package:tracking_app/app/core/values/paths.dart'; class AddressCard extends StatelessWidget { final String title; final String address; final String imagePath; + final String phoneNumber; const AddressCard({ super.key, required this.title, required this.address, required this.imagePath, + required this.phoneNumber, }); @override @@ -60,12 +63,12 @@ class AddressCard extends StatelessWidget { ), ), IconButton( - onPressed: () {}, + onPressed: () => AppLauncher.launchPhone(phoneNumber), icon: Icon(Icons.phone_outlined, color: AppColors.pink, size: 20), ), IconButton( - onPressed: () {}, + onPressed: () => AppLauncher.launchWhatsApp(phoneNumber), icon: ImageIcon( AssetImage(AppPaths.whatsappImage), color: AppColors.pink, From e026fc89c59aa0406d1f791198a2d9bebbfce145 Mon Sep 17 00:00:00 2001 From: mariam Date: Tue, 10 Mar 2026 22:35:04 +0200 Subject: [PATCH 6/9] feat(SCRUM-98): add unit & widget test for order details screen and location screen add unit tests for validation, auth storage, font style, app launcher files. --- .../change_password_dto_mapper.dart | 0 .../auth/data/repos/auth_repo_impl.dart | 2 +- .../auth_storage/auth_storage_test.dart | 69 ++++ .../validation/app_validation_test.dart | 149 +++++++++ .../core/ui_helper/style/font_style_test.dart | 45 +++ test/app/core/utils/app_launcher_test.dart | 69 ++++ .../core/utils/validators_helper_test.dart | 146 +++++++++ .../change_password_dto_mapper_test.dart | 2 +- ...r_details_remote_datasource_impl_test.dart | 110 ++++++- .../data/mapper/drivers_dto_mapper_test.dart | 38 +++ .../data/models/drivers_dto_test.dart | 59 ++++ .../repos/order_details_repo_impl_test.dart | 141 +++++++- .../domain/models/drivers_model_test.dart | 30 ++ .../get_driver_data_usecase_test.dart | 64 ++++ .../get_order_details_usecase_test.dart | 16 +- .../usecases/location_usecase_test.dart | 79 +++++ .../manager/order_details_cubit_test.dart | 304 ++++++++++++++++++ .../pages/location_page_test.dart | 191 +++++++++++ 18 files changed, 1499 insertions(+), 15 deletions(-) rename lib/features/auth/data/{mappers => mapper}/change_password_dto_mapper.dart (100%) create mode 100644 test/app/config/auth_storage/auth_storage_test.dart create mode 100644 test/app/config/validation/app_validation_test.dart create mode 100644 test/app/core/ui_helper/style/font_style_test.dart create mode 100644 test/app/core/utils/app_launcher_test.dart create mode 100644 test/app/core/utils/validators_helper_test.dart rename test/features/auth/data/{mappers => mapper}/change_password_dto_mapper_test.dart (88%) create mode 100644 test/features/driver_orders_details/data/mapper/drivers_dto_mapper_test.dart create mode 100644 test/features/driver_orders_details/data/models/drivers_dto_test.dart create mode 100644 test/features/driver_orders_details/domain/models/drivers_model_test.dart create mode 100644 test/features/driver_orders_details/domain/usecases/get_driver_data_usecase_test.dart create mode 100644 test/features/driver_orders_details/domain/usecases/location_usecase_test.dart create mode 100644 test/features/driver_orders_details/presentation/manager/order_details_cubit_test.dart create mode 100644 test/features/driver_orders_details/presentation/pages/location_page_test.dart diff --git a/lib/features/auth/data/mappers/change_password_dto_mapper.dart b/lib/features/auth/data/mapper/change_password_dto_mapper.dart similarity index 100% rename from lib/features/auth/data/mappers/change_password_dto_mapper.dart rename to lib/features/auth/data/mapper/change_password_dto_mapper.dart diff --git a/lib/features/auth/data/repos/auth_repo_impl.dart b/lib/features/auth/data/repos/auth_repo_impl.dart index 071b909..9d48d33 100644 --- a/lib/features/auth/data/repos/auth_repo_impl.dart +++ b/lib/features/auth/data/repos/auth_repo_impl.dart @@ -2,7 +2,7 @@ import 'package:injectable/injectable.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/auth/data/datasource/auth_remote_datasource.dart'; import 'package:tracking_app/features/auth/data/mapper/vehicles_mapper.dart'; -import 'package:tracking_app/features/auth/data/mappers/change_password_dto_mapper.dart'; +import 'package:tracking_app/features/auth/data/mapper/change_password_dto_mapper.dart'; import 'package:tracking_app/features/auth/data/model/request/LoginRequest.dart'; import 'package:tracking_app/features/auth/data/model/response/LoginResponse.dart'; import 'package:tracking_app/features/auth/data/model/response/change_password_dto.dart'; diff --git a/test/app/config/auth_storage/auth_storage_test.dart b/test/app/config/auth_storage/auth_storage_test.dart new file mode 100644 index 0000000..187c7bf --- /dev/null +++ b/test/app/config/auth_storage/auth_storage_test.dart @@ -0,0 +1,69 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; + +void main() { + late AuthStorage authStorage; + + setUp(() { + authStorage = AuthStorage(); + SharedPreferences.setMockInitialValues({}); + }); + + group('AuthStorage Tests', () { + test( + 'saveToken should call setString with correct key and value', + () async { + const token = 'test_token_123'; + + await authStorage.saveToken(token); + + final storedToken = await authStorage.getToken(); + expect(storedToken, token); + }, + ); + + test('getRememberMe should return false by default if not set', () async { + final result = await authStorage.getRememberMe(); + + expect(result, false); + }); + + test('setRememberMe should store boolean value correctly', () async { + await authStorage.setRememberMe(true); + + final result = await authStorage.getRememberMe(); + expect(result, true); + }); + + test('saveUserJson and getUserJson should handle string data', () async { + const userJson = '{"id": 1, "name": "Gemini"}'; + + await authStorage.saveUserJson(userJson); + final result = await authStorage.getUserJson(); + + expect(result, userJson); + }); + + test('clearOrderId should remove the order id from prefs', () async { + await authStorage.saveOrderId('order_999'); + + await authStorage.clearOrderId(); + final result = await authStorage.getOrderId(); + + expect(result, null); + }); + + test('clearAll should reset all stored values', () async { + await authStorage.saveToken('token'); + await authStorage.setRememberMe(true); + await authStorage.saveOrderId('123'); + + await authStorage.clearAll(); + + expect(await authStorage.getToken(), null); + expect(await authStorage.getRememberMe(), false); + expect(await authStorage.getOrderId(), null); + }); + }); +} diff --git a/test/app/config/validation/app_validation_test.dart b/test/app/config/validation/app_validation_test.dart new file mode 100644 index 0000000..b8f6a47 --- /dev/null +++ b/test/app/config/validation/app_validation_test.dart @@ -0,0 +1,149 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tracking_app/app/config/validation/app_validation.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; + +void main() { + setUpAll(() async { + SharedPreferences.setMockInitialValues({}); + EasyLocalization.logger.enableLevels = []; + }); + + group('Validators Unit Tests', () { + group('firstNameValidator', () { + test('should return required error for null or empty', () { + expect( + Validators.firstNameValidator(null), + LocaleKeys.firstNameRequired.tr(), + ); + expect( + Validators.firstNameValidator(''), + LocaleKeys.firstNameRequired.tr(), + ); + }); + + test( + 'should return invalid error for names < 3 or > 50 or with numbers', + () { + expect( + Validators.firstNameValidator('Ab'), + LocaleKeys.nameInvalid.tr(), + ); + expect( + Validators.firstNameValidator('Ab12'), + LocaleKeys.nameInvalid.tr(), + ); + }, + ); + + test('should return null for valid first name', () { + expect(Validators.firstNameValidator('Ahmed'), null); + }); + }); + + group('phoneValidator', () { + test('should return required error for empty phone', () { + expect(Validators.phoneValidator(''), LocaleKeys.phoneRequired.tr()); + }); + + test('should return invalid for non-Egyptian format or wrong length', () { + // Regex بيطلب يبدأ بـ +201 وبعدها [0-2, 5] وبعدها 8 أرقام + expect( + Validators.phoneValidator('01012345678'), + LocaleKeys.phoneInvalid.tr(), + ); // ناقص الـ +20 + expect( + Validators.phoneValidator('+201312345678'), + LocaleKeys.phoneInvalid.tr(), + ); // رقم 3 مش موجود في الـ range + }); + + test('should return null for valid Egyptian phone (+2010...)', () { + expect(Validators.phoneValidator('+201012345678'), null); + }); + }); + + group('passwordValidator', () { + test( + 'should validate length, capital, small, number, and special char', + () { + expect( + Validators.passwordValidator(''), + LocaleKeys.passwordRequired.tr(), + ); + expect( + Validators.passwordValidator('123ab'), + LocaleKeys.passwordLengthInvalid.tr(), + ); + expect( + Validators.passwordValidator('abcdef123!'), + LocaleKeys.passwordUpperLetterInvalid.tr(), + ); + expect( + Validators.passwordValidator('ABCDEF123!'), + LocaleKeys.passwordLowerLetterInvalid.tr(), + ); + expect( + Validators.passwordValidator('Abcdefgh!'), + LocaleKeys.passwordNumbersInvalid.tr(), + ); + expect( + Validators.passwordValidator('Abcdefgh1'), + LocaleKeys.passwordSpecialCharInvalid.tr(), + ); + }, + ); + + test('should return null for strong password', () { + expect(Validators.passwordValidator('Strong123!'), null); + }); + }); + + group('newPasswordValidator', () { + test('should return error if same as current password', () { + const currentPass = 'OldPass123!'; + expect( + Validators.newPasswordValidator(currentPass, currentPass), + LocaleKeys.cannotBeSame.tr(), + ); + }); + }); + + group('confirmPasswordValidator', () { + test('should return error if passwords do not match', () { + expect( + Validators.confirmPasswordValidator('Pass1', 'Pass2'), + LocaleKeys.passwordsDoNotMatch.tr(), + ); + }); + }); + + group('emailValidator', () { + test('should return invalid for wrong email formats', () { + expect( + Validators.emailValidator('test@'), + LocaleKeys.emailInvalid.tr(), + ); + expect( + Validators.emailValidator('test@domain'), + LocaleKeys.emailInvalid.tr(), + ); + }); + + test('should return null for valid email', () { + expect(Validators.emailValidator('user@example.com'), null); + }); + }); + + group('genderValidator', () { + test('should return error if gender is not selected', () { + expect( + Validators.genderValidator(null), + LocaleKeys.genderRequired.tr(), + ); + expect(Validators.genderValidator(''), LocaleKeys.genderRequired.tr()); + }); + }); + }); +} diff --git a/test/app/core/ui_helper/style/font_style_test.dart b/test/app/core/ui_helper/style/font_style_test.dart new file mode 100644 index 0000000..559dae4 --- /dev/null +++ b/test/app/core/ui_helper/style/font_style_test.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/app/core/ui_helper/style/font_style.dart'; + +void main() { + group('AppStyles Tests', () { + test('font32BlackSemiBold should have correct properties', () { + final style = AppStyles.font32BlackSemiBold; + + expect(style.fontSize, 32); + expect(style.color, AppColors.blackColor); + expect(style.fontWeight, FontWeight.w500); + expect(style.fontFamily, 'SansArabic'); + }); + + test('subtitle should have correct properties', () { + final style = AppStyles.subtitle; + + expect(style.fontSize, 12); + expect(style.color, AppColors.grey); + expect(style.fontWeight, FontWeight.normal); + }); + + test('All styles should use the correct default fontFamily', () { + expect(AppStyles.black24SemiBold.fontFamily, 'SansArabic'); + expect(AppStyles.font16Black.fontFamily, 'SansArabic'); + expect(AppStyles.purple18bold.fontFamily, 'SansArabic'); + }); + + test('Special case: medium20 should use Inter font', () { + expect(AppStyles.medium20.fontFamily, 'Inter'); + expect(AppStyles.medium20.fontSize, 20); + }); + + test('red14Normal should return red color from AppColors', () { + expect(AppStyles.red14Normal.color, AppColors.red); + expect(AppStyles.red14Normal.fontSize, 14); + }); + + test('Check for specific bug: font12White color fix', () { + expect(AppStyles.font12White.color, AppColors.blackColor); + }); + }); +} diff --git a/test/app/core/utils/app_launcher_test.dart b/test/app/core/utils/app_launcher_test.dart new file mode 100644 index 0000000..a17a87b --- /dev/null +++ b/test/app/core/utils/app_launcher_test.dart @@ -0,0 +1,69 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:tracking_app/app/core/utils/app_launcher.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; +import 'app_launcher_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(mixingIn: [MockPlatformInterfaceMixin]), +]) +void main() { + late MockUrlLauncherPlatform mockPlatform; + + setUp(() { + mockPlatform = MockUrlLauncherPlatform(); + UrlLauncherPlatform.instance = mockPlatform; + }); + + group('AppLauncher Tests', () { + test('launchPhone should call launchUrl with tel scheme', () async { + const phoneNumber = '0123456789'; + const expectedUrl = 'tel:0123456789'; + + when(mockPlatform.canLaunch(expectedUrl)).thenAnswer((_) async => true); + when( + mockPlatform.launchUrl(expectedUrl, any), + ).thenAnswer((_) async => true); + + AppLauncher.launchPhone(phoneNumber); + + await untilCalled(mockPlatform.launchUrl(expectedUrl, any)); + + verify(mockPlatform.launchUrl(expectedUrl, any)).called(1); + }); + + test( + 'launchWhatsApp should format Egyptian numbers correctly and launch', + () async { + const phoneNumber = '01012345678'; + const expectedUrl = 'whatsapp://send?phone=201012345678'; + + when( + mockPlatform.launchUrl(expectedUrl, any), + ).thenAnswer((_) async => true); + + AppLauncher.launchWhatsApp(phoneNumber); + + await untilCalled(mockPlatform.launchUrl(expectedUrl, any)); + + verify(mockPlatform.launchUrl(expectedUrl, any)).called(1); + }, + ); + + test('launchWhatsApp should strip non-numeric characters', () async { + const phoneNumber = '+20 (123) 456-789'; + const expectedUrl = 'whatsapp://send?phone=20123456789'; + + when( + mockPlatform.launchUrl(expectedUrl, any), + ).thenAnswer((_) async => true); + + AppLauncher.launchWhatsApp(phoneNumber); + await untilCalled(mockPlatform.launchUrl(expectedUrl, any)); + + verify(mockPlatform.launchUrl(expectedUrl, any)).called(1); + }); + }); +} diff --git a/test/app/core/utils/validators_helper_test.dart b/test/app/core/utils/validators_helper_test.dart new file mode 100644 index 0000000..595ca7e --- /dev/null +++ b/test/app/core/utils/validators_helper_test.dart @@ -0,0 +1,146 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/app/core/utils/validators_helper.dart'; +import 'package:tracking_app/app/core/values/user_error_mesagges.dart'; + +void main() { + group('Validators Tests', () { + group('validateEmail', () { + test('should return error if email is empty', () { + expect(Validators.validateEmail(''), UserErrorMessages.emailRequired); + expect(Validators.validateEmail(null), UserErrorMessages.emailRequired); + }); + + test('should return error if email format is invalid', () { + expect( + Validators.validateEmail('test'), + UserErrorMessages.invalidEmail, + ); + expect( + Validators.validateEmail('test@'), + UserErrorMessages.invalidEmail, + ); + expect( + Validators.validateEmail('test@domain'), + UserErrorMessages.invalidEmail, + ); + }); + + test('should return null if email is valid', () { + expect(Validators.validateEmail('test@example.com'), null); + }); + }); + + group('validatePassword', () { + test('should return error if password is empty', () { + expect( + Validators.validatePassword(''), + UserErrorMessages.passwordRequired, + ); + }); + + test('should return error if password < 6 characters', () { + expect( + Validators.validatePassword('Ab1'), + UserErrorMessages.least6Characters, + ); + }); + + test('should return error if no capital letter', () { + expect( + Validators.validatePassword('abc12345'), + UserErrorMessages.passwordWithCapital, + ); + }); + + test('should return error if no number', () { + expect( + Validators.validatePassword('Abcdefgh'), + UserErrorMessages.passwordWithNumber, + ); + }); + + test('should return null if password is valid', () { + expect(Validators.validatePassword('Password123'), null); + }); + }); + + group('validateRePassword', () { + test('should return error if confirm password is empty', () { + expect( + Validators.validateRePassword('', 'Password123'), + UserErrorMessages.confirmPassword, + ); + }); + + test('should return error if passwords do not match', () { + expect( + Validators.validateRePassword('123', '456'), + UserErrorMessages.passwordDontMatch, + ); + }); + + test('should return null if passwords match', () { + expect(Validators.validateRePassword('Pass123', 'Pass123'), null); + }); + }); + + group('validatePhone', () { + test('should return error if phone is empty', () { + expect(Validators.validatePhone(''), UserErrorMessages.phoneRequired); + }); + + test( + 'should return error if phone format is invalid (Egyptian format)', + () { + expect( + Validators.validatePhone('12345678901'), + UserErrorMessages.invalidNumber, + ); // No 01 at start + expect( + Validators.validatePhone('0101234567'), + UserErrorMessages.invalidNumber, + ); // Too short + }, + ); + + test('should return null if phone is valid', () { + expect(Validators.validatePhone('01012345678'), null); + expect(Validators.validatePhone('01112345678'), null); + }); + }); + + group('validateName / RecipientName / Address', () { + test('validateName should catch special characters and length', () { + expect( + Validators.validateName('ab'), + UserErrorMessages.least3Characters, + ); + expect(Validators.validateName('John@'), UserErrorMessages.invalidName); + expect(Validators.validateName('John Doe'), null); + }); + + test('validateRecipientName should return specific error messages', () { + expect( + Validators.validateRecipientName(''), + UserErrorMessages.requiredRecipientName, + ); + expect( + Validators.validateRecipientName('Al!'), + UserErrorMessages.invalidRecipientName, + ); + }); + + test('validateAddress should return specific error messages', () { + expect( + Validators.validateAddress(''), + UserErrorMessages.requiredAddress, + ); + expect( + Validators.validateAddress('Cairo#5'), + UserErrorMessages.invalidAddress, + ); + expect(Validators.validateAddress('Maadi, Cairo'), null); + }); + }); + }); +} diff --git a/test/features/auth/data/mappers/change_password_dto_mapper_test.dart b/test/features/auth/data/mapper/change_password_dto_mapper_test.dart similarity index 88% rename from test/features/auth/data/mappers/change_password_dto_mapper_test.dart rename to test/features/auth/data/mapper/change_password_dto_mapper_test.dart index a673111..167ca64 100644 --- a/test/features/auth/data/mappers/change_password_dto_mapper_test.dart +++ b/test/features/auth/data/mapper/change_password_dto_mapper_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:tracking_app/features/auth/data/mappers/change_password_dto_mapper.dart'; +import 'package:tracking_app/features/auth/data/mapper/change_password_dto_mapper.dart'; import 'package:tracking_app/features/auth/data/model/response/change_password_dto.dart'; import 'package:tracking_app/features/auth/domain/models/change_password_model.dart'; diff --git a/test/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl_test.dart b/test/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl_test.dart index bc715b1..d67574f 100644 --- a/test/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl_test.dart +++ b/test/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl_test.dart @@ -1,9 +1,12 @@ +import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/drivers_dto.dart'; import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; import 'order_details_remote_datasource_impl_test.mocks.dart'; @@ -12,23 +15,30 @@ import 'order_details_remote_datasource_impl_test.mocks.dart'; CollectionReference, DocumentReference, DocumentSnapshot, + Dio, ]) void main() { late OrderDetailsRemoteDatasourceImpl dataSource; late MockFirebaseFirestore mockFirestore; + late MockDio mockDio; late MockCollectionReference> mockCollection; late MockDocumentReference> mockDocument; late MockDocumentSnapshot> mockSnapshot; const String tOrderId = 'pxkMaEmWYVuvV5jkW0JK'; + const String driverId = '6989f35de364ef61405211a0'; setUp(() { mockFirestore = MockFirebaseFirestore(); + mockDio = MockDio(); mockCollection = MockCollectionReference(); mockDocument = MockDocumentReference(); mockSnapshot = MockDocumentSnapshot(); - dataSource = OrderDetailsRemoteDatasourceImpl(firestore: mockFirestore); + dataSource = OrderDetailsRemoteDatasourceImpl( + firestore: mockFirestore, + dio: mockDio, + ); }); group('getOrderStream', () { final tOrderJson = { @@ -71,4 +81,102 @@ void main() { ); }); }); + + group('getDriversData', () { + final driverData = { + 'id': '6989f35de364ef61405211a0', + 'currentLocation': {'lat': 31.251555, 'lng': 29.9843417}, + 'name': "mariam", + 'phone': '01205708282', + 'deviceToken': '', + }; + + test('should return SuccessApiResult with Stream of DriverDto', () async { + when(mockFirestore.collection('drivers')).thenReturn(mockCollection); + when(mockCollection.doc(driverId)).thenReturn(mockDocument); + + when(mockSnapshot.exists).thenReturn(true); + when(mockSnapshot.data()).thenReturn(driverData); + when(mockSnapshot.id).thenReturn(driverId); + + when( + mockDocument.snapshots(), + ).thenAnswer((_) => Stream.value(mockSnapshot)); + + final result = dataSource.getDriverData(driverId); + + expect(result, isA>>()); + final stream = (result as SuccessApiResult>).data; + await expectLater( + stream, + emits( + isA() + .having((o) => o.name, 'name', 'mariam') + .having((o) => o.id, 'id', driverId), + ), + ); + }); + }); + + group('getLatLngFromAddress', () { + test('should return LatLng when API responds with valid data', () async { + final responseData = [ + {"lat": "30.0444", "lon": "31.2357"}, + ]; + + when( + mockDio.get( + any, + queryParameters: anyNamed('queryParameters'), + options: anyNamed('options'), + ), + ).thenAnswer( + (_) async => Response( + data: responseData, + statusCode: 200, + requestOptions: RequestOptions(path: ''), + ), + ); + + final result = await dataSource.getLatLngFromAddress("Cairo"); + + expect(result, isA>()); + final success = result as SuccessApiResult; + expect(success.data!.latitude, 30.0444); + expect(success.data!.longitude, 31.2357); + }); + }); + + group('getRealRoute', () { + test( + 'should return List when API responds with valid route', + () async { + final responseData = { + "code": "Ok", + "routes": [ + {"geometry": "}_ilFjk~uO??"}, + ], + }; + + when( + mockDio.get(any, queryParameters: anyNamed('queryParameters')), + ).thenAnswer( + (_) async => Response( + data: responseData, + statusCode: 200, + requestOptions: RequestOptions(path: ''), + ), + ); + + final result = await dataSource.getRealRoute( + const LatLng(30.0444, 31.2357), + const LatLng(30.0500, 31.2400), + ); + + expect(result, isA>>()); + final success = result as SuccessApiResult>; + expect(success.data, isNotEmpty); + }, + ); + }); } diff --git a/test/features/driver_orders_details/data/mapper/drivers_dto_mapper_test.dart b/test/features/driver_orders_details/data/mapper/drivers_dto_mapper_test.dart new file mode 100644 index 0000000..e4460b4 --- /dev/null +++ b/test/features/driver_orders_details/data/mapper/drivers_dto_mapper_test.dart @@ -0,0 +1,38 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/driver_orders_details/data/mapper/drivers_dto_mapper.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/drivers_dto.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; + +void main() { + group('DriverDataDtoMapper', () { + test('Convert DriverDataDto to DriverDataModel correctly', () { + final dto = DriverDataDto( + deviceToken: 'token', + id: '111', + phone: '0', + currentLocation: DriverLocationDto(lat: 31, lng: 29), + name: 'Mariam', + ); + + final result = dto.toDriversModel(); + + expect(result, isA()); + expect(result.deviceToken, dto.deviceToken); + expect(result.name, dto.name); + expect(result.phone, dto.phone); + expect(result.currentLocation.lat, dto.currentLocation.lat); + }); + }); + + group('DriverLocationDtoMapper', () { + test('Convert DriverLocationDto to DriverLocationModel correctly', () { + final dto = DriverLocationDto(lat: 30, lng: 29); + + final result = dto.toDriverLocationModel(); + + expect(result, isA()); + expect(result.lat, dto.lat); + expect(result.lng, dto.lng); + }); + }); +} diff --git a/test/features/driver_orders_details/data/models/drivers_dto_test.dart b/test/features/driver_orders_details/data/models/drivers_dto_test.dart new file mode 100644 index 0000000..006150e --- /dev/null +++ b/test/features/driver_orders_details/data/models/drivers_dto_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/drivers_dto.dart'; + +void main() { + group('DriverDataDto Tests', () { + test('should return a valid DriverDataDto from JSON', () { + final Map json = { + 'id': '6989f35de364ef61405211a0', + 'currentLocation': {'lat': 31.251555, 'lng': 29.9843417}, + 'name': "mariam", + 'phone': '01205708282', + 'deviceToken': '', + }; + + final result = DriverDataDto.fromJson(json); + + expect(result.phone, '01205708282'); + expect(result.name, 'mariam'); + expect(result.id, '6989f35de364ef61405211a0'); + expect(result.currentLocation.lat, 31.251555); + }); + + test('should return a valid JSON map from DriverDataDto', () { + final dto = DriverDataDto( + currentLocation: DriverLocationDto(lat: 30, lng: 29), + deviceToken: 'token', + id: '123', + phone: '01205708282', + name: 'Mariam', + ); + + final result = dto.toJson(); + + expect(result['deviceToken'], 'token'); + expect(result['name'], 'Mariam'); + expect(result['id'], '123'); + }); + }); + + group('DriverLocationDto Tests', () { + test('should return a valid DriverLocationDto from JSON', () { + final Map json = {'lat': 30, 'lng': 29}; + + final result = DriverLocationDto.fromJson(json); + + expect(result.lat, 30); + expect(result.lng, 29); + }); + + test('should return a valid JSON map from DriverLocationDto', () { + final dto = DriverLocationDto(lat: 30, lng: 29); + + final result = dto.toJson(); + + expect(result['lat'], 30); + expect(result['lng'], 29); + }); + }); +} diff --git a/test/features/driver_orders_details/data/repos/order_details_repo_impl_test.dart b/test/features/driver_orders_details/data/repos/order_details_repo_impl_test.dart index b10ab3e..b21d092 100644 --- a/test/features/driver_orders_details/data/repos/order_details_repo_impl_test.dart +++ b/test/features/driver_orders_details/data/repos/order_details_repo_impl_test.dart @@ -1,28 +1,40 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/drivers_dto.dart'; import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; import 'package:tracking_app/features/driver_orders_details/data/repos/order_details_repo_impl.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; import 'order_details_repo_impl_test.mocks.dart'; -@GenerateMocks([OrderDetailsRemoteDatasource, DocumentSnapshot]) +@GenerateMocks([OrderDetailsRemoteDatasource, DocumentSnapshot, AuthStorage]) void main() { late OrderDetailsRepoImpl repository; + late MockAuthStorage authStorage; late MockOrderDetailsRemoteDatasource mockRemoteDataSource; setUp(() { mockRemoteDataSource = MockOrderDetailsRemoteDatasource(); - repository = OrderDetailsRepoImpl(mockRemoteDataSource); + authStorage = MockAuthStorage(); + repository = OrderDetailsRepoImpl(mockRemoteDataSource, authStorage); provideDummy>>( ErrorApiResult(error: 'dummy_error'), ); + provideDummy>>( + ErrorApiResult(error: 'dummy_error'), + ); + provideDummy>(ErrorApiResult(error: 'dummy_error')); + provideDummy>>(ErrorApiResult(error: 'dummy_error')); }); const tOrderId = 'pxkMaEmWYVuvV5jkW0JK'; + const driverId = '6989f35de364ef61405211a0'; final tOrderDto = OrderDto( driverId: 'D123', @@ -43,15 +55,25 @@ void main() { ), ); + final driverDto = DriverDataDto( + deviceToken: 'token', + id: '6989f35de364ef61405211a0', + name: 'mariam', + phone: '01205708282', + currentLocation: DriverLocationDto(lat: 30, lng: 29), + ); + group('getOrderDetails', () { test( 'should emit OrderModel when the remote data source returns SuccessApiResult with Stream', () async { + when(authStorage.getOrderId()).thenAnswer((_) async => tOrderId); + when( mockRemoteDataSource.getOrderStream(tOrderId), ).thenReturn(SuccessApiResult(data: Stream.value(tOrderDto))); - final result = repository.getOrderDetails(tOrderId); + final result = await repository.getOrderDetails(); expect(result, isA>>()); final stream = (result as SuccessApiResult>).data; @@ -76,15 +98,126 @@ void main() { 'should throw an Exception when the document does not exist', () async { const errorMessage = "Network Error"; + when(authStorage.getOrderId()).thenAnswer((_) async => tOrderId); + when( mockRemoteDataSource.getOrderStream(tOrderId), ).thenReturn(ErrorApiResult(error: errorMessage)); - final result = repository.getOrderDetails(tOrderId); + final result = await repository.getOrderDetails(); expect(result, isA>>()); expect((result as ErrorApiResult).error, errorMessage); }, ); }); + + group('getDriverData', () { + test( + 'should emit DriverDataModel when the remote data source returns SuccessApiResult with Stream', + () async { + when( + mockRemoteDataSource.getDriverData(driverId), + ).thenReturn(SuccessApiResult(data: Stream.value(driverDto))); + + final result = repository.getDriverData(driverId); + + expect(result, isA>>()); + final stream = + (result as SuccessApiResult>).data; + await expectLater( + stream, + emits( + isA() + .having((o) => o.id, 'driver id', driverId) + .having((o) => o.name, 'user name', driverDto.name) + .having((o) => o.currentLocation.lat, 'lat', 30), + ), + ); + }, + ); + + test( + 'should throw an Exception when the document does not exist', + () async { + const errorMessage = "Network Error"; + when( + mockRemoteDataSource.getDriverData(driverId), + ).thenReturn(ErrorApiResult(error: errorMessage)); + + final result = repository.getDriverData(driverId); + + expect(result, isA>>()); + expect((result as ErrorApiResult).error, errorMessage); + }, + ); + }); + group('getLatLngFromAddress', () { + final tAddress = "Cairo"; + final tLatLng = LatLng(30.0, 31.0); + + test( + 'should return SuccessApiResult when remote data source succeeds', + () async { + when( + mockRemoteDataSource.getLatLngFromAddress(tAddress), + ).thenAnswer((_) async => SuccessApiResult(data: tLatLng)); + + final result = await repository.getLatLngFromAddress(tAddress); + + expect(result, isA>()); + expect((result as SuccessApiResult).data, tLatLng); + }, + ); + + test( + 'should return ErrorApiResult when remote data source fails', + () async { + when(mockRemoteDataSource.getLatLngFromAddress(tAddress)).thenAnswer( + (_) async => ErrorApiResult(error: "Network Error"), + ); + + final result = await repository.getLatLngFromAddress(tAddress); + + expect(result, isA>()); + expect((result as ErrorApiResult).error, "Network Error"); + }, + ); + }); + + group('getRealRoute', () { + final tMyLocation = LatLng(30.0, 31.0); + final tDestination = LatLng(30.5, 31.5); + final tRoute = [LatLng(30.0, 31.0), LatLng(30.5, 31.5)]; + + test( + 'should return SuccessApiResult when remote data source succeeds', + () async { + when( + mockRemoteDataSource.getRealRoute(tMyLocation, tDestination), + ).thenAnswer((_) async => SuccessApiResult>(data: tRoute)); + + final result = await repository.getRealRoute(tMyLocation, tDestination); + + expect(result, isA>>()); + expect((result as SuccessApiResult).data, tRoute); + }, + ); + + test( + 'should return ErrorApiResult when remote data source fails', + () async { + when( + mockRemoteDataSource.getRealRoute(tMyLocation, tDestination), + ).thenAnswer( + (_) async => ErrorApiResult>(error: "Routing Error"), + ); + + final result = await repository.getRealRoute(tMyLocation, tDestination); + + expect(result, isA>>()); + expect((result as ErrorApiResult).error, "Routing Error"); + }, + ); + }); } diff --git a/test/features/driver_orders_details/domain/models/drivers_model_test.dart b/test/features/driver_orders_details/domain/models/drivers_model_test.dart new file mode 100644 index 0000000..4d2baaf --- /dev/null +++ b/test/features/driver_orders_details/domain/models/drivers_model_test.dart @@ -0,0 +1,30 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; + +void main() { + group('DriverDataModel Tests', () { + test('should correctly initialize DriverDataModel with given values', () { + final dataModel = DriverDataModel( + name: 'mariam', + id: '1', + phone: '01205708282', + deviceToken: 'token', + currentLocation: DriverLocationModel(lat: 30, lng: 29), + ); + + expect(dataModel.name, 'mariam'); + expect(dataModel.currentLocation.lat, 30); + expect(dataModel.id, '1'); + }); + + test( + 'should correctly initialize DriverLocationModel with given values', + () { + final location = DriverLocationModel(lat: 30, lng: 29); + + expect(location.lat, 30); + expect(location.lng, 29); + }, + ); + }); +} diff --git a/test/features/driver_orders_details/domain/usecases/get_driver_data_usecase_test.dart b/test/features/driver_orders_details/domain/usecases/get_driver_data_usecase_test.dart new file mode 100644 index 0000000..aeb2464 --- /dev/null +++ b/test/features/driver_orders_details/domain/usecases/get_driver_data_usecase_test.dart @@ -0,0 +1,64 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/repos/order_details_repo.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/usecases/get_driver_data_usecase.dart'; +import 'get_order_details_usecase_test.mocks.dart'; + +@GenerateMocks([OrderDetailsRepo]) +void main() { + late GetDriverDataUsecase usecase; + late MockOrderDetailsRepo mockRepo; + + setUp(() { + mockRepo = MockOrderDetailsRepo(); + usecase = GetDriverDataUsecase(repo: mockRepo); + provideDummy>>( + ErrorApiResult(error: 'dummy'), + ); + }); + + const driverId = 'pxkMaEmWYVuvV5jkW0JK'; + + final driverModel = DriverDataModel( + id: 'id', + name: 'name', + phone: 'phone', + deviceToken: 'deviceToken', + currentLocation: DriverLocationModel(lat: 30, lng: 29), + ); + + group('GetDriverDataUsecase test', () { + test( + 'should return SuccessApiResult containing the Stream from the repository', + () async { + when( + mockRepo.getDriverData(driverId), + ).thenAnswer((_) => SuccessApiResult(data: Stream.value(driverModel))); + + final result = usecase.call(driverId); + + expect(result, isA>>()); + final stream = + (result as SuccessApiResult>).data; + await expectLater(stream, emits(driverModel)); + verify(mockRepo.getDriverData(driverId)).called(1); + }, + ); + + test('should return ErrorApiResult when the repository fails', () async { + when(mockRepo.getDriverData(driverId)).thenAnswer( + (_) => ErrorApiResult>( + error: 'Error from Repository', + ), + ); + + final result = await usecase.call(driverId); + + expect(result, isA>>()); + expect((result as ErrorApiResult).error, 'Error from Repository'); + }); + }); +} diff --git a/test/features/driver_orders_details/domain/usecases/get_order_details_usecase_test.dart b/test/features/driver_orders_details/domain/usecases/get_order_details_usecase_test.dart index d27570b..c5cba7e 100644 --- a/test/features/driver_orders_details/domain/usecases/get_order_details_usecase_test.dart +++ b/test/features/driver_orders_details/domain/usecases/get_order_details_usecase_test.dart @@ -40,25 +40,25 @@ void main() { test( 'should return SuccessApiResult containing the Stream from the repository', () async { - when( - mockRepo.getOrderDetails(any), - ).thenReturn(SuccessApiResult(data: Stream.value(tOrderModel))); + when(mockRepo.getOrderDetails()).thenAnswer( + (_) async => SuccessApiResult(data: Stream.value(tOrderModel)), + ); - final result = usecase.call(tOrderId); + final result = await usecase.call(); expect(result, isA>>()); final stream = (result as SuccessApiResult>).data; await expectLater(stream, emits(tOrderModel)); - verify(mockRepo.getOrderDetails(tOrderId)).called(1); + verify(mockRepo.getOrderDetails()).called(1); }, ); test('should return ErrorApiResult when the repository fails', () async { when( - mockRepo.getOrderDetails(any), - ).thenReturn(ErrorApiResult(error: 'Error from Repository')); + mockRepo.getOrderDetails(), + ).thenAnswer((_) async => ErrorApiResult(error: 'Error from Repository')); - final result = usecase.call(tOrderId); + final result = await usecase.call(); expect(result, isA>>()); expect((result as ErrorApiResult).error, 'Error from Repository'); diff --git a/test/features/driver_orders_details/domain/usecases/location_usecase_test.dart b/test/features/driver_orders_details/domain/usecases/location_usecase_test.dart new file mode 100644 index 0000000..f688c54 --- /dev/null +++ b/test/features/driver_orders_details/domain/usecases/location_usecase_test.dart @@ -0,0 +1,79 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/repos/order_details_repo.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/usecases/location_usecase.dart'; + +import 'get_order_details_usecase_test.mocks.dart'; + +@GenerateMocks([OrderDetailsRepo]) +void main() { + late LocationUsecase usecase; + late MockOrderDetailsRepo mockRepo; + + setUp(() { + mockRepo = MockOrderDetailsRepo(); + usecase = LocationUsecase(mockRepo); + provideDummy>(ErrorApiResult(error: 'dummy')); + provideDummy>>(ErrorApiResult(error: 'dummy')); + }); + + const address = 'Cairo'; + final tLatLng = LatLng(30.0, 31.0); + + group('LocationUsecase.getAddress test', () { + test( + 'should return SuccessApiResult containing the Stream from the repository when get address', + () async { + when( + mockRepo.getLatLngFromAddress(address), + ).thenAnswer((_) async => SuccessApiResult(data: tLatLng)); + + final result = await usecase.getAddress(address); + + expect(result, isA>()); + verify(mockRepo.getLatLngFromAddress(address)).called(1); + }, + ); + + test('should return ErrorApiResult when the repository fails', () async { + when( + mockRepo.getLatLngFromAddress(address), + ).thenAnswer((_) async => ErrorApiResult(error: 'Error from Repository')); + + final result = await usecase.getAddress(address); + + expect(result, isA>()); + expect((result as ErrorApiResult).error, 'Error from Repository'); + }); + }); + + group('LocationUsecase.getRealRoute test', () { + test( + 'should return SuccessApiResult containing the Stream from the repository', + () async { + when( + mockRepo.getRealRoute(tLatLng, tLatLng), + ).thenAnswer((_) async => SuccessApiResult(data: [tLatLng])); + + final result = await usecase.getRealRoute(tLatLng, tLatLng); + + expect(result, isA>>()); + verify(mockRepo.getRealRoute(tLatLng, tLatLng)).called(1); + }, + ); + + test('should return ErrorApiResult when the repository fails', () async { + when( + mockRepo.getRealRoute(tLatLng, tLatLng), + ).thenAnswer((_) async => ErrorApiResult(error: 'Error from Repository')); + + final result = await usecase.getRealRoute(tLatLng, tLatLng); + + expect(result, isA>>()); + expect((result as ErrorApiResult).error, 'Error from Repository'); + }); + }); +} diff --git a/test/features/driver_orders_details/presentation/manager/order_details_cubit_test.dart b/test/features/driver_orders_details/presentation/manager/order_details_cubit_test.dart new file mode 100644 index 0000000..4d64044 --- /dev/null +++ b/test/features/driver_orders_details/presentation/manager/order_details_cubit_test.dart @@ -0,0 +1,304 @@ +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/usecases/get_driver_data_usecase.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/usecases/get_order_details_usecase.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/usecases/location_usecase.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_cubit.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_states.dart'; + +import 'order_details_cubit_test.mocks.dart'; + +@GenerateMocks([GetOrderDetailsUsecase, GetDriverDataUsecase, LocationUsecase]) +void main() { + late OrderDetailsCubit cubit; + + late MockGetOrderDetailsUsecase mockGetOrderDetailsUsecase; + late MockGetDriverDataUsecase mockGetDriverDataUsecase; + late MockLocationUsecase mockLocationUsecase; + + setUpAll(() { + mockGetOrderDetailsUsecase = MockGetOrderDetailsUsecase(); + mockGetDriverDataUsecase = MockGetDriverDataUsecase(); + mockLocationUsecase = MockLocationUsecase(); + + provideDummy>>( + SuccessApiResult(data: Stream.empty()), + ); + provideDummy>>( + SuccessApiResult(data: Stream.empty()), + ); + provideDummy>(SuccessApiResult(data: null)); + provideDummy>>(SuccessApiResult(data: [])); + }); + + setUp(() { + cubit = OrderDetailsCubit( + mockGetOrderDetailsUsecase, + mockGetDriverDataUsecase, + mockLocationUsecase, + ); + }); + + tearDown(() { + cubit.close(); + }); + + final orderData = OrderModel( + orderId: '1', + driverId: '11', + userId: 'userId', + orderDetails: OrderDetailsModel( + items: [], + status: 'deliver', + totalPrice: 500, + pickupAddress: PickedAddressModel(name: 'name', address: 'address'), + orderId: '11', + userAddress: 'userAddress', + ), + userAddress: UserAddressModel( + name: 'name', + address: 'address', + userId: 'userId', + ), + ); + + final driverData = DriverDataModel( + id: 'id', + name: 'name', + phone: 'phone', + deviceToken: 'deviceToken', + currentLocation: DriverLocationModel(lat: 30, lng: 29), + ); + + final driverLocation = LatLng(30.0, 31.0); + final destination = LatLng(31.0, 32.0); + final polylines = [ + LatLng(30.0, 31.0), + LatLng(30.5, 31.5), + LatLng(31.0, 32.0), + ]; + group('get order details', () { + blocTest( + 'emits loading then success when order stream returns data', + build: () { + final controller = StreamController(); + + when( + mockGetOrderDetailsUsecase.call(), + ).thenAnswer((_) async => SuccessApiResult(data: controller.stream)); + + when(mockGetDriverDataUsecase.call(orderData.driverId)).thenReturn( + SuccessApiResult( + data: Stream.value( + DriverDataModel( + id: '', + name: '', + phone: '', + deviceToken: '', + currentLocation: DriverLocationModel(lat: 30, lng: 29), + ), + ), + ), + ); + + Future.microtask(() => controller.add(orderData)); + + return cubit; + }, + act: (cubit) => cubit.getOrderDetails(), + expect: () => [ + isA().having( + (s) => s.data?.status, + "status", + Status.loading, + ), + isA() + .having((s) => s.data?.status, "status", Status.success) + .having( + (s) => s.data?.data?.orderDetails.totalPrice, + "totalPrice", + 500, + ), + isA().having( + (s) => s.driverData?.status, + "driverStatus", + Status.loading, + ), + isA().having( + (s) => s.driverData?.status, + "driverStatus", + Status.success, + ), + ], + verify: (_) { + verify(mockGetOrderDetailsUsecase.call()).called(1); + verify(mockGetDriverDataUsecase.call('11')).called(1); + }, + ); + + blocTest( + 'emits loading then error when getOrderDetailsUsecase fails', + build: () { + when(mockGetOrderDetailsUsecase.call()).thenAnswer( + (_) async => ErrorApiResult>( + error: "Failed to fetch order", + ), + ); + + return cubit; + }, + act: (cubit) => cubit.getOrderDetails(), + expect: () => [ + isA().having( + (s) => s.data?.status, + "status", + Status.loading, + ), + isA().having( + (s) => s.data?.status, + "status", + Status.error, + ), + ], + verify: (_) { + verify(mockGetOrderDetailsUsecase.call()).called(1); + }, + ); + }); + + group('get driver details', () { + blocTest( + 'emits loading then success when driver stream returns data', + build: () { + final controller = StreamController(); + + when( + mockGetDriverDataUsecase.call(driverData.id), + ).thenReturn(SuccessApiResult(data: controller.stream)); + + Future.microtask(() => controller.add(driverData)); + + return cubit; + }, + act: (cubit) => cubit.getDriverData(driverData.id), + expect: () => [ + isA().having( + (s) => s.driverData?.status, + "driverStatus", + Status.loading, + ), + isA().having( + (s) => s.driverData?.status, + "driverStatus", + Status.success, + ), + ], + verify: (_) { + verify(mockGetDriverDataUsecase.call(driverData.id)).called(1); + }, + ); + + blocTest( + 'emits loading then error when getDriverDataUsecase fails', + build: () { + when(mockGetDriverDataUsecase.call(driverData.id)).thenReturn( + ErrorApiResult>( + error: "Failed to fetch order", + ), + ); + + return cubit; + }, + act: (cubit) => cubit.getDriverData(driverData.id), + expect: () => [ + isA().having( + (s) => s.driverData?.status, + "status", + Status.loading, + ), + isA().having( + (s) => s.driverData?.status, + "status", + Status.error, + ), + ], + verify: (_) { + verify(mockGetDriverDataUsecase.call(driverData.id)).called(1); + }, + ); + }); + + group('set destination', () { + blocTest( + 'emits destination then polylines when setDestinationFromAddress succeeds', + build: () { + when( + mockLocationUsecase.getAddress("Test Address"), + ).thenAnswer((_) async => SuccessApiResult(data: destination)); + + when( + mockLocationUsecase.getRealRoute(driverLocation, destination), + ).thenAnswer((_) async => SuccessApiResult(data: polylines)); + + return cubit; + }, + act: (cubit) => + cubit.setDestinationFromAddress("Test Address", driverLocation), + expect: () => [ + isA().having( + (s) => s.destination, + "destination", + destination, + ), + isA().having( + (s) => s.polylines, + "polylines", + polylines, + ), + ], + verify: (_) { + verify(mockLocationUsecase.getAddress("Test Address")).called(1); + verify( + mockLocationUsecase.getRealRoute(driverLocation, destination), + ).called(1); + }, + ); + }); + + group('get route', () { + blocTest( + 'emits polylines when getRoute succeeds', + build: () { + cubit.emit(cubit.state.copyWith(destination: destination)); + + when( + mockLocationUsecase.getRealRoute(driverLocation, destination), + ).thenAnswer((_) async => SuccessApiResult(data: polylines)); + + return cubit; + }, + act: (cubit) => cubit.getRoute(driverLocation), + expect: () => [ + isA().having( + (s) => s.polylines, + "polylines", + polylines, + ), + ], + verify: (_) { + verify( + mockLocationUsecase.getRealRoute(driverLocation, destination), + ).called(1); + }, + ); + }); +} diff --git a/test/features/driver_orders_details/presentation/pages/location_page_test.dart b/test/features/driver_orders_details/presentation/pages/location_page_test.dart new file mode 100644 index 0000000..4caf50f --- /dev/null +++ b/test/features/driver_orders_details/presentation/pages/location_page_test.dart @@ -0,0 +1,191 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_cubit.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_states.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/pages/location_page.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/widgets/address_card.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/widgets/section_title.dart'; + +import 'drivers_orders_details_page_test.mocks.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + late MockOrderDetailsCubit mockCubit; + final driverData = DriverDataModel( + deviceToken: '', + currentLocation: DriverLocationModel(lat: 30.0, lng: 31.0), + id: '', + name: '', + phone: '', + ); + setUp(() async { + await getIt.reset(); + mockCubit = MockOrderDetailsCubit(); + getIt.registerFactory(() => mockCubit); + when(mockCubit.state).thenReturn(OrderDetailsStates()); + when(mockCubit.stream).thenAnswer((_) => const Stream.empty()); + }); + + Widget buildTestableWidget() { + return EasyLocalization( + supportedLocales: const [Locale('en')], + path: 'assets/translations', + fallbackLocale: const Locale('en'), + startLocale: const Locale('en'), + saveLocale: false, + child: Builder( + builder: (context) { + return MaterialApp( + home: BlocProvider.value( + value: mockCubit, + child: const LocationPage(locationType: 'pickup'), + ), + ); + }, + ), + ); + } + + group('Location Page widget test', () { + testWidgets('LocationPage shows loading indicator when driver is null', ( + WidgetTester tester, + ) async { + when( + mockCubit.state, + ).thenReturn(OrderDetailsStates(driverData: Resource.loading())); + when(mockCubit.stream).thenAnswer( + (_) => Stream.value(OrderDetailsStates(driverData: Resource.loading())), + ); + + await tester.pumpWidget(buildTestableWidget()); + await tester.pump(); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + }); + testWidgets('Full LocationPage interaction and listener coverage', ( + tester, + ) async { + final orderData = OrderModel( + userAddress: UserAddressModel(name: '', address: '', userId: ''), + orderId: '', + driverId: '', + userId: '', + orderDetails: OrderDetailsModel( + items: [], + status: '', + totalPrice: 500, + pickupAddress: PickedAddressModel(name: '', address: ''), + orderId: '', + userAddress: '', + ), + ); + + final fullState = OrderDetailsStates( + driverData: Resource.success(driverData), + data: Resource.success(orderData), + polylines: [LatLng(30.0, 31.0), LatLng(30.1, 31.1)], + destination: LatLng(30.1, 31.1), + ); + + when(mockCubit.state).thenReturn(fullState); + when(mockCubit.stream).thenAnswer((_) => Stream.value(fullState)); + when( + mockCubit.setDestinationFromAddress(any, any), + ).thenAnswer((_) async {}); + await tester.pumpWidget(buildTestableWidget()); + await tester.pumpAndSettle(); + + final map = tester.widget(find.byType(GoogleMap)); + expect(map.mapType, MapType.normal); + expect(map.initialCameraPosition.zoom, 18); + + expect(fullState.polylines, isNotEmpty); + expect(fullState.destination, isNotNull); + + verify(mockCubit.setDestinationFromAddress(any, any)).called(1); + }); + + testWidgets('LocationPage shows GoogleMap when driver exists', ( + WidgetTester tester, + ) async { + when(mockCubit.state).thenReturn( + OrderDetailsStates(driverData: Resource.success(driverData)), + ); + when(mockCubit.stream).thenAnswer( + (_) => Stream.value( + OrderDetailsStates( + driverData: Resource.success(driverData), + data: Resource.success(null), + ), + ), + ); + + await tester.pumpWidget(buildTestableWidget()); + await tester.pump(); + + expect(find.byType(Padding), findsWidgets); + expect(find.byType(Scaffold), findsOneWidget); + expect(find.byType(Column), findsWidgets); + expect(find.byType(SizedBox), findsWidgets); + expect(find.byType(GoogleMap), findsOneWidget); + expect( + find.descendant( + of: find.byType(Expanded), + matching: find.byType(GoogleMap), + ), + findsOneWidget, + ); + expect( + find.descendant( + of: find.byType(Stack), + matching: find.byType(GoogleMap), + ), + findsOneWidget, + ); + expect(find.byType(Positioned), findsWidgets); + expect(find.byType(InkWell), findsAtLeast(1)); + expect(find.byType(CircleAvatar), findsNWidgets(3)); + expect(find.byType(AddressCard), findsWidgets); + expect( + find.descendant( + of: find.byType(Column), + matching: find.byType(AddressCard), + ), + findsWidgets, + ); + expect(find.byType(SectionTitle), findsWidgets); + expect( + find.descendant( + of: find.byType(Column), + matching: find.byType(SectionTitle), + ), + findsWidgets, + ); + }); + + testWidgets('Back button is displayed', (WidgetTester tester) async { + when(mockCubit.state).thenReturn( + OrderDetailsStates(driverData: Resource.success(driverData)), + ); + when(mockCubit.stream).thenAnswer( + (_) => Stream.value( + OrderDetailsStates(driverData: Resource.success(driverData)), + ), + ); + + await tester.pumpWidget(buildTestableWidget()); + await tester.pump(); + + expect(find.byIcon(Icons.arrow_back_ios_new), findsOneWidget); + await tester.pump(); + }); + }); +} From 3cf1c061e0cb14b9a8635b40344c9b2efe24297c Mon Sep 17 00:00:00 2001 From: mariam Date: Tue, 10 Mar 2026 23:48:47 +0200 Subject: [PATCH 7/9] feat(SCRUM-98): refactor tests --- lib/app/config/di/di.config.dart | 25 ++--- .../order_details_remote_datasource_impl.dart | 84 ++++++++-------- .../data/repos/order_details_repo_impl.dart | 5 +- .../domain/repos/order_details_repo.dart | 11 +-- .../manager/order_details_cubit.dart | 95 +++++++------------ ...r_details_remote_datasource_impl_test.dart | 2 - .../manager/order_details_cubit_test.dart | 31 +++++- 7 files changed, 124 insertions(+), 129 deletions(-) diff --git a/lib/app/config/di/di.config.dart b/lib/app/config/di/di.config.dart index bac407a..630b64d 100644 --- a/lib/app/config/di/di.config.dart +++ b/lib/app/config/di/di.config.dart @@ -209,13 +209,6 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i708.AuthRemoteDataSource>( () => _i777.AuthRemoteDataSourceImpl(gh<_i890.ApiClient>()), ); - gh.factory<_i375.OrderDetailsCubit>( - () => _i375.OrderDetailsCubit( - gh<_i1045.GetOrderDetailsUsecase>(), - gh<_i883.GetDriverDataUsecase>(), - gh<_i449.LocationUsecase>(), - ), - ); gh.factory<_i712.AuthRepo>( () => _i566.AuthRepoImpl(gh<_i708.AuthRemoteDataSource>()), ); @@ -231,14 +224,6 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i112.VerifyResetCodeUsecase>( () => _i112.VerifyResetCodeUsecase(gh<_i712.AuthRepo>()), ); - gh.factory<_i375.OrderDetailsCubit>( - () => _i375.OrderDetailsCubit( - gh<_i1045.GetOrderDetailsUsecase>(), - gh<_i727.UpdateOrderStateUsecase>(), - gh<_i809.PushNotificationUsecase>(), - gh<_i44.SendDeviceNotificationUsecase>(), - ), - ); gh.factoryParam<_i466.VerifyResetCodeCubit, String, dynamic>( (email, _) => _i466.VerifyResetCodeCubit( gh<_i112.VerifyResetCodeUsecase>(), @@ -259,6 +244,16 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i499.DriverOrderRepo>( () => _i1020.DriverOrderRepositoryImpl(gh<_i743.DriverOrderDataSource>()), ); + gh.factory<_i375.OrderDetailsCubit>( + () => _i375.OrderDetailsCubit( + gh<_i1045.GetOrderDetailsUsecase>(), + gh<_i883.GetDriverDataUsecase>(), + gh<_i449.LocationUsecase>(), + gh<_i727.UpdateOrderStateUsecase>(), + gh<_i809.PushNotificationUsecase>(), + gh<_i44.SendDeviceNotificationUsecase>(), + ), + ); gh.factory<_i863.ProfileRepo>( () => _i1048.ProfileRepoImpl( gh<_i943.ProfileRemoteDatasource>(), diff --git a/lib/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart b/lib/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart index 12e40d0..5d0f3a6 100644 --- a/lib/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart +++ b/lib/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart @@ -5,7 +5,6 @@ import 'package:injectable/injectable.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:tracking_app/features/driver_orders_details/data/models/drivers_dto.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/services.dart'; import 'package:googleapis_auth/auth_io.dart'; import 'package:tracking_app/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart'; @@ -18,8 +17,8 @@ class OrderDetailsRemoteDatasourceImpl implements OrderDetailsRemoteDatasource { OrderDetailsRemoteDatasourceImpl({ required FirebaseFirestore firestore, required Dio dio, - }) : _firestore = firestore, - _dio = dio; + }) : _dio = dio, + _firestore = firestore; @override ApiResult> getOrderStream(String orderId) { @@ -57,34 +56,13 @@ class OrderDetailsRemoteDatasourceImpl implements OrderDetailsRemoteDatasource { return SuccessApiResult>(data: stream); } catch (e) { return ErrorApiResult>(error: e.toString()); - Future> updateOrderState({ - required String orderId, - required String state, - }) async { - try { - final querySnapshot = await _firestore - .collection('orders') - .where('orderId', isEqualTo: orderId) - .get(); - if (querySnapshot.docs.isNotEmpty) { - await querySnapshot.docs.first.reference.update({ - 'oder_dt.status': state, - }); - } else { - await _firestore.collection('orders').doc(orderId).update({ - 'oder_dt.status': state, - }); - } - return SuccessApiResult(data: null); - } catch (e) { - return ErrorApiResult(error: e.toString()); } } @override Future> getLatLngFromAddress(String address) async { try { - final response = await dio.get( + final response = await _dio.get( "https://nominatim.openstreetmap.org/search", queryParameters: { "q": "$address, Egypt", @@ -108,18 +86,6 @@ class OrderDetailsRemoteDatasourceImpl implements OrderDetailsRemoteDatasource { return SuccessApiResult(data: null); } catch (e) { return ErrorApiResult(error: e.toString()); - Future> pushNotification({ - required String title, - required String des, - }) async { - try { - await _firestore.collection('notification').add({ - 'title': title, - 'des': des, - }); - return SuccessApiResult(data: null); - } catch (e) { - return ErrorApiResult(error: e.toString()); } } @@ -129,7 +95,7 @@ class OrderDetailsRemoteDatasourceImpl implements OrderDetailsRemoteDatasource { LatLng destination, ) async { try { - final response = await dio.get( + final response = await _dio.get( "https://router.project-osrm.org/route/v1/driving/" "${myLocation.longitude},${myLocation.latitude};" "${destination.longitude},${destination.latitude}", @@ -155,6 +121,24 @@ class OrderDetailsRemoteDatasourceImpl implements OrderDetailsRemoteDatasource { return ErrorApiResult>(error: 'No route found'); } catch (e) { return ErrorApiResult>(error: e.toString()); + } + } + + Future> pushNotification({ + required String title, + required String des, + }) async { + try { + await _firestore.collection('notification').add({ + 'title': title, + 'des': des, + }); + return SuccessApiResult(data: null); + } catch (e) { + return ErrorApiResult(error: e.toString()); + } + } + Future> sendDeviceNotification({ required String userId, required String title, @@ -232,4 +216,28 @@ class OrderDetailsRemoteDatasourceImpl implements OrderDetailsRemoteDatasource { return ErrorApiResult(error: e.toString()); } } + + Future> updateOrderState({ + required String orderId, + required String state, + }) async { + try { + final querySnapshot = await _firestore + .collection('orders') + .where('orderId', isEqualTo: orderId) + .get(); + if (querySnapshot.docs.isNotEmpty) { + await querySnapshot.docs.first.reference.update({ + 'oder_dt.status': state, + }); + } else { + await _firestore.collection('orders').doc(orderId).update({ + 'oder_dt.status': state, + }); + } + return SuccessApiResult(data: null); + } catch (e) { + return ErrorApiResult(error: e.toString()); + } + } } diff --git a/lib/features/driver_orders_details/data/repos/order_details_repo_impl.dart b/lib/features/driver_orders_details/data/repos/order_details_repo_impl.dart index 9d4bdd1..3dcf0f7 100644 --- a/lib/features/driver_orders_details/data/repos/order_details_repo_impl.dart +++ b/lib/features/driver_orders_details/data/repos/order_details_repo_impl.dart @@ -13,9 +13,6 @@ import 'package:tracking_app/features/driver_orders_details/domain/models/notfic import 'package:tracking_app/features/driver_orders_details/domain/models/orderStates.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; import 'package:tracking_app/features/driver_orders_details/domain/repos/order_details_repo.dart'; -import 'package:tracking_app/features/driver_orders_details/domain/usecases/update_order_state_usecase.dart'; -import 'package:tracking_app/features/driver_orders_details/domain/usecases/push_notification_usecase.dart'; -import 'package:tracking_app/features/driver_orders_details/domain/usecases/send_device_notification_usecase.dart'; @Injectable(as: OrderDetailsRepo) class OrderDetailsRepoImpl implements OrderDetailsRepo { @@ -66,6 +63,8 @@ class OrderDetailsRepoImpl implements OrderDetailsRepo { LatLng destination, ) { return _remoteDataSource.getRealRoute(myLocation, destination); + } + Future> updateOrderState( UpdateOrderStateParams params, ) async { diff --git a/lib/features/driver_orders_details/domain/repos/order_details_repo.dart b/lib/features/driver_orders_details/domain/repos/order_details_repo.dart index 39bab3f..ddfd4fe 100644 --- a/lib/features/driver_orders_details/domain/repos/order_details_repo.dart +++ b/lib/features/driver_orders_details/domain/repos/order_details_repo.dart @@ -1,6 +1,9 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/notcicationModel.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/notficationDevice.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orderStates.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; abstract class OrderDetailsRepo { @@ -12,13 +15,7 @@ abstract class OrderDetailsRepo { Future>> getRealRoute( LatLng myLocation, LatLng destination, -import 'package:tracking_app/features/driver_orders_details/domain/models/notcicationModel.dart'; -import 'package:tracking_app/features/driver_orders_details/domain/models/notficationDevice.dart'; -import 'package:tracking_app/features/driver_orders_details/domain/models/orderStates.dart'; -import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; - -abstract class OrderDetailsRepo { - ApiResult> getOrderDetails(String orderId); + ); Future> updateOrderState(UpdateOrderStateParams params); Future> pushNotification(PushNotificationParams params); Future> sendDeviceNotification( diff --git a/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart b/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart index 1cba78b..9469907 100644 --- a/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart +++ b/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart @@ -2,7 +2,9 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/config/di/di.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; @@ -11,7 +13,6 @@ import 'package:tracking_app/features/driver_orders_details/domain/usecases/loca import 'package:tracking_app/features/driver_orders_details/domain/models/notcicationModel.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/notficationDevice.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/orderStates.dart'; -import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; import 'package:tracking_app/features/driver_orders_details/domain/usecases/push_notification_usecase.dart'; import 'package:tracking_app/features/driver_orders_details/domain/usecases/send_device_notification_usecase.dart'; import '../../domain/usecases/get_order_details_usecase.dart'; @@ -23,6 +24,9 @@ import 'order_details_states.dart'; class OrderDetailsCubit extends Cubit { final GetOrderDetailsUsecase _getOrderDetailsUsecase; final GetDriverDataUsecase _getDriverDataUsecase; + final UpdateOrderStateUsecase _updateOrderStateUsecase; + final PushNotificationUsecase _pushNotificationUsecase; + final SendDeviceNotificationUsecase _sendDeviceNotificationUsecase; final LocationUsecase _locationUsecase; StreamSubscription? _orderSubscription; StreamSubscription? _driverSubscription; @@ -31,29 +35,23 @@ class OrderDetailsCubit extends Cubit { this._getOrderDetailsUsecase, this._getDriverDataUsecase, this._locationUsecase, - final UpdateOrderStateUsecase _updateOrderStateUsecase; - final PushNotificationUsecase _pushNotificationUsecase; - final SendDeviceNotificationUsecase _sendDeviceNotificationUsecase; - StreamSubscription? _subscription; - final _authStorage = getIt(); - - OrderDetailsCubit( - this._getOrderDetailsUsecase, this._updateOrderStateUsecase, this._pushNotificationUsecase, this._sendDeviceNotificationUsecase, ) : super(OrderDetailsStates()); + final _authStorage = getIt(); + void onIntent(OrderDetailsIntent intent) { switch (intent) { case GetOrderDetails(): - _getOrderDetails(); + getOrderDetails(); case UpdateOrderState(currentStatus: final status): _updateOrderState(status); } } - void _getOrderDetails() async { + void getOrderDetails() async { emit(state.copyWith(data: Resource.loading())); _orderSubscription?.cancel(); @@ -70,31 +68,6 @@ class OrderDetailsCubit extends Cubit { onError: (error) { emit(state.copyWith(data: Resource.error(error.toString()))); }, - _subscription?.cancel(); - - try { - final orderId = await _authStorage.getOrderId(); - if (orderId == null || orderId.isEmpty) { - emit(state.copyWith(data: Resource.error('Order ID not found'))); - return; - } - - final result = _getOrderDetailsUsecase.call(orderId); - - if (result is SuccessApiResult>) { - _subscription = result.data.listen( - (order) => emit(state.copyWith(data: Resource.success(order))), - onError: (error) => - emit(state.copyWith(data: Resource.error(error.toString()))), - ); - } else if (result is ErrorApiResult>) { - emit(state.copyWith(data: Resource.error(result.error))); - } - } catch (e) { - emit( - state.copyWith( - data: Resource.error('Error retrieving order details: $e'), - ), ); } else if (result is ErrorApiResult>) { emit(state.copyWith(data: Resource.error(result.error))); @@ -114,6 +87,18 @@ class OrderDetailsCubit extends Cubit { } } + Future getRoute(LatLng driverLocation) async { + if (state.destination == null) return; + + final result = await _locationUsecase.getRealRoute( + driverLocation, + state.destination!, + ); + if (result is SuccessApiResult>) { + emit(state.copyWith(polylines: result.data)); + } + } + Future setDestinationFromAddress( String address, LatLng driverLocation, @@ -125,15 +110,19 @@ class OrderDetailsCubit extends Cubit { } } - Future getRoute(LatLng driverLocation) async { - if (state.destination == null) return; - - final result = await _locationUsecase.getRealRoute( - driverLocation, - state.destination!, - ); - if (result is SuccessApiResult>) { - emit(state.copyWith(polylines: result.data)); + String? _nextStateFor(String currentStatus) { + switch (currentStatus.toLowerCase()) { + case 'pending': + case 'accepted': + return 'Picked'; + case 'picked': + return 'Out for delivery'; + case 'out for delivery': + return 'Arrived'; + case 'arrived': + return 'Delivered'; + default: + return null; } } @@ -169,22 +158,6 @@ class OrderDetailsCubit extends Cubit { } } - String? _nextStateFor(String currentStatus) { - switch (currentStatus.toLowerCase()) { - case 'pending': - case 'accepted': - return 'Picked'; - case 'picked': - return 'Out for delivery'; - case 'out for delivery': - return 'Arrived'; - case 'arrived': - return 'Delivered'; - default: - return null; - } - } - @override Future close() { _orderSubscription?.cancel(); diff --git a/test/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl_test.dart b/test/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl_test.dart index 1835e87..be1433a 100644 --- a/test/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl_test.dart +++ b/test/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl_test.dart @@ -4,7 +4,6 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:dio/dio.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart'; import 'package:tracking_app/features/driver_orders_details/data/models/drivers_dto.dart'; @@ -25,7 +24,6 @@ void main() { late MockCollectionReference> mockCollection; late MockDocumentReference> mockDocument; late MockDocumentSnapshot> mockSnapshot; - late MockDio mockDio; const String tOrderId = 'pxkMaEmWYVuvV5jkW0JK'; const String driverId = '6989f35de364ef61405211a0'; diff --git a/test/features/driver_orders_details/presentation/manager/order_details_cubit_test.dart b/test/features/driver_orders_details/presentation/manager/order_details_cubit_test.dart index 4d64044..8081b6b 100644 --- a/test/features/driver_orders_details/presentation/manager/order_details_cubit_test.dart +++ b/test/features/driver_orders_details/presentation/manager/order_details_cubit_test.dart @@ -1,34 +1,53 @@ import 'dart:async'; - import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/config/di/di.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; import 'package:tracking_app/features/driver_orders_details/domain/usecases/get_driver_data_usecase.dart'; import 'package:tracking_app/features/driver_orders_details/domain/usecases/get_order_details_usecase.dart'; import 'package:tracking_app/features/driver_orders_details/domain/usecases/location_usecase.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/usecases/push_notification_usecase.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/usecases/send_device_notification_usecase.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/usecases/update_order_state_usecase.dart'; import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_cubit.dart'; import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_states.dart'; import 'order_details_cubit_test.mocks.dart'; -@GenerateMocks([GetOrderDetailsUsecase, GetDriverDataUsecase, LocationUsecase]) +@GenerateMocks([ + GetOrderDetailsUsecase, + GetDriverDataUsecase, + LocationUsecase, + PushNotificationUsecase, + UpdateOrderStateUsecase, + SendDeviceNotificationUsecase, + AuthStorage, +]) void main() { late OrderDetailsCubit cubit; - late MockGetOrderDetailsUsecase mockGetOrderDetailsUsecase; late MockGetDriverDataUsecase mockGetDriverDataUsecase; late MockLocationUsecase mockLocationUsecase; + late MockUpdateOrderStateUsecase _updateOrderStateUsecase; + late MockPushNotificationUsecase _pushNotificationUsecase; + late MockSendDeviceNotificationUsecase _sendDeviceNotificationUsecase; + late MockAuthStorage authStorage; setUpAll(() { mockGetOrderDetailsUsecase = MockGetOrderDetailsUsecase(); mockGetDriverDataUsecase = MockGetDriverDataUsecase(); mockLocationUsecase = MockLocationUsecase(); + _updateOrderStateUsecase = MockUpdateOrderStateUsecase(); + _pushNotificationUsecase = MockPushNotificationUsecase(); + _sendDeviceNotificationUsecase = MockSendDeviceNotificationUsecase(); + authStorage = MockAuthStorage(); provideDummy>>( SuccessApiResult(data: Stream.empty()), @@ -41,15 +60,21 @@ void main() { }); setUp(() { + getIt.registerSingleton(authStorage); + cubit = OrderDetailsCubit( mockGetOrderDetailsUsecase, mockGetDriverDataUsecase, mockLocationUsecase, + _updateOrderStateUsecase, + _pushNotificationUsecase, + _sendDeviceNotificationUsecase, ); }); tearDown(() { cubit.close(); + getIt.reset(); }); final orderData = OrderModel( From e1b3e6a36b2dca239a418255b1c9cb3de7814c22 Mon Sep 17 00:00:00 2001 From: mariam Date: Sat, 14 Mar 2026 00:51:25 +0200 Subject: [PATCH 8/9] feat(SCRUM-98): implement driver location update functionality --- .../order_details_remote_datasource_impl.dart | 11 ++++++ .../order_details_remote_datasource.dart | 3 +- .../data/repos/order_details_repo_impl.dart | 9 +++++ .../domain/repos/order_details_repo.dart | 3 +- .../domain/usecases/location_usecase.dart | 4 ++ .../manager/order_details_cubit.dart | 39 +++++++++++++++++++ 6 files changed, 65 insertions(+), 4 deletions(-) diff --git a/lib/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart b/lib/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart index 5d0f3a6..9e2dc51 100644 --- a/lib/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart +++ b/lib/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart @@ -124,6 +124,17 @@ class OrderDetailsRemoteDatasourceImpl implements OrderDetailsRemoteDatasource { } } + @override + Future updateDriverLocation( + String driverId, + double lat, + double lng, + ) async { + await FirebaseFirestore.instance.collection('drivers').doc(driverId).update( + {"currentLocation.lat": lat, "currentLocation.lng": lng}, + ); + } + Future> pushNotification({ required String title, required String des, diff --git a/lib/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart b/lib/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart index 5ee2be0..0fe9f94 100644 --- a/lib/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart +++ b/lib/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart @@ -6,9 +6,8 @@ import 'package:tracking_app/features/driver_orders_details/data/models/orders_d abstract class OrderDetailsRemoteDatasource { ApiResult> getOrderStream(String orderId); ApiResult> getDriverData(String driverId); - Future> getLatLngFromAddress(String address); - + Future updateDriverLocation(String driverId, double lat, double lng); Future>> getRealRoute( LatLng myLocation, LatLng destination, diff --git a/lib/features/driver_orders_details/data/repos/order_details_repo_impl.dart b/lib/features/driver_orders_details/data/repos/order_details_repo_impl.dart index 3dcf0f7..437dfbb 100644 --- a/lib/features/driver_orders_details/data/repos/order_details_repo_impl.dart +++ b/lib/features/driver_orders_details/data/repos/order_details_repo_impl.dart @@ -94,4 +94,13 @@ class OrderDetailsRepoImpl implements OrderDetailsRepo { body: params.body, ); } + + @override + Future updateDriverLocation( + String driverId, + double lat, + double lng, + ) async { + return _remoteDataSource.updateDriverLocation(driverId, lat, lng); + } } diff --git a/lib/features/driver_orders_details/domain/repos/order_details_repo.dart b/lib/features/driver_orders_details/domain/repos/order_details_repo.dart index ddfd4fe..b53698b 100644 --- a/lib/features/driver_orders_details/domain/repos/order_details_repo.dart +++ b/lib/features/driver_orders_details/domain/repos/order_details_repo.dart @@ -9,9 +9,8 @@ import 'package:tracking_app/features/driver_orders_details/domain/models/orders abstract class OrderDetailsRepo { Future>> getOrderDetails(); ApiResult> getDriverData(String driverId); - Future> getLatLngFromAddress(String address); - + Future updateDriverLocation(String driverId, double lat, double lng); Future>> getRealRoute( LatLng myLocation, LatLng destination, diff --git a/lib/features/driver_orders_details/domain/usecases/location_usecase.dart b/lib/features/driver_orders_details/domain/usecases/location_usecase.dart index c881b2c..358e4fe 100644 --- a/lib/features/driver_orders_details/domain/usecases/location_usecase.dart +++ b/lib/features/driver_orders_details/domain/usecases/location_usecase.dart @@ -19,4 +19,8 @@ class LocationUsecase { ) { return _repo.getRealRoute(driverLocation, destination); } + + Future updateDriverLocation(String driverId, double lat, double lng) { + return _repo.updateDriverLocation(driverId, lat, lng); + } } diff --git a/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart b/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart index 9469907..d310550 100644 --- a/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart +++ b/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart @@ -30,6 +30,7 @@ class OrderDetailsCubit extends Cubit { final LocationUsecase _locationUsecase; StreamSubscription? _orderSubscription; StreamSubscription? _driverSubscription; + Timer? _driverMoveTimer; OrderDetailsCubit( this._getOrderDetailsUsecase, @@ -106,10 +107,47 @@ class OrderDetailsCubit extends Cubit { final result = await _locationUsecase.getAddress(address); if (result is SuccessApiResult && result.data != null) { emit(state.copyWith(destination: result.data)); + startDriverSimulation(); await getRoute(driverLocation); } } + LatLng moveTowards(LatLng current, LatLng destination, double step) { + double latDiff = destination.latitude - current.latitude; + double lngDiff = destination.longitude - current.longitude; + + double newLat = current.latitude + (latDiff * step); + double newLng = current.longitude + (lngDiff * step); + + return LatLng(newLat, newLng); + } + + void startDriverSimulation() { + _driverMoveTimer?.cancel(); + + _driverMoveTimer = Timer.periodic(const Duration(seconds: 10), ( + timer, + ) async { + final driver = state.driverData?.data; + final destination = state.destination; + + if (driver == null || destination == null) return; + + LatLng current = LatLng( + driver.currentLocation.lat, + driver.currentLocation.lng, + ); + + LatLng newLocation = moveTowards(current, destination, 0.05); + + await _locationUsecase.updateDriverLocation( + driver.id, + newLocation.latitude, + newLocation.longitude, + ); + }); + } + String? _nextStateFor(String currentStatus) { switch (currentStatus.toLowerCase()) { case 'pending': @@ -162,6 +200,7 @@ class OrderDetailsCubit extends Cubit { Future close() { _orderSubscription?.cancel(); _driverSubscription?.cancel(); + _driverMoveTimer?.cancel(); return super.close(); } } From e17b80b2f400cf71a8510b6a32b62e8ab62b6fd2 Mon Sep 17 00:00:00 2001 From: mariam Date: Mon, 16 Mar 2026 18:07:31 +0200 Subject: [PATCH 9/9] feat(SCRUM-98): enhance cubit and use cases --- assets/translations/ar.json | 4 +- assets/translations/en.json | 3 +- lib/app/config/di/di.config.dart | 40 ++- lib/app/core/router/app_router.dart | 9 +- lib/app/core/utils/app_launcher.dart | 2 +- .../domain/models/location_type.dart | 1 + .../domain/usecases/get_address_usecase.dart | 15 + ...ecase.dart => get_real_route_usecase.dart} | 12 +- .../update_driver_location_usecase.dart | 13 + .../manager/order_details_cubit.dart | 47 ++- .../pages/drivers_orders_details_page.dart | 304 ++++++++-------- .../presentation/pages/location_page.dart | 19 +- lib/generated/locale_keys.g.dart | 9 +- .../usecases/location_usecase_test.dart | 79 ----- .../manager/order_details_cubit_test.dart | 329 ------------------ .../pages/location_page_test.dart | 3 +- 16 files changed, 278 insertions(+), 611 deletions(-) create mode 100644 lib/features/driver_orders_details/domain/models/location_type.dart create mode 100644 lib/features/driver_orders_details/domain/usecases/get_address_usecase.dart rename lib/features/driver_orders_details/domain/usecases/{location_usecase.dart => get_real_route_usecase.dart} (61%) create mode 100644 lib/features/driver_orders_details/domain/usecases/update_driver_location_usecase.dart delete mode 100644 test/features/driver_orders_details/domain/usecases/location_usecase_test.dart delete mode 100644 test/features/driver_orders_details/presentation/manager/order_details_cubit_test.dart diff --git a/assets/translations/ar.json b/assets/translations/ar.json index 74f94c3..390c475 100644 --- a/assets/translations/ar.json +++ b/assets/translations/ar.json @@ -265,5 +265,7 @@ "btnArrivedAtPickupPoint": "وصلت الى نقطة الالتقاء", "btnStartDeliver": "بدء التوصيل", "btnArrivedToUser": "وصلت إلى المستخدم", - "btnDeliveredToUser": "تم التوصيل للمستخدم" + "btnDeliveredToUser": "تم التوصيل للمستخدم", + "finishYourOrder": "عليك انهاء توصيل الطلب اولا" + } \ No newline at end of file diff --git a/assets/translations/en.json b/assets/translations/en.json index f6f1011..20db732 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -268,5 +268,6 @@ "btnArrivedAtPickupPoint": "Arrived at Pickup point", "btnStartDeliver": "Start deliver", "btnArrivedToUser": "Arrived to the user", - "btnDeliveredToUser": "Delivered to the user" + "btnDeliveredToUser": "Delivered to the user", + "finishYourOrder": "You must finish the order first" } \ No newline at end of file diff --git a/lib/app/config/di/di.config.dart b/lib/app/config/di/di.config.dart index 630b64d..4891c79 100644 --- a/lib/app/config/di/di.config.dart +++ b/lib/app/config/di/di.config.dart @@ -61,16 +61,20 @@ import '../../../features/driver_orders_details/data/repos/order_details_repo_im as _i55; import '../../../features/driver_orders_details/domain/repos/order_details_repo.dart' as _i313; +import '../../../features/driver_orders_details/domain/usecases/get_address_usecase.dart' + as _i453; import '../../../features/driver_orders_details/domain/usecases/get_driver_data_usecase.dart' as _i883; import '../../../features/driver_orders_details/domain/usecases/get_order_details_usecase.dart' as _i1045; -import '../../../features/driver_orders_details/domain/usecases/location_usecase.dart' - as _i449; +import '../../../features/driver_orders_details/domain/usecases/get_real_route_usecase.dart' + as _i707; import '../../../features/driver_orders_details/domain/usecases/push_notification_usecase.dart' as _i809; import '../../../features/driver_orders_details/domain/usecases/send_device_notification_usecase.dart' as _i44; +import '../../../features/driver_orders_details/domain/usecases/update_driver_location_usecase.dart' + as _i294; import '../../../features/driver_orders_details/domain/usecases/update_order_state_usecase.dart' as _i727; import '../../../features/driver_orders_details/presentation/manager/order_details_cubit.dart' @@ -174,8 +178,14 @@ extension GetItInjectableX on _i174.GetIt { gh<_i603.AuthStorage>(), ), ); - gh.factory<_i449.LocationUsecase>( - () => _i449.LocationUsecase(gh<_i313.OrderDetailsRepo>()), + gh.factory<_i453.GetAddressUsecase>( + () => _i453.GetAddressUsecase(gh<_i313.OrderDetailsRepo>()), + ); + gh.factory<_i707.GetRealRouteUsecase>( + () => _i707.GetRealRouteUsecase(gh<_i313.OrderDetailsRepo>()), + ); + gh.factory<_i294.UpdateDriverLocationUsecase>( + () => _i294.UpdateDriverLocationUsecase(gh<_i313.OrderDetailsRepo>()), ); gh.factory<_i919.MyOrdersRepo>( () => _i754.MyOrdersRepoImpl(gh<_i466.MyOrdersRemoteDataSource>()), @@ -212,6 +222,18 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i712.AuthRepo>( () => _i566.AuthRepoImpl(gh<_i708.AuthRemoteDataSource>()), ); + gh.factory<_i375.OrderDetailsCubit>( + () => _i375.OrderDetailsCubit( + gh<_i1045.GetOrderDetailsUsecase>(), + gh<_i883.GetDriverDataUsecase>(), + gh<_i453.GetAddressUsecase>(), + gh<_i707.GetRealRouteUsecase>(), + gh<_i294.UpdateDriverLocationUsecase>(), + gh<_i727.UpdateOrderStateUsecase>(), + gh<_i809.PushNotificationUsecase>(), + gh<_i44.SendDeviceNotificationUsecase>(), + ), + ); gh.factory<_i991.ChangePasswordUsecase>( () => _i991.ChangePasswordUsecase(gh<_i712.AuthRepo>()), ); @@ -244,16 +266,6 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i499.DriverOrderRepo>( () => _i1020.DriverOrderRepositoryImpl(gh<_i743.DriverOrderDataSource>()), ); - gh.factory<_i375.OrderDetailsCubit>( - () => _i375.OrderDetailsCubit( - gh<_i1045.GetOrderDetailsUsecase>(), - gh<_i883.GetDriverDataUsecase>(), - gh<_i449.LocationUsecase>(), - gh<_i727.UpdateOrderStateUsecase>(), - gh<_i809.PushNotificationUsecase>(), - gh<_i44.SendDeviceNotificationUsecase>(), - ), - ); gh.factory<_i863.ProfileRepo>( () => _i1048.ProfileRepoImpl( gh<_i943.ProfileRemoteDatasource>(), diff --git a/lib/app/core/router/app_router.dart b/lib/app/core/router/app_router.dart index 3fae5ef..ff9bb45 100644 --- a/lib/app/core/router/app_router.dart +++ b/lib/app/core/router/app_router.dart @@ -4,8 +4,10 @@ import 'package:tracking_app/app/config/di/di.dart'; import 'package:tracking_app/app/core/router/route_names.dart'; import 'package:tracking_app/features/Onboarding/presentation/pages/onboardingScreen.dart'; import 'package:tracking_app/features/app_sections/presentation/pages/app_sections.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/location_type.dart'; import 'package:tracking_app/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart'; import 'package:tracking_app/features/driver_orders_details/presentation/pages/location_page.dart'; +import 'package:tracking_app/features/home/presentation/pages/driverOrderScreen.dart'; import 'package:tracking_app/features/profile/data/models/driver_model.dart'; import 'package:tracking_app/features/profile/presentation/pages/edit_driver_profile_page.dart'; import 'package:tracking_app/features/profile/presentation/pages/edit_vehicle_page.dart'; @@ -15,7 +17,6 @@ import 'package:tracking_app/features/my_orders/presentation/pages/order_details import 'package:tracking_app/features/auth/presentation/apply/view/apply_view.dart'; import 'package:tracking_app/features/auth/presentation/forget_pass/manager/cubit/forget_pass_cubit.dart'; import 'package:tracking_app/features/auth/presentation/forget_pass/pages/forget_pass_page.dart'; -import 'package:tracking_app/features/auth/presentation/login/pages/loginScreen.dart'; import 'package:tracking_app/features/auth/presentation/reset_password/manager/reset_password_cubit.dart'; import 'package:tracking_app/features/auth/presentation/reset_password/pages/change_password_page.dart'; import 'package:tracking_app/features/auth/presentation/reset_password/pages/reset_password.dart'; @@ -23,7 +24,7 @@ import 'package:tracking_app/features/auth/presentation/verify_reset/manger/cubi import 'package:tracking_app/features/auth/presentation/verify_reset/pages/verify_reset_page.dart'; final GoRouter appRouter = GoRouter( - initialLocation: RouteNames.ordersDetailsPage, + initialLocation: RouteNames.login, routes: [ GoRoute( path: RouteNames.changePassword, @@ -37,7 +38,7 @@ final GoRouter appRouter = GoRouter( GoRoute( path: RouteNames.login, - builder: (context, state) => const LoginScreen(), + builder: (context, state) => const DriverOrderScreen(), ), GoRoute( @@ -115,7 +116,7 @@ final GoRouter appRouter = GoRouter( GoRoute( path: RouteNames.locationPage, builder: (context, state) { - final locationType = state.extra as String; + final locationType = state.extra as LocationType; return LocationPage(locationType: locationType); }, ), diff --git a/lib/app/core/utils/app_launcher.dart b/lib/app/core/utils/app_launcher.dart index 0468568..a096dff 100644 --- a/lib/app/core/utils/app_launcher.dart +++ b/lib/app/core/utils/app_launcher.dart @@ -1,6 +1,6 @@ import 'package:url_launcher/url_launcher.dart'; -class AppLauncher { +abstract class AppLauncher { static void launchPhone(String phoneNumber) async { final Uri url = Uri(scheme: 'tel', path: phoneNumber); if (await canLaunchUrl(url)) { diff --git a/lib/features/driver_orders_details/domain/models/location_type.dart b/lib/features/driver_orders_details/domain/models/location_type.dart new file mode 100644 index 0000000..4572c33 --- /dev/null +++ b/lib/features/driver_orders_details/domain/models/location_type.dart @@ -0,0 +1 @@ +enum LocationType { pickup, user } diff --git a/lib/features/driver_orders_details/domain/usecases/get_address_usecase.dart b/lib/features/driver_orders_details/domain/usecases/get_address_usecase.dart new file mode 100644 index 0000000..85f8158 --- /dev/null +++ b/lib/features/driver_orders_details/domain/usecases/get_address_usecase.dart @@ -0,0 +1,15 @@ +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/repos/order_details_repo.dart'; + +@injectable +class GetAddressUsecase { + final OrderDetailsRepo _repo; + + GetAddressUsecase(this._repo); + + Future> getAddress(String address) { + return _repo.getLatLngFromAddress(address); + } +} diff --git a/lib/features/driver_orders_details/domain/usecases/location_usecase.dart b/lib/features/driver_orders_details/domain/usecases/get_real_route_usecase.dart similarity index 61% rename from lib/features/driver_orders_details/domain/usecases/location_usecase.dart rename to lib/features/driver_orders_details/domain/usecases/get_real_route_usecase.dart index 358e4fe..46ee447 100644 --- a/lib/features/driver_orders_details/domain/usecases/location_usecase.dart +++ b/lib/features/driver_orders_details/domain/usecases/get_real_route_usecase.dart @@ -4,14 +4,10 @@ import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/driver_orders_details/domain/repos/order_details_repo.dart'; @injectable -class LocationUsecase { +class GetRealRouteUsecase { final OrderDetailsRepo _repo; - LocationUsecase(this._repo); - - Future> getAddress(String address) { - return _repo.getLatLngFromAddress(address); - } + GetRealRouteUsecase(this._repo); Future>> getRealRoute( LatLng driverLocation, @@ -19,8 +15,4 @@ class LocationUsecase { ) { return _repo.getRealRoute(driverLocation, destination); } - - Future updateDriverLocation(String driverId, double lat, double lng) { - return _repo.updateDriverLocation(driverId, lat, lng); - } } diff --git a/lib/features/driver_orders_details/domain/usecases/update_driver_location_usecase.dart b/lib/features/driver_orders_details/domain/usecases/update_driver_location_usecase.dart new file mode 100644 index 0000000..70d735d --- /dev/null +++ b/lib/features/driver_orders_details/domain/usecases/update_driver_location_usecase.dart @@ -0,0 +1,13 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/repos/order_details_repo.dart'; + +@injectable +class UpdateDriverLocationUsecase { + final OrderDetailsRepo _repo; + + UpdateDriverLocationUsecase(this._repo); + + Future updateDriverLocation(String driverId, double lat, double lng) { + return _repo.updateDriverLocation(driverId, lat, lng); + } +} diff --git a/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart b/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart index d310550..a604918 100644 --- a/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart +++ b/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart @@ -9,12 +9,14 @@ import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; import 'package:tracking_app/features/driver_orders_details/domain/usecases/get_driver_data_usecase.dart'; -import 'package:tracking_app/features/driver_orders_details/domain/usecases/location_usecase.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/usecases/get_address_usecase.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/notcicationModel.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/notficationDevice.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/orderStates.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/usecases/get_real_route_usecase.dart'; import 'package:tracking_app/features/driver_orders_details/domain/usecases/push_notification_usecase.dart'; import 'package:tracking_app/features/driver_orders_details/domain/usecases/send_device_notification_usecase.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/usecases/update_driver_location_usecase.dart'; import '../../domain/usecases/get_order_details_usecase.dart'; import '../../domain/usecases/update_order_state_usecase.dart'; import 'order_details_intents.dart'; @@ -27,7 +29,9 @@ class OrderDetailsCubit extends Cubit { final UpdateOrderStateUsecase _updateOrderStateUsecase; final PushNotificationUsecase _pushNotificationUsecase; final SendDeviceNotificationUsecase _sendDeviceNotificationUsecase; - final LocationUsecase _locationUsecase; + final GetAddressUsecase _getAddressUsecase; + final GetRealRouteUsecase _getRealRouteUsecase; + final UpdateDriverLocationUsecase _updateDriverLocationUsecase; StreamSubscription? _orderSubscription; StreamSubscription? _driverSubscription; Timer? _driverMoveTimer; @@ -35,7 +39,9 @@ class OrderDetailsCubit extends Cubit { OrderDetailsCubit( this._getOrderDetailsUsecase, this._getDriverDataUsecase, - this._locationUsecase, + this._getAddressUsecase, + this._getRealRouteUsecase, + this._updateDriverLocationUsecase, this._updateOrderStateUsecase, this._pushNotificationUsecase, this._sendDeviceNotificationUsecase, @@ -91,12 +97,14 @@ class OrderDetailsCubit extends Cubit { Future getRoute(LatLng driverLocation) async { if (state.destination == null) return; - final result = await _locationUsecase.getRealRoute( + final result = await _getRealRouteUsecase.getRealRoute( driverLocation, state.destination!, ); + if (result is SuccessApiResult>) { emit(state.copyWith(polylines: result.data)); + startDriverSimulation(); } } @@ -104,7 +112,7 @@ class OrderDetailsCubit extends Cubit { String address, LatLng driverLocation, ) async { - final result = await _locationUsecase.getAddress(address); + final result = await _getAddressUsecase.getAddress(address); if (result is SuccessApiResult && result.data != null) { emit(state.copyWith(destination: result.data)); startDriverSimulation(); @@ -125,7 +133,7 @@ class OrderDetailsCubit extends Cubit { void startDriverSimulation() { _driverMoveTimer?.cancel(); - _driverMoveTimer = Timer.periodic(const Duration(seconds: 10), ( + _driverMoveTimer = Timer.periodic(const Duration(seconds: 3), ( timer, ) async { final driver = state.driverData?.data; @@ -133,18 +141,31 @@ class OrderDetailsCubit extends Cubit { if (driver == null || destination == null) return; - LatLng current = LatLng( + LatLng currentLocation = LatLng( driver.currentLocation.lat, driver.currentLocation.lng, ); - LatLng newLocation = moveTowards(current, destination, 0.05); - - await _locationUsecase.updateDriverLocation( - driver.id, - newLocation.latitude, - newLocation.longitude, + final result = await _getRealRouteUsecase.getRealRoute( + currentLocation, + destination, ); + + if (result is SuccessApiResult>) { + final route = result.data; + + if (route.isEmpty) return; + + final nextPoint = route.length > 2 ? route[1] : route.first; + + await _updateDriverLocationUsecase.updateDriverLocation( + driver.id, + nextPoint.latitude, + nextPoint.longitude, + ); + + emit(state.copyWith(polylines: route)); + } }); } diff --git a/lib/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart b/lib/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart index da19ace..b29b827 100644 --- a/lib/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart +++ b/lib/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart @@ -8,6 +8,7 @@ import 'package:tracking_app/app/core/router/route_names.dart'; import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; import 'package:tracking_app/app/core/values/paths.dart'; import 'package:tracking_app/app/core/widgets/custom_button.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/location_type.dart'; import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_cubit.dart'; import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_intents.dart'; import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_states.dart'; @@ -23,163 +24,186 @@ class DriversOrdersDetailsPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios, color: AppColors.blackColor), - onPressed: () => context.pop(), - ), - title: Text( - LocaleKeys.orderDetails.tr(), - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 20, - color: AppColors.blackColor, + final order = getIt().state.data?.data; + final status = OrderStatus.fromString(order?.orderDetails.status); + + return PopScope( + canPop: status == OrderStatus.delivered, + onPopInvoked: (didPop) { + if (!didPop && status != OrderStatus.delivered) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(LocaleKeys.finishYourOrder.tr())), + ); + } + }, + child: Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: AppColors.blackColor), + onPressed: () { + if (status == OrderStatus.delivered) { + context.pop(); + } + }, + ), + title: Text( + LocaleKeys.orderDetails.tr(), + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 20, + color: AppColors.blackColor, + ), ), ), - ), - body: BlocProvider( - create: (context) => - getIt()..onIntent(GetOrderDetails()), - child: BlocBuilder( - builder: (context, state) { - if (state.data?.status == Status.loading) { - return const Center(child: CircularProgressIndicator()); - } else if (state.data?.status == Status.error) { - return Center(child: Text(state.data!.error.toString())); - } else if (state.data?.status == Status.success) { - final order = state.data!.data; - final status = OrderStatus.fromString(order?.orderDetails.status); + body: BlocProvider( + create: (context) => + getIt()..onIntent(GetOrderDetails()), + child: BlocBuilder( + builder: (context, state) { + if (state.data?.status == Status.loading) { + return const Center(child: CircularProgressIndicator()); + } else if (state.data?.status == Status.error) { + return Center(child: Text(state.data!.error.toString())); + } else if (state.data?.status == Status.success) { + final order = state.data!.data; + final status = OrderStatus.fromString( + order?.orderDetails.status, + ); - int currentStep = status.step; - return SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: List.generate(5, (index) { - return Expanded( - child: Container( - height: 4, - margin: const EdgeInsets.symmetric(horizontal: 2), - decoration: BoxDecoration( - color: index < currentStep - ? AppColors.green - : AppColors.lightGrey, - borderRadius: BorderRadius.circular(2), + int currentStep = status.step; + return SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: List.generate(5, (index) { + return Expanded( + child: Container( + height: 4, + margin: const EdgeInsets.symmetric(horizontal: 2), + decoration: BoxDecoration( + color: index < currentStep + ? AppColors.green + : AppColors.lightGrey, + borderRadius: BorderRadius.circular(2), + ), ), - ), - ); - }), - ), - const SizedBox(height: 20), - - Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppColors.lightPink, - borderRadius: BorderRadius.circular(12), + ); + }), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${LocaleKeys.status.tr()}${order?.orderDetails.status}', - style: TextStyle( - color: AppColors.green, - fontWeight: FontWeight.bold, - fontSize: 16, + const SizedBox(height: 20), + + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.lightPink, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${LocaleKeys.status.tr()}${order?.orderDetails.status}', + style: TextStyle( + color: AppColors.green, + fontWeight: FontWeight.bold, + fontSize: 16, + ), ), - ), - const SizedBox(height: 4), - Text( - '${LocaleKeys.orderId.tr()}${order?.orderId}', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, + const SizedBox(height: 4), + Text( + '${LocaleKeys.orderId.tr()}${order?.orderId}', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), ), - ), - const SizedBox(height: 4), - Text( - 'Wed, 03 Sep 2024, 11:00 AM', - style: TextStyle( - color: AppColors.grey, - fontSize: 14, + const SizedBox(height: 4), + Text( + 'Wed, 03 Sep 2024, 11:00 AM', + style: TextStyle( + color: AppColors.grey, + fontSize: 14, + ), ), - ), - ], + ], + ), ), - ), - const SizedBox(height: 24), + const SizedBox(height: 24), - SectionTitle(title: LocaleKeys.pickupAddress.tr()), - InkWell( - onTap: () => context.push( - RouteNames.locationPage, - extra: 'pickup', - ), - child: AddressCard( - title: order?.orderDetails.pickupAddress.name ?? '', - address: - order?.orderDetails.pickupAddress.address ?? '', - imagePath: AppPaths.flowerLogo, - phoneNumber: (state.driverData?.data?.phone).toString(), + SectionTitle(title: LocaleKeys.pickupAddress.tr()), + InkWell( + onTap: () => context.push( + RouteNames.locationPage, + extra: LocationType.pickup, + ), + child: AddressCard( + title: order?.orderDetails.pickupAddress.name ?? '', + address: + order?.orderDetails.pickupAddress.address ?? '', + imagePath: AppPaths.flowerLogo, + phoneNumber: (state.driverData?.data?.phone) + .toString(), + ), ), - ), - const SizedBox(height: 16), - SectionTitle(title: LocaleKeys.userAddress.tr()), - InkWell( - onTap: () => - context.push(RouteNames.locationPage, extra: 'user'), - child: AddressCard( - title: order?.userAddress.name ?? '', - address: order?.userAddress.address ?? '', - imagePath: AppPaths.flowerLogo, - phoneNumber: (state.driverData?.data?.phone).toString(), + const SizedBox(height: 16), + SectionTitle(title: LocaleKeys.userAddress.tr()), + InkWell( + onTap: () => context.push( + RouteNames.locationPage, + extra: LocationType.user, + ), + child: AddressCard( + title: order?.userAddress.name ?? '', + address: order?.userAddress.address ?? '', + imagePath: AppPaths.flowerLogo, + phoneNumber: (state.driverData?.data?.phone) + .toString(), + ), ), - ), - const SizedBox(height: 24), + const SizedBox(height: 24), - SectionTitle(title: LocaleKeys.orderDetails.tr()), - OrderItems(), - const SizedBox(height: 16), + SectionTitle(title: LocaleKeys.orderDetails.tr()), + OrderItems(), + const SizedBox(height: 16), - BottomRowSection( - label: LocaleKeys.total.tr(), - value: - '${LocaleKeys.egp.tr()} ${order?.orderDetails.totalPrice.toStringAsFixed(2)}', - ), - BottomRowSection( - label: LocaleKeys.payment_method.tr(), - value: LocaleKeys.cash_on_delivery.tr(), - ), + BottomRowSection( + label: LocaleKeys.total.tr(), + value: + '${LocaleKeys.egp.tr()} ${order?.orderDetails.totalPrice.toStringAsFixed(2)}', + ), + BottomRowSection( + label: LocaleKeys.payment_method.tr(), + value: LocaleKeys.cash_on_delivery.tr(), + ), - const SizedBox(height: 32), + const SizedBox(height: 32), - SizedBox( - width: double.infinity, - height: 55, - child: CustomButton( - isEnabled: status != OrderStatus.delivered, - onPressed: () { - if (status != OrderStatus.delivered && - order != null) { - context.read().onIntent( - UpdateOrderState(order.orderDetails.status), - ); - } - }, - isLoading: false, - text: status.buttonTextKey.tr(), + SizedBox( + width: double.infinity, + height: 55, + child: CustomButton( + isEnabled: status != OrderStatus.delivered, + onPressed: () { + if (status != OrderStatus.delivered && + order != null) { + context.read().onIntent( + UpdateOrderState(order.orderDetails.status), + ); + } + }, + isLoading: false, + text: status.buttonTextKey.tr(), + ), ), - ), - ], - ), - ); - } - return const SizedBox.shrink(); - }, + ], + ), + ); + } + return const SizedBox.shrink(); + }, + ), ), ), ); diff --git a/lib/features/driver_orders_details/presentation/pages/location_page.dart b/lib/features/driver_orders_details/presentation/pages/location_page.dart index 254fd04..93cf845 100644 --- a/lib/features/driver_orders_details/presentation/pages/location_page.dart +++ b/lib/features/driver_orders_details/presentation/pages/location_page.dart @@ -9,6 +9,7 @@ import 'package:tracking_app/app/config/di/di.dart'; import 'package:tracking_app/app/core/ui_helper/assets/images.dart'; import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; import 'package:tracking_app/app/core/values/paths.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/location_type.dart'; import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_cubit.dart'; import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_states.dart'; import 'package:tracking_app/features/driver_orders_details/presentation/widgets/address_card.dart'; @@ -16,7 +17,7 @@ import 'package:tracking_app/features/driver_orders_details/presentation/widgets import 'package:tracking_app/generated/locale_keys.g.dart'; class LocationPage extends StatefulWidget { - final String locationType; + final LocationType locationType; const LocationPage({super.key, required this.locationType}); @override @@ -51,7 +52,7 @@ class _LocationPageState extends State { driverIcon = await getMarkerIcon(Assets.driverLocation); destinationIcon = await getMarkerIcon( - widget.locationType == 'pickup' + widget.locationType == LocationType.pickup ? Assets.floweryLocation : Assets.userLocation, ); @@ -97,7 +98,7 @@ class _LocationPageState extends State { ); String address; - if (widget.locationType == 'pickup') { + if (widget.locationType == LocationType.pickup) { address = order.orderDetails.pickupAddress.address; } else { address = order.userAddress.address; @@ -125,16 +126,6 @@ class _LocationPageState extends State { }; } setState(() {}); - - print( - '<<<<<<<<< driverLocation ${driverLocation.latitude}, ${driverLocation.longitude}', - ); - print( - '<<<<<<<<< pickupAddress ${state.data?.data?.orderDetails.pickupAddress.address}', - ); - print( - '<<<<<<<<< userAddress ${state.data?.data?.userAddress.address.toString()}', - ); }, builder: (context, state) { @@ -170,7 +161,7 @@ class _LocationPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (widget.locationType == 'pickup') ...[ + if (widget.locationType == LocationType.pickup) ...[ SectionTitle(title: LocaleKeys.pickupAddress.tr()), AddressCard( title: diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index a5dd194..d0907a7 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -269,8 +269,9 @@ abstract class LocaleKeys { static const reject = 'reject'; static const noPendingOrders = 'noPendingOrders'; static const floweryRider = 'floweryRider'; - static const btnArrivedAtPickupPoint = 'ArrivedAtPickupPoint'; - static const btnStartDeliver = 'StartDeliver'; - static const btnArrivedToUser = 'ArrivedToUser'; - static const btnDeliveredToUser = 'DeliveredToUser'; + static const btnArrivedAtPickupPoint = 'btnArrivedAtPickupPoint'; + static const btnStartDeliver = 'btnStartDeliver'; + static const btnArrivedToUser = 'btnArrivedToUser'; + static const btnDeliveredToUser = 'btnDeliveredToUser'; + static const finishYourOrder = 'finishYourOrder'; } diff --git a/test/features/driver_orders_details/domain/usecases/location_usecase_test.dart b/test/features/driver_orders_details/domain/usecases/location_usecase_test.dart deleted file mode 100644 index f688c54..0000000 --- a/test/features/driver_orders_details/domain/usecases/location_usecase_test.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; -import 'package:tracking_app/app/core/network/api_result.dart'; -import 'package:tracking_app/features/driver_orders_details/domain/repos/order_details_repo.dart'; -import 'package:tracking_app/features/driver_orders_details/domain/usecases/location_usecase.dart'; - -import 'get_order_details_usecase_test.mocks.dart'; - -@GenerateMocks([OrderDetailsRepo]) -void main() { - late LocationUsecase usecase; - late MockOrderDetailsRepo mockRepo; - - setUp(() { - mockRepo = MockOrderDetailsRepo(); - usecase = LocationUsecase(mockRepo); - provideDummy>(ErrorApiResult(error: 'dummy')); - provideDummy>>(ErrorApiResult(error: 'dummy')); - }); - - const address = 'Cairo'; - final tLatLng = LatLng(30.0, 31.0); - - group('LocationUsecase.getAddress test', () { - test( - 'should return SuccessApiResult containing the Stream from the repository when get address', - () async { - when( - mockRepo.getLatLngFromAddress(address), - ).thenAnswer((_) async => SuccessApiResult(data: tLatLng)); - - final result = await usecase.getAddress(address); - - expect(result, isA>()); - verify(mockRepo.getLatLngFromAddress(address)).called(1); - }, - ); - - test('should return ErrorApiResult when the repository fails', () async { - when( - mockRepo.getLatLngFromAddress(address), - ).thenAnswer((_) async => ErrorApiResult(error: 'Error from Repository')); - - final result = await usecase.getAddress(address); - - expect(result, isA>()); - expect((result as ErrorApiResult).error, 'Error from Repository'); - }); - }); - - group('LocationUsecase.getRealRoute test', () { - test( - 'should return SuccessApiResult containing the Stream from the repository', - () async { - when( - mockRepo.getRealRoute(tLatLng, tLatLng), - ).thenAnswer((_) async => SuccessApiResult(data: [tLatLng])); - - final result = await usecase.getRealRoute(tLatLng, tLatLng); - - expect(result, isA>>()); - verify(mockRepo.getRealRoute(tLatLng, tLatLng)).called(1); - }, - ); - - test('should return ErrorApiResult when the repository fails', () async { - when( - mockRepo.getRealRoute(tLatLng, tLatLng), - ).thenAnswer((_) async => ErrorApiResult(error: 'Error from Repository')); - - final result = await usecase.getRealRoute(tLatLng, tLatLng); - - expect(result, isA>>()); - expect((result as ErrorApiResult).error, 'Error from Repository'); - }); - }); -} diff --git a/test/features/driver_orders_details/presentation/manager/order_details_cubit_test.dart b/test/features/driver_orders_details/presentation/manager/order_details_cubit_test.dart deleted file mode 100644 index 8081b6b..0000000 --- a/test/features/driver_orders_details/presentation/manager/order_details_cubit_test.dart +++ /dev/null @@ -1,329 +0,0 @@ -import 'dart:async'; -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; -import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; -import 'package:tracking_app/app/config/base_state/base_state.dart'; -import 'package:tracking_app/app/config/di/di.dart'; -import 'package:tracking_app/app/core/network/api_result.dart'; -import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; -import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; -import 'package:tracking_app/features/driver_orders_details/domain/usecases/get_driver_data_usecase.dart'; -import 'package:tracking_app/features/driver_orders_details/domain/usecases/get_order_details_usecase.dart'; -import 'package:tracking_app/features/driver_orders_details/domain/usecases/location_usecase.dart'; -import 'package:tracking_app/features/driver_orders_details/domain/usecases/push_notification_usecase.dart'; -import 'package:tracking_app/features/driver_orders_details/domain/usecases/send_device_notification_usecase.dart'; -import 'package:tracking_app/features/driver_orders_details/domain/usecases/update_order_state_usecase.dart'; -import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_cubit.dart'; -import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_states.dart'; - -import 'order_details_cubit_test.mocks.dart'; - -@GenerateMocks([ - GetOrderDetailsUsecase, - GetDriverDataUsecase, - LocationUsecase, - PushNotificationUsecase, - UpdateOrderStateUsecase, - SendDeviceNotificationUsecase, - AuthStorage, -]) -void main() { - late OrderDetailsCubit cubit; - late MockGetOrderDetailsUsecase mockGetOrderDetailsUsecase; - late MockGetDriverDataUsecase mockGetDriverDataUsecase; - late MockLocationUsecase mockLocationUsecase; - late MockUpdateOrderStateUsecase _updateOrderStateUsecase; - late MockPushNotificationUsecase _pushNotificationUsecase; - late MockSendDeviceNotificationUsecase _sendDeviceNotificationUsecase; - late MockAuthStorage authStorage; - - setUpAll(() { - mockGetOrderDetailsUsecase = MockGetOrderDetailsUsecase(); - mockGetDriverDataUsecase = MockGetDriverDataUsecase(); - mockLocationUsecase = MockLocationUsecase(); - _updateOrderStateUsecase = MockUpdateOrderStateUsecase(); - _pushNotificationUsecase = MockPushNotificationUsecase(); - _sendDeviceNotificationUsecase = MockSendDeviceNotificationUsecase(); - authStorage = MockAuthStorage(); - - provideDummy>>( - SuccessApiResult(data: Stream.empty()), - ); - provideDummy>>( - SuccessApiResult(data: Stream.empty()), - ); - provideDummy>(SuccessApiResult(data: null)); - provideDummy>>(SuccessApiResult(data: [])); - }); - - setUp(() { - getIt.registerSingleton(authStorage); - - cubit = OrderDetailsCubit( - mockGetOrderDetailsUsecase, - mockGetDriverDataUsecase, - mockLocationUsecase, - _updateOrderStateUsecase, - _pushNotificationUsecase, - _sendDeviceNotificationUsecase, - ); - }); - - tearDown(() { - cubit.close(); - getIt.reset(); - }); - - final orderData = OrderModel( - orderId: '1', - driverId: '11', - userId: 'userId', - orderDetails: OrderDetailsModel( - items: [], - status: 'deliver', - totalPrice: 500, - pickupAddress: PickedAddressModel(name: 'name', address: 'address'), - orderId: '11', - userAddress: 'userAddress', - ), - userAddress: UserAddressModel( - name: 'name', - address: 'address', - userId: 'userId', - ), - ); - - final driverData = DriverDataModel( - id: 'id', - name: 'name', - phone: 'phone', - deviceToken: 'deviceToken', - currentLocation: DriverLocationModel(lat: 30, lng: 29), - ); - - final driverLocation = LatLng(30.0, 31.0); - final destination = LatLng(31.0, 32.0); - final polylines = [ - LatLng(30.0, 31.0), - LatLng(30.5, 31.5), - LatLng(31.0, 32.0), - ]; - group('get order details', () { - blocTest( - 'emits loading then success when order stream returns data', - build: () { - final controller = StreamController(); - - when( - mockGetOrderDetailsUsecase.call(), - ).thenAnswer((_) async => SuccessApiResult(data: controller.stream)); - - when(mockGetDriverDataUsecase.call(orderData.driverId)).thenReturn( - SuccessApiResult( - data: Stream.value( - DriverDataModel( - id: '', - name: '', - phone: '', - deviceToken: '', - currentLocation: DriverLocationModel(lat: 30, lng: 29), - ), - ), - ), - ); - - Future.microtask(() => controller.add(orderData)); - - return cubit; - }, - act: (cubit) => cubit.getOrderDetails(), - expect: () => [ - isA().having( - (s) => s.data?.status, - "status", - Status.loading, - ), - isA() - .having((s) => s.data?.status, "status", Status.success) - .having( - (s) => s.data?.data?.orderDetails.totalPrice, - "totalPrice", - 500, - ), - isA().having( - (s) => s.driverData?.status, - "driverStatus", - Status.loading, - ), - isA().having( - (s) => s.driverData?.status, - "driverStatus", - Status.success, - ), - ], - verify: (_) { - verify(mockGetOrderDetailsUsecase.call()).called(1); - verify(mockGetDriverDataUsecase.call('11')).called(1); - }, - ); - - blocTest( - 'emits loading then error when getOrderDetailsUsecase fails', - build: () { - when(mockGetOrderDetailsUsecase.call()).thenAnswer( - (_) async => ErrorApiResult>( - error: "Failed to fetch order", - ), - ); - - return cubit; - }, - act: (cubit) => cubit.getOrderDetails(), - expect: () => [ - isA().having( - (s) => s.data?.status, - "status", - Status.loading, - ), - isA().having( - (s) => s.data?.status, - "status", - Status.error, - ), - ], - verify: (_) { - verify(mockGetOrderDetailsUsecase.call()).called(1); - }, - ); - }); - - group('get driver details', () { - blocTest( - 'emits loading then success when driver stream returns data', - build: () { - final controller = StreamController(); - - when( - mockGetDriverDataUsecase.call(driverData.id), - ).thenReturn(SuccessApiResult(data: controller.stream)); - - Future.microtask(() => controller.add(driverData)); - - return cubit; - }, - act: (cubit) => cubit.getDriverData(driverData.id), - expect: () => [ - isA().having( - (s) => s.driverData?.status, - "driverStatus", - Status.loading, - ), - isA().having( - (s) => s.driverData?.status, - "driverStatus", - Status.success, - ), - ], - verify: (_) { - verify(mockGetDriverDataUsecase.call(driverData.id)).called(1); - }, - ); - - blocTest( - 'emits loading then error when getDriverDataUsecase fails', - build: () { - when(mockGetDriverDataUsecase.call(driverData.id)).thenReturn( - ErrorApiResult>( - error: "Failed to fetch order", - ), - ); - - return cubit; - }, - act: (cubit) => cubit.getDriverData(driverData.id), - expect: () => [ - isA().having( - (s) => s.driverData?.status, - "status", - Status.loading, - ), - isA().having( - (s) => s.driverData?.status, - "status", - Status.error, - ), - ], - verify: (_) { - verify(mockGetDriverDataUsecase.call(driverData.id)).called(1); - }, - ); - }); - - group('set destination', () { - blocTest( - 'emits destination then polylines when setDestinationFromAddress succeeds', - build: () { - when( - mockLocationUsecase.getAddress("Test Address"), - ).thenAnswer((_) async => SuccessApiResult(data: destination)); - - when( - mockLocationUsecase.getRealRoute(driverLocation, destination), - ).thenAnswer((_) async => SuccessApiResult(data: polylines)); - - return cubit; - }, - act: (cubit) => - cubit.setDestinationFromAddress("Test Address", driverLocation), - expect: () => [ - isA().having( - (s) => s.destination, - "destination", - destination, - ), - isA().having( - (s) => s.polylines, - "polylines", - polylines, - ), - ], - verify: (_) { - verify(mockLocationUsecase.getAddress("Test Address")).called(1); - verify( - mockLocationUsecase.getRealRoute(driverLocation, destination), - ).called(1); - }, - ); - }); - - group('get route', () { - blocTest( - 'emits polylines when getRoute succeeds', - build: () { - cubit.emit(cubit.state.copyWith(destination: destination)); - - when( - mockLocationUsecase.getRealRoute(driverLocation, destination), - ).thenAnswer((_) async => SuccessApiResult(data: polylines)); - - return cubit; - }, - act: (cubit) => cubit.getRoute(driverLocation), - expect: () => [ - isA().having( - (s) => s.polylines, - "polylines", - polylines, - ), - ], - verify: (_) { - verify( - mockLocationUsecase.getRealRoute(driverLocation, destination), - ).called(1); - }, - ); - }); -} diff --git a/test/features/driver_orders_details/presentation/pages/location_page_test.dart b/test/features/driver_orders_details/presentation/pages/location_page_test.dart index 4caf50f..a373b24 100644 --- a/test/features/driver_orders_details/presentation/pages/location_page_test.dart +++ b/test/features/driver_orders_details/presentation/pages/location_page_test.dart @@ -7,6 +7,7 @@ import 'package:mockito/mockito.dart'; import 'package:tracking_app/app/config/base_state/base_state.dart'; import 'package:tracking_app/app/config/di/di.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/location_type.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_cubit.dart'; import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_states.dart'; @@ -46,7 +47,7 @@ void main() { return MaterialApp( home: BlocProvider.value( value: mockCubit, - child: const LocationPage(locationType: 'pickup'), + child: const LocationPage(locationType: LocationType.pickup), ), ); },