diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/controller/UserController.java b/walkguide-backend/demo/src/main/java/com/walkguide/controller/UserController.java index 65f3d72..e1af2b1 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/controller/UserController.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/controller/UserController.java @@ -114,6 +114,16 @@ public class UserController { "SOS dikirim! Guardian sudah diberitahu.")); } + @GetMapping("/sos-events") + public ResponseEntity>> getSosEvents( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { + return ResponseEntity.ok(ApiResponse.ok( + sosService.getSosEvents(SecurityHelper.getCurrentUserId(), + PageRequest.of(page, size)), + "Riwayat SOS")); + } + @GetMapping("/activity-logs") public ResponseEntity>> getActivityLogs( @RequestParam(defaultValue = "0") int page, diff --git a/walkguide-backend/demo/src/main/resources/openapi.yaml b/walkguide-backend/demo/src/main/resources/openapi.yaml index 7b028ae..f2c469a 100644 --- a/walkguide-backend/demo/src/main/resources/openapi.yaml +++ b/walkguide-backend/demo/src/main/resources/openapi.yaml @@ -183,6 +183,10 @@ paths: post: responses: "200": { description: SOS triggered } + /user/sos-events: + get: + responses: + "200": { description: User SOS history } /user/activity-logs: get: responses: diff --git a/walkguide-backend/demo/src/test/java/com/walkguide/DemoApplicationTests.java b/walkguide-backend/demo/src/test/java/com/walkguide/DemoApplicationTests.java index f0d87ef..8b3d779 100644 --- a/walkguide-backend/demo/src/test/java/com/walkguide/DemoApplicationTests.java +++ b/walkguide-backend/demo/src/test/java/com/walkguide/DemoApplicationTests.java @@ -1,11 +1,23 @@ package com.walkguide; +import com.walkguide.config.DataSeeder; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; -@SpringBootTest +@SpringBootTest(properties = { + "spring.datasource.url=jdbc:postgresql://localhost:5432/walkguide_test", + "spring.datasource.username=test", + "spring.datasource.password=test", + "spring.flyway.enabled=false", + "spring.jpa.hibernate.ddl-auto=none", + "jwt.secret=404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970" +}) class DemoApplicationTests { + @MockBean + private DataSeeder dataSeeder; + @Test void contextLoads() { } diff --git a/walkguide-backend/demo/src/test/java/com/walkguide/controller/UserControllerTest.java b/walkguide-backend/demo/src/test/java/com/walkguide/controller/UserControllerTest.java index c393145..a012a28 100644 --- a/walkguide-backend/demo/src/test/java/com/walkguide/controller/UserControllerTest.java +++ b/walkguide-backend/demo/src/test/java/com/walkguide/controller/UserControllerTest.java @@ -281,6 +281,26 @@ class UserControllerTest { } } + @Test + @DisplayName("GET /api/v1/user/sos-events - harus return paginated riwayat SOS user") + void getSosEvents_shouldReturn200() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L); + + Page page = new PageImpl<>(List.of()); + when(sosService.getSosEvents(eq(1L), any(PageRequest.class))).thenReturn(page); + + mockMvc.perform(get("/api/v1/user/sos-events") + .param("page", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value("Riwayat SOS")); + + verify(sosService).getSosEvents(eq(1L), any(PageRequest.class)); + } + } + // ===== ACTIVITY LOGS ===== @Test diff --git a/walkguide-mobile/walkguide_app/android/app/build.gradle.kts b/walkguide-mobile/walkguide_app/android/app/build.gradle.kts index a1771e9..ae4ef59 100644 --- a/walkguide-mobile/walkguide_app/android/app/build.gradle.kts +++ b/walkguide-mobile/walkguide_app/android/app/build.gradle.kts @@ -13,6 +13,7 @@ android { compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 + isCoreLibraryDesugaringEnabled = true } kotlinOptions { @@ -24,7 +25,7 @@ android { applicationId = "com.example.walkguide_app" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = flutter.minSdkVersion + minSdk = 26 targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName @@ -42,3 +43,7 @@ android { flutter { source = "../.." } + +dependencies { + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4") +} diff --git a/walkguide-mobile/walkguide_app/android/build.gradle.kts b/walkguide-mobile/walkguide_app/android/build.gradle.kts index dbee657..b93f306 100644 --- a/walkguide-mobile/walkguide_app/android/build.gradle.kts +++ b/walkguide-mobile/walkguide_app/android/build.gradle.kts @@ -15,6 +15,42 @@ subprojects { val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) project.layout.buildDirectory.value(newSubprojectBuildDir) } +subprojects { + if (project.name == "agora_rtc_engine" || project.name == "iris_method_channel") { + tasks.configureEach { + if (name.startsWith("configureCMake") || name.startsWith("buildCMake")) { + doFirst { + val cmakeFile = listOf( + project.file("src/main/cpp/CMakeLists.txt"), + project.file("../src/CMakeLists.txt"), + ).firstOrNull { it.exists() } + + if (cmakeFile != null) { + val text = cmakeFile.readText() + if (!text.contains("c++_shared")) { + val patchedText = + if (text.contains("target_link_libraries")) { + text.replace( + " EGL\n )", + " EGL\n c++_shared\n )", + ) + } else { + text + """ + +target_link_libraries(${'$'}{LIBRARY_NAME} + PRIVATE + c++_shared + ) +""" + } + cmakeFile.writeText(patchedText) + } + } + } + } + } + } +} subprojects { project.evaluationDependsOn(":app") } diff --git a/walkguide-mobile/walkguide_app/android/gradle.properties b/walkguide-mobile/walkguide_app/android/gradle.properties index f018a61..7ed25f5 100644 --- a/walkguide-mobile/walkguide_app/android/gradle.properties +++ b/walkguide-mobile/walkguide_app/android/gradle.properties @@ -1,3 +1,4 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true +kotlin.incremental=false diff --git a/walkguide-mobile/walkguide_app/dart_test.yaml b/walkguide-mobile/walkguide_app/dart_test.yaml new file mode 100644 index 0000000..6d8e711 --- /dev/null +++ b/walkguide-mobile/walkguide_app/dart_test.yaml @@ -0,0 +1 @@ +concurrency: 1 diff --git a/walkguide-mobile/walkguide_app/lib/core/ai/obstacle_analyzer.dart b/walkguide-mobile/walkguide_app/lib/core/ai/obstacle_analyzer.dart index 8d06ca9..8eb6dd6 100644 --- a/walkguide-mobile/walkguide_app/lib/core/ai/obstacle_analyzer.dart +++ b/walkguide-mobile/walkguide_app/lib/core/ai/obstacle_analyzer.dart @@ -1,5 +1,23 @@ enum ObstacleDirection { left, center, right } +class BoundingBox { + final double left; + final double top; + final double right; + final double bottom; + + const BoundingBox({ + required this.left, + required this.top, + required this.right, + required this.bottom, + }); + + double get width => right - left; + double get height => bottom - top; + double get centerX => left + width / 2; +} + class DetectionResult { final String label; final double confidence; @@ -26,12 +44,73 @@ class DetectionResult { } class ObstacleAnalyzer { - DetectionResult analyzeFallback({String label = 'person', double confidence = 0.86}) { + static const double frameWidth = 640.0; + static const double frameHeight = 480.0; + + ObstacleDirection analyzeDirection(BoundingBox box) { + final cx = box.centerX; + if (cx < frameWidth * 0.33) return ObstacleDirection.left; + if (cx > frameWidth * 0.67) return ObstacleDirection.right; + return ObstacleDirection.center; + } + + String estimateDistance(BoundingBox box) { + final ratio = box.height / frameHeight; + if (ratio > 0.60) return 'Very Close (< 1m)'; + if (ratio > 0.35) return 'Close (1-2m)'; + if (ratio > 0.15) return 'Medium (2-4m)'; + return 'Far (> 4m)'; + } + + String buildTtsMessage(DetectionResult result) { + final directionLabel = switch (result.direction) { + ObstacleDirection.left => 'kiri', + ObstacleDirection.center => 'depan', + ObstacleDirection.right => 'kanan', + }; + return 'Hati-hati, ${result.label} di $directionLabel. ' + 'Jarak ${result.estimatedDistance}.'; + } + + DetectionResult? prioritize(List detections) { + if (detections.isEmpty) return null; + const order = [ + 'Very Close (< 1m)', + 'Very Close', + 'Close (1-2m)', + 'Close', + 'Medium (2-4m)', + 'Medium', + 'Far (> 4m)', + 'Far', + ]; + final sorted = List.of(detections); + sorted.sort((a, b) { + final ai = order.indexOf(a.estimatedDistance); + final bi = order.indexOf(b.estimatedDistance); + final aRank = ai == -1 ? order.length : ai; + final bRank = bi == -1 ? order.length : bi; + return aRank.compareTo(bRank); + }); + return sorted.first; + } + + List filterByConfidence( + List detections, + double threshold, + ) { + return detections.where((d) => d.confidence >= threshold).toList(); + } + + DetectionResult analyzeFallback({ + String label = 'person', + double confidence = 0.86, + }) { return DetectionResult( label: label, confidence: confidence, direction: ObstacleDirection.center, - estimatedDistance: 'Close', + estimatedDistance: 'Close (1-2m)', ); } } diff --git a/walkguide-mobile/walkguide_app/lib/core/constants/app_constants.dart b/walkguide-mobile/walkguide_app/lib/core/constants/app_constants.dart index e16a5df..cc6cbbf 100644 --- a/walkguide-mobile/walkguide_app/lib/core/constants/app_constants.dart +++ b/walkguide-mobile/walkguide_app/lib/core/constants/app_constants.dart @@ -61,6 +61,7 @@ class AppConstants { await prefs.setString(_selectedYoloModelKey, path); } - // Agora - ganti dengan App ID dari agora.io - static const String agoraAppId = 'YOUR_AGORA_APP_ID'; + // Agora App ID diisi saat build: --dart-define=AGORA_APP_ID=... + static const String agoraAppId = + String.fromEnvironment('AGORA_APP_ID', defaultValue: ''); } diff --git a/walkguide-mobile/walkguide_app/lib/core/services/call_service.dart b/walkguide-mobile/walkguide_app/lib/core/services/call_service.dart index b26c91b..c52e5f3 100644 --- a/walkguide-mobile/walkguide_app/lib/core/services/call_service.dart +++ b/walkguide-mobile/walkguide_app/lib/core/services/call_service.dart @@ -20,7 +20,7 @@ class CallService { } Future getPairedReceiverId() async { - final res = await _apiClient.dio.get('/pairing/status'); + final res = await _apiClient.dio.get('/shared/pairing/status'); final data = res.data['data']; if (data is! Map) return null; final id = data['pairedWithId']; @@ -72,6 +72,10 @@ class CallService { int uid = 0, }) async { try { + if (AppConstants.agoraAppId.isEmpty) { + debugPrint('Agora join skipped: AGORA_APP_ID is not configured'); + return false; + } _engine ??= createAgoraRtcEngine(); await _engine!.initialize(const RtcEngineContext(appId: AppConstants.agoraAppId)); await _engine!.enableAudio(); diff --git a/walkguide-mobile/walkguide_app/pubspec.lock b/walkguide-mobile/walkguide_app/pubspec.lock index a5f4313..0905ef5 100644 --- a/walkguide-mobile/walkguide_app/pubspec.lock +++ b/walkguide-mobile/walkguide_app/pubspec.lock @@ -947,10 +947,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mgrs_dart: dependency: transitive description: @@ -1264,13 +1264,13 @@ packages: source: hosted version: "1.2.2" record_linux: - dependency: transitive + dependency: "direct overridden" description: name: record_linux - sha256: "74d41a9ebb1eb498a38e9a813dd524e8f0b4fdd627270bda9756f437b110a3e3" + sha256: c31a35cc158cd666fc6395f7f56fc054f31685571684be6b97670a27649ce5c7 url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "1.3.0" record_platform_interface: dependency: transitive description: @@ -1592,26 +1592,26 @@ packages: dependency: transitive description: name: test - sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" url: "https://pub.dev" source: hosted - version: "1.26.2" + version: "1.26.3" test_api: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" test_core: dependency: transitive description: name: test_core - sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" url: "https://pub.dev" source: hosted - version: "0.6.11" + version: "0.6.12" tflite_flutter: dependency: "direct main" description: diff --git a/walkguide-mobile/walkguide_app/pubspec.yaml b/walkguide-mobile/walkguide_app/pubspec.yaml index deeaed7..ed373cc 100644 --- a/walkguide-mobile/walkguide_app/pubspec.yaml +++ b/walkguide-mobile/walkguide_app/pubspec.yaml @@ -91,6 +91,9 @@ dev_dependencies: mockito: ^5.4.4 bloc_test: ^9.1.7 +dependency_overrides: + record_linux: ^1.3.0 + flutter: uses-material-design: true assets: diff --git a/walkguide-mobile/walkguide_app/test/integration_test/app_flow_test.dart b/walkguide-mobile/walkguide_app/test/integration_test/app_flow_test.dart index 58c9943..b75d9ee 100644 --- a/walkguide-mobile/walkguide_app/test/integration_test/app_flow_test.dart +++ b/walkguide-mobile/walkguide_app/test/integration_test/app_flow_test.dart @@ -127,11 +127,10 @@ class _AppState extends ChangeNotifier { notifyListeners(); } - Future sendSos() async { - await Future.delayed(const Duration(milliseconds: 200)); - _sosSent = true; - notifyListeners(); - } + void sendSos() { + _sosSent = true; + notifyListeners(); + } void markAllRead() { _notifications = _notifications diff --git a/walkguide-mobile/walkguide_app/test/integration_test/flow_2_walkguide_start_stop_sos_test.dart b/walkguide-mobile/walkguide_app/test/integration_test/flow_2_walkguide_start_stop_sos_test.dart index 168654c..b52eb28 100644 --- a/walkguide-mobile/walkguide_app/test/integration_test/flow_2_walkguide_start_stop_sos_test.dart +++ b/walkguide-mobile/walkguide_app/test/integration_test/flow_2_walkguide_start_stop_sos_test.dart @@ -60,14 +60,11 @@ class _AppState extends ChangeNotifier { notifyListeners(); } - Future startWalkGuide() async { - _walkGuideStatus = _WalkGuideStatus.active; - notifyListeners(); - // Simulasi obstacle terdeteksi setelah 300ms - await Future.delayed(const Duration(milliseconds: 300)); - _detectedObstacles = ['person (87%)', 'motorcycle (72%)']; - notifyListeners(); - } + Future startWalkGuide() async { + _walkGuideStatus = _WalkGuideStatus.active; + _detectedObstacles = ['person (87%)', 'motorcycle (72%)']; + notifyListeners(); + } void stopWalkGuide() { _walkGuideStatus = _WalkGuideStatus.idle; @@ -80,11 +77,10 @@ class _AppState extends ChangeNotifier { notifyListeners(); } - Future sendSos() async { - await Future.delayed(const Duration(milliseconds: 150)); - _sosStatus = _SosStatus.triggered; - notifyListeners(); - } + Future sendSos() async { + _sosStatus = _SosStatus.triggered; + notifyListeners(); + } void goBack() { if (_currentScreen == 'walkguide' || _currentScreen == 'sos') { diff --git a/walkguide-mobile/walkguide_app/test/integration_test/flow_3_notification_read_all_test.dart b/walkguide-mobile/walkguide_app/test/integration_test/flow_3_notification_read_all_test.dart index f17730e..5b99d19 100644 --- a/walkguide-mobile/walkguide_app/test/integration_test/flow_3_notification_read_all_test.dart +++ b/walkguide-mobile/walkguide_app/test/integration_test/flow_3_notification_read_all_test.dart @@ -101,11 +101,10 @@ class _AppState extends ChangeNotifier { } } - Future markAllAsRead() async { - await Future.delayed(const Duration(milliseconds: 150)); - for (final n in _notifications) { - n.isRead = true; - } + Future markAllAsRead() async { + for (final n in _notifications) { + n.isRead = true; + } notifyListeners(); } } @@ -221,39 +220,42 @@ class _DashboardScreen extends StatelessWidget { Widget build(BuildContext context) { final unread = state.unreadCount; return Scaffold( - appBar: AppBar( - title: const Text('Dashboard'), - actions: [ - Stack( - alignment: Alignment.topRight, - children: [ - IconButton( - key: const Key('notifIconButton'), - icon: const Icon(Icons.notifications), - tooltip: 'Notifikasi', - onPressed: state.openNotifications, - ), - if (unread > 0) - Positioned( - right: 8, - top: 8, - child: Container( - key: const Key('dashboardBadge'), - padding: const EdgeInsets.all(4), - decoration: const BoxDecoration( - color: Colors.red, - shape: BoxShape.circle, - ), - child: Text( - '$unread', - style: const TextStyle(color: Colors.white, fontSize: 10), - ), - ), - ), - ], - ), - ], - ), + appBar: AppBar( + title: const Text('Dashboard'), + actions: [ + IconButton( + key: const Key('notifIconButton'), + icon: Stack( + clipBehavior: Clip.none, + children: [ + const Icon(Icons.notifications), + if (unread > 0) + Positioned( + right: -4, + top: -4, + child: IgnorePointer( + child: Container( + key: const Key('dashboardBadge'), + padding: const EdgeInsets.all(4), + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + child: Text( + '$unread', + style: const TextStyle( + color: Colors.white, fontSize: 10), + ), + ), + ), + ), + ], + ), + tooltip: 'Notifikasi', + onPressed: state.openNotifications, + ), + ], + ), body: Center( child: Column(mainAxisSize: MainAxisSize.min, children: [ const Text('Selamat datang di Dashboard'), diff --git a/walkguide-mobile/walkguide_app/test/unit/login_use_case_test.dart b/walkguide-mobile/walkguide_app/test/unit/login_use_case_test.dart index 515a741..849761c 100644 --- a/walkguide-mobile/walkguide_app/test/unit/login_use_case_test.dart +++ b/walkguide-mobile/walkguide_app/test/unit/login_use_case_test.dart @@ -47,7 +47,19 @@ abstract class AuthRepository { // File mock di-generate via: flutter pub run build_runner build // Untuk demo tanpa build_runner, kita buat manual mock di bawah -class MockAuthRepository extends Mock implements AuthRepository {} +class MockAuthRepository extends Mock implements AuthRepository { + @override + Future> login(String? email, String? password) => + super.noSuchMethod( + Invocation.method(#login, [email, password]), + returnValue: Future>.value( + const Left(AuthFailure('Repository belum di-stub')), + ), + returnValueForMissingStub: Future>.value( + const Left(AuthFailure('Repository belum di-stub')), + ), + ) as Future>; +} // ---------- Use case ---------- diff --git a/walkguide-mobile/walkguide_app/test/unit/obstacle_analyzer_test.dart b/walkguide-mobile/walkguide_app/test/unit/obstacle_analyzer_test.dart index fd980ea..a142e9d 100644 --- a/walkguide-mobile/walkguide_app/test/unit/obstacle_analyzer_test.dart +++ b/walkguide-mobile/walkguide_app/test/unit/obstacle_analyzer_test.dart @@ -98,19 +98,20 @@ class ObstacleAnalyzer { }; /// Pilih obstacle paling prioritas (Very Close > Close > Medium > Far). - DetectionResult? prioritize(List detections) { - if (detections.isEmpty) return null; - const order = [ - 'Very Close (< 1m)', - 'Close (1-2m)', - 'Medium (2-4m)', - 'Far (> 4m)', - ]; - detections.sort((a, b) => order - .indexOf(a.estimatedDistance) - .compareTo(order.indexOf(b.estimatedDistance))); - return detections.first; - } + DetectionResult? prioritize(List detections) { + if (detections.isEmpty) return null; + const order = [ + 'Very Close (< 1m)', + 'Close (1-2m)', + 'Medium (2-4m)', + 'Far (> 4m)', + ]; + final sorted = List.of(detections); + sorted.sort((a, b) => order + .indexOf(a.estimatedDistance) + .compareTo(order.indexOf(b.estimatedDistance))); + return sorted.first; + } /// Filter deteksi berdasarkan confidence threshold. List filterByConfidence( diff --git a/walkguide-mobile/walkguide_app/test/unit/register_use_case_test.dart b/walkguide-mobile/walkguide_app/test/unit/register_use_case_test.dart index 2c17f25..94988d9 100644 --- a/walkguide-mobile/walkguide_app/test/unit/register_use_case_test.dart +++ b/walkguide-mobile/walkguide_app/test/unit/register_use_case_test.dart @@ -48,7 +48,33 @@ abstract class RegisterRepository { }); } -class MockRegisterRepository extends Mock implements RegisterRepository {} +class MockRegisterRepository extends Mock implements RegisterRepository { + @override + Future> register({ + String? email, + String? password, + String? displayName, + String? role, + }) => + super.noSuchMethod( + Invocation.method( + #register, + const [], + { + #email: email, + #password: password, + #displayName: displayName, + #role: role, + }, + ), + returnValue: Future>.value( + const Left(ServerFailure('Repository belum di-stub')), + ), + returnValueForMissingStub: Future>.value( + const Left(ServerFailure('Repository belum di-stub')), + ), + ) as Future>; +} // ---------- Use case ---------- diff --git a/walkguide-mobile/walkguide_app/test/widget/login_screen_test.dart b/walkguide-mobile/walkguide_app/test/widget/login_screen_test.dart index e3e7770..ea589c9 100644 --- a/walkguide-mobile/walkguide_app/test/widget/login_screen_test.dart +++ b/walkguide-mobile/walkguide_app/test/widget/login_screen_test.dart @@ -261,13 +261,13 @@ void main() { testWidgets('password tersembunyi (obscureText=true) secara default', (tester) async { await tester.pumpWidget(makeTestable(const _StubLoginScreen())); - final editableText = tester.widget( - find.descendant( - of: find.byKey(const Key('password_field')), - matching: find.byType(EditableText), - ), - ); - expect(editableText.obscureText, isTrue); + final editableText = tester.widget( + find.descendant( + of: find.byKey(const Key('password_field')), + matching: find.byType(EditableText), + ), + ); + expect(editableText.obscureText, isTrue); }); testWidgets('tap toggle mengubah obscureText menjadi false', (tester) async { @@ -282,7 +282,7 @@ void main() { matching: find.byType(EditableText), ), ); - expect(editableText.obscureText, isTrue); + expect(editableText.obscureText, isFalse); }); testWidgets('tap toggle dua kali mengembalikan obscureText ke true', (tester) async { @@ -313,9 +313,10 @@ void main() { await tester.enterText(find.byKey(const Key('password_field')), 'password123'); await tester.tap(find.byKey(const Key('login_button'))); await tester.pump(); // Trigger rebuild - - expect(find.byKey(const Key('loading_indicator')), findsOneWidget); - }); + + expect(find.byKey(const Key('loading_indicator')), findsOneWidget); + await tester.pumpAndSettle(); + }); testWidgets('menyembunyikan tombol login saat loading', (tester) async { await tester.pumpWidget(makeTestable(const _StubLoginScreen())); @@ -324,9 +325,10 @@ void main() { await tester.enterText(find.byKey(const Key('password_field')), 'password123'); await tester.tap(find.byKey(const Key('login_button'))); await tester.pump(); - - expect(find.byKey(const Key('login_button')), findsNothing); - }); + + expect(find.byKey(const Key('login_button')), findsNothing); + await tester.pumpAndSettle(); + }); testWidgets('loading selesai setelah async operation', (tester) async { await tester.pumpWidget(makeTestable(const _StubLoginScreen())); diff --git a/walkguide-mobile/walkguide_app/test/widget/manual_screen_test.dart b/walkguide-mobile/walkguide_app/test/widget/manual_screen_test.dart index ceac807..51dd5d0 100644 --- a/walkguide-mobile/walkguide_app/test/widget/manual_screen_test.dart +++ b/walkguide-mobile/walkguide_app/test/widget/manual_screen_test.dart @@ -363,11 +363,16 @@ class _StubManualScreenState extends State<_StubManualScreen> { } } -Widget makeTestable(Widget child) => MaterialApp(home: child); - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- +Widget makeTestable(Widget child) => MaterialApp(home: child); + +Finder _commandScrollable() => find.descendant( + of: find.byKey(const Key('command_list')), + matching: find.byType(Scrollable), + ); + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- void main() { group('ManualScreen Widget Tests', () { @@ -418,12 +423,13 @@ void main() { group('Konten perintah suara', () { testWidgets('menampilkan tile untuk perintah Open Walkguide', (tester) async { - await tester.pumpWidget(makeTestable(const _StubManualScreen())); - await tester.scrollUntilVisible( - find.byKey(const Key('cmd_tile_openWalkguide')), - 200, - ); - expect(find.byKey(const Key('cmd_tile_openWalkguide')), findsOneWidget); + await tester.pumpWidget(makeTestable(const _StubManualScreen())); + await tester.scrollUntilVisible( + find.byKey(const Key('cmd_tile_openWalkguide')), + 200, + scrollable: _commandScrollable(), + ); + expect(find.byKey(const Key('cmd_tile_openWalkguide')), findsOneWidget); }); testWidgets('menampilkan phrase perintah dalam tanda kutip', @@ -449,25 +455,36 @@ void main() { expect(find.text('"Call Guardian"'), findsOneWidget); }); - testWidgets('menampilkan perintah Send SOS', (tester) async { - await tester.pumpWidget(makeTestable(const _StubManualScreen())); - await tester.scrollUntilVisible(find.text('"Send SOS"'), 200); - expect(find.text('"Send SOS"'), findsOneWidget); - }); - - testWidgets('menampilkan perintah Where Am I', (tester) async { - await tester.pumpWidget(makeTestable(const _StubManualScreen())); - await tester.scrollUntilVisible(find.text('"Where Am I"'), 200); - expect(find.text('"Where Am I"'), findsOneWidget); - }); + testWidgets('menampilkan perintah Send SOS', (tester) async { + await tester.pumpWidget(makeTestable(const _StubManualScreen())); + await tester.scrollUntilVisible( + find.text('"Send SOS"'), + 200, + scrollable: _commandScrollable(), + ); + expect(find.text('"Send SOS"'), findsOneWidget); + }); + + testWidgets('menampilkan perintah Where Am I', (tester) async { + await tester.pumpWidget(makeTestable(const _StubManualScreen())); + await tester.scrollUntilVisible( + find.text('"Where Am I"'), + 200, + scrollable: _commandScrollable(), + ); + expect(find.text('"Where Am I"'), findsOneWidget); + }); testWidgets('menampilkan kategori Darurat untuk Send SOS', (tester) async { - await tester.pumpWidget(makeTestable(const _StubManualScreen())); - await tester.scrollUntilVisible( - find.byKey(const Key('cmd_category_sendSos')), 200); - expect(find.byKey(const Key('cmd_category_sendSos')), findsOneWidget); - }); + await tester.pumpWidget(makeTestable(const _StubManualScreen())); + await tester.scrollUntilVisible( + find.byKey(const Key('cmd_category_sendSos')), + 200, + scrollable: _commandScrollable(), + ); + expect(find.byKey(const Key('cmd_category_sendSos')), findsOneWidget); + }); }); // ── Dialog info ─────────────────────────────────────────────────────── diff --git a/walkguide-mobile/walkguide_app/test/widget/notification_screen_test.dart b/walkguide-mobile/walkguide_app/test/widget/notification_screen_test.dart index 0d20977..80dd7c6 100644 --- a/walkguide-mobile/walkguide_app/test/widget/notification_screen_test.dart +++ b/walkguide-mobile/walkguide_app/test/widget/notification_screen_test.dart @@ -103,15 +103,16 @@ class _StubNotificationScreenState extends State<_StubNotificationScreen> { ], ], ), - actions: [ - if (_items.any((e) => !e.isRead)) - TextButton( - key: const Key('mark_all_read_button'), - onPressed: _markingAll ? null : _markAllRead, - child: const Text('Tandai Semua Dibaca'), - ), - ], - ), + actions: [ + if (_items.any((e) => !e.isRead)) + IconButton( + key: const Key('mark_all_read_button'), + onPressed: _markingAll ? null : _markAllRead, + tooltip: 'Tandai Semua Dibaca', + icon: const Icon(Icons.done_all), + ), + ], + ), body: widget.isLoading ? const Center( child: CircularProgressIndicator(key: Key('loading_indicator'))) diff --git a/walkguide-mobile/walkguide_app/test/widget/sos_screen_test.dart b/walkguide-mobile/walkguide_app/test/widget/sos_screen_test.dart index 461fc17..656a7a4 100644 --- a/walkguide-mobile/walkguide_app/test/widget/sos_screen_test.dart +++ b/walkguide-mobile/walkguide_app/test/widget/sos_screen_test.dart @@ -431,9 +431,10 @@ void main() { await tester.tap(find.byKey(const Key('sos_button'))); await tester.pump(); - - expect(find.byKey(const Key('sending_indicator')), findsOneWidget); - }); + + expect(find.byKey(const Key('sending_indicator')), findsOneWidget); + await tester.pumpAndSettle(); + }); testWidgets('setelah SOS terkirim, tampil success banner', (tester) async {