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."));
|
||||
}
|
||||
|
||||
@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")
|
||||
public ResponseEntity<ApiResponse<Page<ActivityLogResponse>>> getActivityLogs(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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() {
|
||||
}
|
||||
|
||||
@ -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 =====
|
||||
|
||||
@Test
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
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 }
|
||||
|
||||
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<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(
|
||||
label: label,
|
||||
confidence: confidence,
|
||||
direction: ObstacleDirection.center,
|
||||
estimatedDistance: 'Close',
|
||||
estimatedDistance: 'Close (1-2m)',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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: '');
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ class CallService {
|
||||
}
|
||||
|
||||
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'];
|
||||
if (data is! Map<String, dynamic>) 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();
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -127,11 +127,10 @@ class _AppState extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> sendSos() async {
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
_sosSent = true;
|
||||
notifyListeners();
|
||||
}
|
||||
void sendSos() {
|
||||
_sosSent = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void markAllRead() {
|
||||
_notifications = _notifications
|
||||
|
||||
@ -60,14 +60,11 @@ class _AppState extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> 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<void> 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<void> sendSos() async {
|
||||
await Future.delayed(const Duration(milliseconds: 150));
|
||||
_sosStatus = _SosStatus.triggered;
|
||||
notifyListeners();
|
||||
}
|
||||
Future<void> sendSos() async {
|
||||
_sosStatus = _SosStatus.triggered;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void goBack() {
|
||||
if (_currentScreen == 'walkguide' || _currentScreen == 'sos') {
|
||||
|
||||
@ -101,11 +101,10 @@ class _AppState extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> markAllAsRead() async {
|
||||
await Future.delayed(const Duration(milliseconds: 150));
|
||||
for (final n in _notifications) {
|
||||
n.isRead = true;
|
||||
}
|
||||
Future<void> 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'),
|
||||
|
||||
@ -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<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 ----------
|
||||
|
||||
|
||||
@ -98,19 +98,20 @@ class ObstacleAnalyzer {
|
||||
};
|
||||
|
||||
/// Pilih obstacle paling prioritas (Very Close > Close > Medium > Far).
|
||||
DetectionResult? prioritize(List<DetectionResult> 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<DetectionResult> detections) {
|
||||
if (detections.isEmpty) return null;
|
||||
const order = [
|
||||
'Very Close (< 1m)',
|
||||
'Close (1-2m)',
|
||||
'Medium (2-4m)',
|
||||
'Far (> 4m)',
|
||||
];
|
||||
final sorted = List<DetectionResult>.of(detections);
|
||||
sorted.sort((a, b) => order
|
||||
.indexOf(a.estimatedDistance)
|
||||
.compareTo(order.indexOf(b.estimatedDistance)));
|
||||
return sorted.first;
|
||||
}
|
||||
|
||||
/// Filter deteksi berdasarkan confidence threshold.
|
||||
List<DetectionResult> filterByConfidence(
|
||||
|
||||
@ -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 ----------
|
||||
|
||||
|
||||
@ -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<EditableText>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('password_field')),
|
||||
matching: find.byType(EditableText),
|
||||
),
|
||||
);
|
||||
expect(editableText.obscureText, isTrue);
|
||||
final editableText = tester.widget<EditableText>(
|
||||
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()));
|
||||
|
||||
@ -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 ───────────────────────────────────────────────────────
|
||||
|
||||
@ -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')))
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user