feat: finalize AI analyzer, sync endpoints, and stabilize builds
This commit is contained in:
parent
f697ef16cd
commit
23d0cf6f66
@ -114,6 +114,16 @@ public class UserController {
|
|||||||
"SOS dikirim! Guardian sudah diberitahu."));
|
"SOS dikirim! Guardian sudah diberitahu."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sos-events")
|
||||||
|
public ResponseEntity<ApiResponse<Page<SosEventResponse>>> 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")
|
@GetMapping("/activity-logs")
|
||||||
public ResponseEntity<ApiResponse<Page<ActivityLogResponse>>> getActivityLogs(
|
public ResponseEntity<ApiResponse<Page<ActivityLogResponse>>> getActivityLogs(
|
||||||
@RequestParam(defaultValue = "0") int page,
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
|||||||
@ -183,6 +183,10 @@ paths:
|
|||||||
post:
|
post:
|
||||||
responses:
|
responses:
|
||||||
"200": { description: SOS triggered }
|
"200": { description: SOS triggered }
|
||||||
|
/user/sos-events:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200": { description: User SOS history }
|
||||||
/user/activity-logs:
|
/user/activity-logs:
|
||||||
get:
|
get:
|
||||||
responses:
|
responses:
|
||||||
|
|||||||
@ -1,11 +1,23 @@
|
|||||||
package com.walkguide;
|
package com.walkguide;
|
||||||
|
|
||||||
|
import com.walkguide.config.DataSeeder;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
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 {
|
class DemoApplicationTests {
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private DataSeeder dataSeeder;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void contextLoads() {
|
void contextLoads() {
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
|
||||||
|
sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);
|
||||||
|
|
||||||
|
Page<SosEventResponse> 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 =====
|
// ===== ACTIVITY LOGS =====
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@ -13,6 +13,7 @@ android {
|
|||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
@ -24,7 +25,7 @@ android {
|
|||||||
applicationId = "com.example.walkguide_app"
|
applicationId = "com.example.walkguide_app"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
minSdk = flutter.minSdkVersion
|
minSdk = 26
|
||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
versionCode = flutter.versionCode
|
versionCode = flutter.versionCode
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
@ -42,3 +43,7 @@ android {
|
|||||||
flutter {
|
flutter {
|
||||||
source = "../.."
|
source = "../.."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
||||||
|
}
|
||||||
|
|||||||
@ -15,6 +15,42 @@ subprojects {
|
|||||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
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 {
|
subprojects {
|
||||||
project.evaluationDependsOn(":app")
|
project.evaluationDependsOn(":app")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
kotlin.incremental=false
|
||||||
|
|||||||
1
walkguide-mobile/walkguide_app/dart_test.yaml
Normal file
1
walkguide-mobile/walkguide_app/dart_test.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
concurrency: 1
|
||||||
@ -1,5 +1,23 @@
|
|||||||
enum ObstacleDirection { left, center, right }
|
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 {
|
class DetectionResult {
|
||||||
final String label;
|
final String label;
|
||||||
final double confidence;
|
final double confidence;
|
||||||
@ -26,12 +44,73 @@ class DetectionResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ObstacleAnalyzer {
|
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<DetectionResult> 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<DetectionResult>.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<DetectionResult> filterByConfidence(
|
||||||
|
List<DetectionResult> detections,
|
||||||
|
double threshold,
|
||||||
|
) {
|
||||||
|
return detections.where((d) => d.confidence >= threshold).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
DetectionResult analyzeFallback({
|
||||||
|
String label = 'person',
|
||||||
|
double confidence = 0.86,
|
||||||
|
}) {
|
||||||
return DetectionResult(
|
return DetectionResult(
|
||||||
label: label,
|
label: label,
|
||||||
confidence: confidence,
|
confidence: confidence,
|
||||||
direction: ObstacleDirection.center,
|
direction: ObstacleDirection.center,
|
||||||
estimatedDistance: 'Close',
|
estimatedDistance: 'Close (1-2m)',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,6 +61,7 @@ class AppConstants {
|
|||||||
await prefs.setString(_selectedYoloModelKey, path);
|
await prefs.setString(_selectedYoloModelKey, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agora - ganti dengan App ID dari agora.io
|
// Agora App ID diisi saat build: --dart-define=AGORA_APP_ID=...
|
||||||
static const String agoraAppId = 'YOUR_AGORA_APP_ID';
|
static const String agoraAppId =
|
||||||
|
String.fromEnvironment('AGORA_APP_ID', defaultValue: '');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,7 @@ class CallService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<int?> getPairedReceiverId() async {
|
Future<int?> getPairedReceiverId() async {
|
||||||
final res = await _apiClient.dio.get('/pairing/status');
|
final res = await _apiClient.dio.get('/shared/pairing/status');
|
||||||
final data = res.data['data'];
|
final data = res.data['data'];
|
||||||
if (data is! Map<String, dynamic>) return null;
|
if (data is! Map<String, dynamic>) return null;
|
||||||
final id = data['pairedWithId'];
|
final id = data['pairedWithId'];
|
||||||
@ -72,6 +72,10 @@ class CallService {
|
|||||||
int uid = 0,
|
int uid = 0,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
|
if (AppConstants.agoraAppId.isEmpty) {
|
||||||
|
debugPrint('Agora join skipped: AGORA_APP_ID is not configured');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
_engine ??= createAgoraRtcEngine();
|
_engine ??= createAgoraRtcEngine();
|
||||||
await _engine!.initialize(const RtcEngineContext(appId: AppConstants.agoraAppId));
|
await _engine!.initialize(const RtcEngineContext(appId: AppConstants.agoraAppId));
|
||||||
await _engine!.enableAudio();
|
await _engine!.enableAudio();
|
||||||
|
|||||||
@ -947,10 +947,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.17.0"
|
||||||
mgrs_dart:
|
mgrs_dart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1264,13 +1264,13 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.2"
|
version: "1.2.2"
|
||||||
record_linux:
|
record_linux:
|
||||||
dependency: transitive
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
name: record_linux
|
name: record_linux
|
||||||
sha256: "74d41a9ebb1eb498a38e9a813dd524e8f0b4fdd627270bda9756f437b110a3e3"
|
sha256: c31a35cc158cd666fc6395f7f56fc054f31685571684be6b97670a27649ce5c7
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.2"
|
version: "1.3.0"
|
||||||
record_platform_interface:
|
record_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1592,26 +1592,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test
|
name: test
|
||||||
sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb"
|
sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.26.2"
|
version: "1.26.3"
|
||||||
test_api:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.6"
|
version: "0.7.7"
|
||||||
test_core:
|
test_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_core
|
name: test_core
|
||||||
sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a"
|
sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.11"
|
version: "0.6.12"
|
||||||
tflite_flutter:
|
tflite_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -91,6 +91,9 @@ dev_dependencies:
|
|||||||
mockito: ^5.4.4
|
mockito: ^5.4.4
|
||||||
bloc_test: ^9.1.7
|
bloc_test: ^9.1.7
|
||||||
|
|
||||||
|
dependency_overrides:
|
||||||
|
record_linux: ^1.3.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
assets:
|
assets:
|
||||||
|
|||||||
@ -127,8 +127,7 @@ class _AppState extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> sendSos() async {
|
void sendSos() {
|
||||||
await Future.delayed(const Duration(milliseconds: 200));
|
|
||||||
_sosSent = true;
|
_sosSent = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,9 +62,6 @@ class _AppState extends ChangeNotifier {
|
|||||||
|
|
||||||
Future<void> startWalkGuide() async {
|
Future<void> startWalkGuide() async {
|
||||||
_walkGuideStatus = _WalkGuideStatus.active;
|
_walkGuideStatus = _WalkGuideStatus.active;
|
||||||
notifyListeners();
|
|
||||||
// Simulasi obstacle terdeteksi setelah 300ms
|
|
||||||
await Future.delayed(const Duration(milliseconds: 300));
|
|
||||||
_detectedObstacles = ['person (87%)', 'motorcycle (72%)'];
|
_detectedObstacles = ['person (87%)', 'motorcycle (72%)'];
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@ -81,7 +78,6 @@ class _AppState extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> sendSos() async {
|
Future<void> sendSos() async {
|
||||||
await Future.delayed(const Duration(milliseconds: 150));
|
|
||||||
_sosStatus = _SosStatus.triggered;
|
_sosStatus = _SosStatus.triggered;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -102,7 +102,6 @@ class _AppState extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> markAllAsRead() async {
|
Future<void> markAllAsRead() async {
|
||||||
await Future.delayed(const Duration(milliseconds: 150));
|
|
||||||
for (final n in _notifications) {
|
for (final n in _notifications) {
|
||||||
n.isRead = true;
|
n.isRead = true;
|
||||||
}
|
}
|
||||||
@ -224,33 +223,36 @@ class _DashboardScreen extends StatelessWidget {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Dashboard'),
|
title: const Text('Dashboard'),
|
||||||
actions: [
|
actions: [
|
||||||
Stack(
|
IconButton(
|
||||||
alignment: Alignment.topRight,
|
key: const Key('notifIconButton'),
|
||||||
children: [
|
icon: Stack(
|
||||||
IconButton(
|
clipBehavior: Clip.none,
|
||||||
key: const Key('notifIconButton'),
|
children: [
|
||||||
icon: const Icon(Icons.notifications),
|
const Icon(Icons.notifications),
|
||||||
tooltip: 'Notifikasi',
|
if (unread > 0)
|
||||||
onPressed: state.openNotifications,
|
Positioned(
|
||||||
),
|
right: -4,
|
||||||
if (unread > 0)
|
top: -4,
|
||||||
Positioned(
|
child: IgnorePointer(
|
||||||
right: 8,
|
child: Container(
|
||||||
top: 8,
|
key: const Key('dashboardBadge'),
|
||||||
child: Container(
|
padding: const EdgeInsets.all(4),
|
||||||
key: const Key('dashboardBadge'),
|
decoration: const BoxDecoration(
|
||||||
padding: const EdgeInsets.all(4),
|
color: Colors.red,
|
||||||
decoration: const BoxDecoration(
|
shape: BoxShape.circle,
|
||||||
color: Colors.red,
|
),
|
||||||
shape: BoxShape.circle,
|
child: Text(
|
||||||
),
|
'$unread',
|
||||||
child: Text(
|
style: const TextStyle(
|
||||||
'$unread',
|
color: Colors.white, fontSize: 10),
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 10),
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
|
tooltip: 'Notifikasi',
|
||||||
|
onPressed: state.openNotifications,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -47,7 +47,19 @@ abstract class AuthRepository {
|
|||||||
// File mock di-generate via: flutter pub run build_runner build
|
// File mock di-generate via: flutter pub run build_runner build
|
||||||
// Untuk demo tanpa build_runner, kita buat manual mock di bawah
|
// 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<Either<Failure, UserEntity>> login(String? email, String? password) =>
|
||||||
|
super.noSuchMethod(
|
||||||
|
Invocation.method(#login, [email, password]),
|
||||||
|
returnValue: Future<Either<Failure, UserEntity>>.value(
|
||||||
|
const Left(AuthFailure('Repository belum di-stub')),
|
||||||
|
),
|
||||||
|
returnValueForMissingStub: Future<Either<Failure, UserEntity>>.value(
|
||||||
|
const Left(AuthFailure('Repository belum di-stub')),
|
||||||
|
),
|
||||||
|
) as Future<Either<Failure, UserEntity>>;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- Use case ----------
|
// ---------- Use case ----------
|
||||||
|
|
||||||
|
|||||||
@ -106,10 +106,11 @@ class ObstacleAnalyzer {
|
|||||||
'Medium (2-4m)',
|
'Medium (2-4m)',
|
||||||
'Far (> 4m)',
|
'Far (> 4m)',
|
||||||
];
|
];
|
||||||
detections.sort((a, b) => order
|
final sorted = List<DetectionResult>.of(detections);
|
||||||
|
sorted.sort((a, b) => order
|
||||||
.indexOf(a.estimatedDistance)
|
.indexOf(a.estimatedDistance)
|
||||||
.compareTo(order.indexOf(b.estimatedDistance)));
|
.compareTo(order.indexOf(b.estimatedDistance)));
|
||||||
return detections.first;
|
return sorted.first;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Filter deteksi berdasarkan confidence threshold.
|
/// Filter deteksi berdasarkan confidence threshold.
|
||||||
|
|||||||
@ -48,7 +48,33 @@ abstract class RegisterRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class MockRegisterRepository extends Mock implements RegisterRepository {}
|
class MockRegisterRepository extends Mock implements RegisterRepository {
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, UserEntity>> 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<Either<Failure, UserEntity>>.value(
|
||||||
|
const Left(ServerFailure('Repository belum di-stub')),
|
||||||
|
),
|
||||||
|
returnValueForMissingStub: Future<Either<Failure, UserEntity>>.value(
|
||||||
|
const Left(ServerFailure('Repository belum di-stub')),
|
||||||
|
),
|
||||||
|
) as Future<Either<Failure, UserEntity>>;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- Use case ----------
|
// ---------- Use case ----------
|
||||||
|
|
||||||
|
|||||||
@ -282,7 +282,7 @@ void main() {
|
|||||||
matching: find.byType(EditableText),
|
matching: find.byType(EditableText),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(editableText.obscureText, isTrue);
|
expect(editableText.obscureText, isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('tap toggle dua kali mengembalikan obscureText ke true', (tester) async {
|
testWidgets('tap toggle dua kali mengembalikan obscureText ke true', (tester) async {
|
||||||
@ -315,6 +315,7 @@ void main() {
|
|||||||
await tester.pump(); // Trigger rebuild
|
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 {
|
testWidgets('menyembunyikan tombol login saat loading', (tester) async {
|
||||||
@ -326,6 +327,7 @@ void main() {
|
|||||||
await tester.pump();
|
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 {
|
testWidgets('loading selesai setelah async operation', (tester) async {
|
||||||
|
|||||||
@ -365,6 +365,11 @@ class _StubManualScreenState extends State<_StubManualScreen> {
|
|||||||
|
|
||||||
Widget makeTestable(Widget child) => MaterialApp(home: child);
|
Widget makeTestable(Widget child) => MaterialApp(home: child);
|
||||||
|
|
||||||
|
Finder _commandScrollable() => find.descendant(
|
||||||
|
of: find.byKey(const Key('command_list')),
|
||||||
|
matching: find.byType(Scrollable),
|
||||||
|
);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Tests
|
// Tests
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -422,6 +427,7 @@ void main() {
|
|||||||
await tester.scrollUntilVisible(
|
await tester.scrollUntilVisible(
|
||||||
find.byKey(const Key('cmd_tile_openWalkguide')),
|
find.byKey(const Key('cmd_tile_openWalkguide')),
|
||||||
200,
|
200,
|
||||||
|
scrollable: _commandScrollable(),
|
||||||
);
|
);
|
||||||
expect(find.byKey(const Key('cmd_tile_openWalkguide')), findsOneWidget);
|
expect(find.byKey(const Key('cmd_tile_openWalkguide')), findsOneWidget);
|
||||||
});
|
});
|
||||||
@ -451,13 +457,21 @@ void main() {
|
|||||||
|
|
||||||
testWidgets('menampilkan perintah Send SOS', (tester) async {
|
testWidgets('menampilkan perintah Send SOS', (tester) async {
|
||||||
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
|
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
|
||||||
await tester.scrollUntilVisible(find.text('"Send SOS"'), 200);
|
await tester.scrollUntilVisible(
|
||||||
|
find.text('"Send SOS"'),
|
||||||
|
200,
|
||||||
|
scrollable: _commandScrollable(),
|
||||||
|
);
|
||||||
expect(find.text('"Send SOS"'), findsOneWidget);
|
expect(find.text('"Send SOS"'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('menampilkan perintah Where Am I', (tester) async {
|
testWidgets('menampilkan perintah Where Am I', (tester) async {
|
||||||
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
|
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
|
||||||
await tester.scrollUntilVisible(find.text('"Where Am I"'), 200);
|
await tester.scrollUntilVisible(
|
||||||
|
find.text('"Where Am I"'),
|
||||||
|
200,
|
||||||
|
scrollable: _commandScrollable(),
|
||||||
|
);
|
||||||
expect(find.text('"Where Am I"'), findsOneWidget);
|
expect(find.text('"Where Am I"'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -465,7 +479,10 @@ void main() {
|
|||||||
(tester) async {
|
(tester) async {
|
||||||
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
|
await tester.pumpWidget(makeTestable(const _StubManualScreen()));
|
||||||
await tester.scrollUntilVisible(
|
await tester.scrollUntilVisible(
|
||||||
find.byKey(const Key('cmd_category_sendSos')), 200);
|
find.byKey(const Key('cmd_category_sendSos')),
|
||||||
|
200,
|
||||||
|
scrollable: _commandScrollable(),
|
||||||
|
);
|
||||||
expect(find.byKey(const Key('cmd_category_sendSos')), findsOneWidget);
|
expect(find.byKey(const Key('cmd_category_sendSos')), findsOneWidget);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -105,10 +105,11 @@ class _StubNotificationScreenState extends State<_StubNotificationScreen> {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
if (_items.any((e) => !e.isRead))
|
if (_items.any((e) => !e.isRead))
|
||||||
TextButton(
|
IconButton(
|
||||||
key: const Key('mark_all_read_button'),
|
key: const Key('mark_all_read_button'),
|
||||||
onPressed: _markingAll ? null : _markAllRead,
|
onPressed: _markingAll ? null : _markAllRead,
|
||||||
child: const Text('Tandai Semua Dibaca'),
|
tooltip: 'Tandai Semua Dibaca',
|
||||||
|
icon: const Icon(Icons.done_all),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -433,6 +433,7 @@ void main() {
|
|||||||
await tester.pump();
|
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',
|
testWidgets('setelah SOS terkirim, tampil success banner',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user