feat: finalize AI analyzer, sync endpoints, and stabilize builds
This commit is contained in:
@@ -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
|
||||
|
||||
+9
-13
@@ -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') {
|
||||
|
||||
+40
-38
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user