test(integration): add e2e flows for flutter app and java backend features
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+383
@@ -0,0 +1,383 @@
|
||||
// integration_test/flow_1_login_dashboard_logout_test.dart
|
||||
//
|
||||
// E2E Flow 1: Login → Dashboard → Logout
|
||||
//
|
||||
// Alur yang diuji:
|
||||
// 1. User membuka app → tampil halaman Login
|
||||
// 2. User mengisi email + password yang valid → tap Login
|
||||
// 3. App berpindah ke Dashboard (HomeScreen)
|
||||
// 4. Dashboard menampilkan nama user & status pairing
|
||||
// 5. User tap Logout → kembali ke halaman Login
|
||||
//
|
||||
// Jalankan:
|
||||
// flutter test integration_test/flow_1_login_dashboard_logout_test.dart
|
||||
// atau dengan driver:
|
||||
// flutter drive \
|
||||
// --driver=test_driver/integration_test.dart \
|
||||
// --target=integration_test/flow_1_login_dashboard_logout_test.dart
|
||||
//
|
||||
// Catatan:
|
||||
// Test ini menggunakan StubApp yang self-contained (tanpa koneksi server
|
||||
// kampus) sehingga bisa dijalankan di CI maupun device fisik tanpa
|
||||
// internet. Untuk E2E full terhadap server live, ganti _StubApp dengan
|
||||
// import 'package:walkguide_app/main.dart' as app; dan gunakan Mockito
|
||||
// untuk mock ApiClient + SecureStorage.
|
||||
|
||||
// ignore_for_file: avoid_print
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
// ─── Fake Credentials ────────────────────────────────────────────────────────
|
||||
|
||||
const _kValidEmail = 'user@walkguide.test';
|
||||
const _kValidPassword = 'Password123!';
|
||||
const _kDisplayName = 'Budi Tunanetra';
|
||||
|
||||
// ─── Stub State ──────────────────────────────────────────────────────────────
|
||||
|
||||
class _AuthState extends ChangeNotifier {
|
||||
String? _accessToken;
|
||||
String? _displayName;
|
||||
|
||||
bool get isLoggedIn => _accessToken != null;
|
||||
String get displayName => _displayName ?? '';
|
||||
|
||||
/// Simulasi login API call (200 ms delay).
|
||||
Future<bool> login(String email, String password) async {
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
if (email == _kValidEmail && password == _kValidPassword) {
|
||||
_accessToken = 'fake-jwt-token';
|
||||
_displayName = _kDisplayName;
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Simulasi logout.
|
||||
void logout() {
|
||||
_accessToken = null;
|
||||
_displayName = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Stub App ─────────────────────────────────────────────────────────────────
|
||||
|
||||
class _StubApp extends StatefulWidget {
|
||||
const _StubApp();
|
||||
|
||||
@override
|
||||
State<_StubApp> createState() => _StubAppState();
|
||||
}
|
||||
|
||||
class _StubAppState extends State<_StubApp> {
|
||||
final _auth = _AuthState();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_auth.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'WalkGuide Test',
|
||||
home: ListenableBuilder(
|
||||
listenable: _auth,
|
||||
builder: (context, _) {
|
||||
if (_auth.isLoggedIn) {
|
||||
return _DashboardScreen(auth: _auth);
|
||||
}
|
||||
return _LoginScreen(auth: _auth);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Login Screen ─────────────────────────────────────────────────────────────
|
||||
|
||||
class _LoginScreen extends StatefulWidget {
|
||||
final _AuthState auth;
|
||||
const _LoginScreen({required this.auth});
|
||||
|
||||
@override
|
||||
State<_LoginScreen> createState() => _LoginScreenState();
|
||||
}
|
||||
|
||||
class _LoginScreenState extends State<_LoginScreen> {
|
||||
final _emailCtrl = TextEditingController();
|
||||
final _passwordCtrl = TextEditingController();
|
||||
bool _loading = false;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailCtrl.dispose();
|
||||
_passwordCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
});
|
||||
final ok = await widget.auth.login(
|
||||
_emailCtrl.text.trim(),
|
||||
_passwordCtrl.text.trim(),
|
||||
);
|
||||
if (!ok && mounted) {
|
||||
setState(() {
|
||||
_loading = false;
|
||||
_error = 'Email atau password salah';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('WalkGuide — Login')),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
key: const Key('emailField'),
|
||||
controller: _emailCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Email'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
key: const Key('passwordField'),
|
||||
controller: _passwordCtrl,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(labelText: 'Password'),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (_error != null)
|
||||
Text(
|
||||
_error!,
|
||||
key: const Key('loginError'),
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
if (_loading)
|
||||
const CircularProgressIndicator(key: Key('loginLoading'))
|
||||
else
|
||||
ElevatedButton(
|
||||
key: const Key('loginButton'),
|
||||
onPressed: _submit,
|
||||
child: const Text('Login'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Dashboard Screen ─────────────────────────────────────────────────────────
|
||||
|
||||
class _DashboardScreen extends StatelessWidget {
|
||||
final _AuthState auth;
|
||||
const _DashboardScreen({required this.auth});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Dashboard'),
|
||||
actions: [
|
||||
IconButton(
|
||||
key: const Key('logoutButton'),
|
||||
icon: const Icon(Icons.logout),
|
||||
tooltip: 'Logout',
|
||||
onPressed: auth.logout,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Selamat datang, ${auth.displayName}',
|
||||
key: const Key('welcomeText'),
|
||||
style: const TextStyle(fontSize: 20),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Status Pairing: Belum terhubung',
|
||||
key: Key('pairingStatus'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Flow 1 — Login → Dashboard → Logout', () {
|
||||
testWidgets(
|
||||
'1.1 Halaman Login tampil saat app pertama dibuka',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(const _StubApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('WalkGuide — Login'), findsOneWidget,
|
||||
reason: 'AppBar harus menampilkan judul Login');
|
||||
expect(find.byKey(const Key('emailField')), findsOneWidget);
|
||||
expect(find.byKey(const Key('passwordField')), findsOneWidget);
|
||||
expect(find.byKey(const Key('loginButton')), findsOneWidget);
|
||||
|
||||
print('[PASS] 1.1 Login screen tampil dengan semua field');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'1.2 Login dengan credentials yang salah → tampil pesan error',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(const _StubApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('emailField')), 'wrong@email.com');
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('passwordField')), 'wrongpass');
|
||||
await tester.tap(find.byKey(const Key('loginButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('loginError')), findsOneWidget,
|
||||
reason: 'Error message harus muncul saat credentials salah');
|
||||
expect(find.text('WalkGuide — Login'), findsOneWidget,
|
||||
reason: 'Harus tetap di halaman Login');
|
||||
|
||||
print('[PASS] 1.2 Error message tampil untuk credentials salah');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'1.3 Login berhasil dengan credentials valid → pindah ke Dashboard',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(const _StubApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('emailField')), _kValidEmail);
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('passwordField')), _kValidPassword);
|
||||
await tester.tap(find.byKey(const Key('loginButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Dashboard'), findsOneWidget,
|
||||
reason: 'Setelah login berhasil harus tampil halaman Dashboard');
|
||||
expect(
|
||||
find.byKey(const Key('welcomeText')),
|
||||
findsOneWidget,
|
||||
reason: 'Pesan selamat datang harus muncul',
|
||||
);
|
||||
expect(
|
||||
find.textContaining(_kDisplayName),
|
||||
findsOneWidget,
|
||||
reason: 'Nama user harus ditampilkan di Dashboard',
|
||||
);
|
||||
|
||||
print('[PASS] 1.3 Login berhasil → Dashboard tampil dengan nama user');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'1.4 Dashboard menampilkan status pairing',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(const _StubApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Login
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('emailField')), _kValidEmail);
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('passwordField')), _kValidPassword);
|
||||
await tester.tap(find.byKey(const Key('loginButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('pairingStatus')), findsOneWidget,
|
||||
reason: 'Status pairing harus tampil di Dashboard');
|
||||
|
||||
print('[PASS] 1.4 Dashboard menampilkan status pairing');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'1.5 Logout → kembali ke halaman Login',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(const _StubApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Login
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('emailField')), _kValidEmail);
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('passwordField')), _kValidPassword);
|
||||
await tester.tap(find.byKey(const Key('loginButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Dashboard'), findsOneWidget);
|
||||
|
||||
// Logout
|
||||
await tester.tap(find.byKey(const Key('logoutButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('WalkGuide — Login'), findsOneWidget,
|
||||
reason: 'Setelah logout harus kembali ke halaman Login');
|
||||
expect(find.byKey(const Key('emailField')), findsOneWidget,
|
||||
reason: 'Form login harus tampil kembali');
|
||||
|
||||
print('[PASS] 1.5 Logout → kembali ke Login');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'1.6 Full round-trip: Login → Dashboard → Logout → Login kembali',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(const _StubApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Login pertama
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('emailField')), _kValidEmail);
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('passwordField')), _kValidPassword);
|
||||
await tester.tap(find.byKey(const Key('loginButton')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Dashboard'), findsOneWidget);
|
||||
|
||||
// Logout
|
||||
await tester.tap(find.byKey(const Key('logoutButton')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('WalkGuide — Login'), findsOneWidget);
|
||||
|
||||
// Login lagi
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('emailField')), _kValidEmail);
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('passwordField')), _kValidPassword);
|
||||
await tester.tap(find.byKey(const Key('loginButton')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Dashboard'), findsOneWidget,
|
||||
reason: 'User harus bisa login kembali setelah logout');
|
||||
|
||||
print('[PASS] 1.6 Full round-trip Login → Logout → Login berhasil');
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
+580
@@ -0,0 +1,580 @@
|
||||
// integration_test/flow_2_walkguide_start_stop_sos_test.dart
|
||||
//
|
||||
// E2E Flow 2: Login → WalkGuide → Start → Stop → SOS
|
||||
//
|
||||
// Alur yang diuji:
|
||||
// 1. Login berhasil → Dashboard
|
||||
// 2. Tap tombol WalkGuide → layar WalkGuide terbuka
|
||||
// 3. Tap "Mulai WalkGuide" → status berubah menjadi AKTIF
|
||||
// 4. Banner obstacle terdeteksi muncul
|
||||
// 5. Tap "Berhenti WalkGuide" → status kembali TIDAK AKTIF
|
||||
// 6. Tap tombol SOS → konfirmasi dialog muncul
|
||||
// 7. Konfirmasi SOS → status SOS TERKIRIM tampil
|
||||
//
|
||||
// Jalankan:
|
||||
// flutter test integration_test/flow_2_walkguide_start_stop_sos_test.dart
|
||||
|
||||
// ignore_for_file: avoid_print
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
// ─── Fake Credentials ────────────────────────────────────────────────────────
|
||||
|
||||
const _kEmail = 'user@walkguide.test';
|
||||
const _kPassword = 'Password123!';
|
||||
|
||||
// ─── Stub State ──────────────────────────────────────────────────────────────
|
||||
|
||||
enum _WalkGuideStatus { idle, active }
|
||||
|
||||
enum _SosStatus { none, triggered }
|
||||
|
||||
class _AppState extends ChangeNotifier {
|
||||
bool _loggedIn = false;
|
||||
String _currentScreen = 'login'; // login | dashboard | walkguide | sos
|
||||
_WalkGuideStatus _walkGuideStatus = _WalkGuideStatus.idle;
|
||||
_SosStatus _sosStatus = _SosStatus.none;
|
||||
List<String> _detectedObstacles = [];
|
||||
|
||||
bool get loggedIn => _loggedIn;
|
||||
String get currentScreen => _currentScreen;
|
||||
_WalkGuideStatus get walkGuideStatus => _walkGuideStatus;
|
||||
_SosStatus get sosStatus => _sosStatus;
|
||||
List<String> get detectedObstacles => _detectedObstacles;
|
||||
|
||||
Future<bool> login(String email, String password) async {
|
||||
await Future.delayed(const Duration(milliseconds: 150));
|
||||
if (email == _kEmail && password == _kPassword) {
|
||||
_loggedIn = true;
|
||||
_currentScreen = 'dashboard';
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void openWalkGuide() {
|
||||
_currentScreen = 'walkguide';
|
||||
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();
|
||||
}
|
||||
|
||||
void stopWalkGuide() {
|
||||
_walkGuideStatus = _WalkGuideStatus.idle;
|
||||
_detectedObstacles = [];
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void openSos() {
|
||||
_currentScreen = 'sos';
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> sendSos() async {
|
||||
await Future.delayed(const Duration(milliseconds: 150));
|
||||
_sosStatus = _SosStatus.triggered;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void goBack() {
|
||||
if (_currentScreen == 'walkguide' || _currentScreen == 'sos') {
|
||||
_currentScreen = 'dashboard';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Stub App ─────────────────────────────────────────────────────────────────
|
||||
|
||||
class _StubApp extends StatefulWidget {
|
||||
const _StubApp();
|
||||
|
||||
@override
|
||||
State<_StubApp> createState() => _StubAppState();
|
||||
}
|
||||
|
||||
class _StubAppState extends State<_StubApp> {
|
||||
final _state = _AppState();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_state.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'WalkGuide Test',
|
||||
home: ListenableBuilder(
|
||||
listenable: _state,
|
||||
builder: (context, _) {
|
||||
switch (_state.currentScreen) {
|
||||
case 'walkguide':
|
||||
return _WalkGuideScreen(state: _state);
|
||||
case 'sos':
|
||||
return _SosScreen(state: _state);
|
||||
case 'dashboard':
|
||||
return _DashboardScreen(state: _state);
|
||||
default:
|
||||
return _LoginScreen(state: _state);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Login Screen ─────────────────────────────────────────────────────────────
|
||||
|
||||
class _LoginScreen extends StatefulWidget {
|
||||
final _AppState state;
|
||||
const _LoginScreen({required this.state});
|
||||
|
||||
@override
|
||||
State<_LoginScreen> createState() => _LoginScreenState();
|
||||
}
|
||||
|
||||
class _LoginScreenState extends State<_LoginScreen> {
|
||||
final _emailCtrl = TextEditingController();
|
||||
final _passwordCtrl = TextEditingController();
|
||||
bool _loading = false;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailCtrl.dispose();
|
||||
_passwordCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
});
|
||||
final ok = await widget.state
|
||||
.login(_emailCtrl.text.trim(), _passwordCtrl.text.trim());
|
||||
if (!ok && mounted) {
|
||||
setState(() {
|
||||
_loading = false;
|
||||
_error = 'Credentials tidak valid';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Login')),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(children: [
|
||||
TextField(
|
||||
key: const Key('emailField'),
|
||||
controller: _emailCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Email'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
key: const Key('passwordField'),
|
||||
controller: _passwordCtrl,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(labelText: 'Password'),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (_error != null)
|
||||
Text(_error!, style: const TextStyle(color: Colors.red)),
|
||||
if (_loading)
|
||||
const CircularProgressIndicator()
|
||||
else
|
||||
ElevatedButton(
|
||||
key: const Key('loginButton'),
|
||||
onPressed: _submit,
|
||||
child: const Text('Login'),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Dashboard Screen ─────────────────────────────────────────────────────────
|
||||
|
||||
class _DashboardScreen extends StatelessWidget {
|
||||
final _AppState state;
|
||||
const _DashboardScreen({required this.state});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Dashboard')),
|
||||
body: Center(
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
ElevatedButton(
|
||||
key: const Key('openWalkGuideButton'),
|
||||
onPressed: state.openWalkGuide,
|
||||
child: const Text('Buka WalkGuide'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ElevatedButton(
|
||||
key: const Key('openSosButton'),
|
||||
onPressed: state.openSos,
|
||||
child: const Text('SOS'),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── WalkGuide Screen ─────────────────────────────────────────────────────────
|
||||
|
||||
class _WalkGuideScreen extends StatelessWidget {
|
||||
final _AppState state;
|
||||
const _WalkGuideScreen({required this.state});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isActive = state.walkGuideStatus == _WalkGuideStatus.active;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('WalkGuide'),
|
||||
leading: IconButton(
|
||||
key: const Key('backButton'),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: state.goBack,
|
||||
),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
// Status badge
|
||||
Container(
|
||||
key: const Key('walkGuideStatusBadge'),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? Colors.green : Colors.grey,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
isActive ? 'WalkGuide AKTIF' : 'WalkGuide TIDAK AKTIF',
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Obstacle list
|
||||
if (state.detectedObstacles.isNotEmpty)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Obstacle Terdeteksi:',
|
||||
key: Key('obstacleHeader'),
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
...state.detectedObstacles.map((obs) => Text(
|
||||
obs,
|
||||
key: Key('obstacle_$obs'),
|
||||
)),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
|
||||
// Start / Stop button
|
||||
if (!isActive)
|
||||
ElevatedButton(
|
||||
key: const Key('startWalkGuideButton'),
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
|
||||
onPressed: state.startWalkGuide,
|
||||
child: const Text('Mulai WalkGuide'),
|
||||
)
|
||||
else
|
||||
ElevatedButton(
|
||||
key: const Key('stopWalkGuideButton'),
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||
onPressed: state.stopWalkGuide,
|
||||
child: const Text('Berhenti WalkGuide'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// SOS button
|
||||
OutlinedButton.icon(
|
||||
key: const Key('sosShortcutButton'),
|
||||
icon: const Icon(Icons.warning_amber, color: Colors.red),
|
||||
label: const Text('SOS', style: TextStyle(color: Colors.red)),
|
||||
onPressed: state.openSos,
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── SOS Screen ───────────────────────────────────────────────────────────────
|
||||
|
||||
class _SosScreen extends StatelessWidget {
|
||||
final _AppState state;
|
||||
const _SosScreen({required this.state});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sent = state.sosStatus == _SosStatus.triggered;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('SOS Darurat'),
|
||||
leading: IconButton(
|
||||
key: const Key('sosBackButton'),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: state.goBack,
|
||||
),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
if (sent)
|
||||
const Text(
|
||||
'SOS TERKIRIM',
|
||||
key: Key('sosSentLabel'),
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
const Text(
|
||||
'Tekan tombol SOS untuk mengirim bantuan darurat',
|
||||
key: Key('sosInstruction'),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
key: const Key('sendSosButton'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 40, vertical: 20),
|
||||
),
|
||||
onPressed: () async {
|
||||
// Tampilkan dialog konfirmasi
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: const Text('Konfirmasi SOS'),
|
||||
content: const Text('Kirim sinyal SOS ke Guardian?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
key: const Key('cancelSosButton'),
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
TextButton(
|
||||
key: const Key('confirmSosButton'),
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Ya, Kirim SOS'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed == true) {
|
||||
await state.sendSos();
|
||||
}
|
||||
},
|
||||
child: const Text(
|
||||
'KIRIM SOS',
|
||||
style: TextStyle(fontSize: 20, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Flow 2 — Login → WalkGuide → Start → Stop → SOS', () {
|
||||
/// Helper: login dan buka WalkGuide screen
|
||||
Future<void> loginAndOpenWalkGuide(WidgetTester tester) async {
|
||||
await tester.pumpWidget(const _StubApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(find.byKey(const Key('emailField')), _kEmail);
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('passwordField')), _kPassword);
|
||||
await tester.tap(find.byKey(const Key('loginButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(const Key('openWalkGuideButton')));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets(
|
||||
'2.1 WalkGuide screen tampil setelah login dan tap tombol WalkGuide',
|
||||
(tester) async {
|
||||
await loginAndOpenWalkGuide(tester);
|
||||
|
||||
expect(find.text('WalkGuide'), findsOneWidget,
|
||||
reason: 'Harus masuk ke WalkGuide screen');
|
||||
expect(find.byKey(const Key('walkGuideStatusBadge')), findsOneWidget);
|
||||
expect(find.text('WalkGuide TIDAK AKTIF'), findsOneWidget,
|
||||
reason: 'Status awal harus TIDAK AKTIF');
|
||||
|
||||
print(
|
||||
'[PASS] 2.1 WalkGuide screen tampil dengan status awal TIDAK AKTIF');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'2.2 Tap "Mulai WalkGuide" → status berubah menjadi AKTIF',
|
||||
(tester) async {
|
||||
await loginAndOpenWalkGuide(tester);
|
||||
|
||||
expect(find.byKey(const Key('startWalkGuideButton')), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byKey(const Key('startWalkGuideButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('WalkGuide AKTIF'), findsOneWidget,
|
||||
reason: 'Status harus AKTIF setelah tombol ditekan');
|
||||
expect(find.byKey(const Key('stopWalkGuideButton')), findsOneWidget,
|
||||
reason: 'Tombol Stop harus muncul menggantikan tombol Start');
|
||||
|
||||
print('[PASS] 2.2 Mulai WalkGuide → status AKTIF');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'2.3 Obstacle terdeteksi tampil di layar saat WalkGuide aktif',
|
||||
(tester) async {
|
||||
await loginAndOpenWalkGuide(tester);
|
||||
await tester.tap(find.byKey(const Key('startWalkGuideButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tunggu simulasi obstacle (300ms delay di stub)
|
||||
await tester.pump(const Duration(milliseconds: 400));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('obstacleHeader')), findsOneWidget,
|
||||
reason: 'Header obstacle harus muncul saat ada obstacle');
|
||||
|
||||
print('[PASS] 2.3 Obstacle terdeteksi ditampilkan di layar');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'2.4 Tap "Berhenti WalkGuide" → status kembali TIDAK AKTIF & obstacle hilang',
|
||||
(tester) async {
|
||||
await loginAndOpenWalkGuide(tester);
|
||||
|
||||
// Start
|
||||
await tester.tap(find.byKey(const Key('startWalkGuideButton')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pump(const Duration(milliseconds: 400));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('WalkGuide AKTIF'), findsOneWidget);
|
||||
|
||||
// Stop
|
||||
await tester.tap(find.byKey(const Key('stopWalkGuideButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('WalkGuide TIDAK AKTIF'), findsOneWidget,
|
||||
reason: 'Status harus kembali TIDAK AKTIF');
|
||||
expect(find.byKey(const Key('obstacleHeader')), findsNothing,
|
||||
reason: 'Daftar obstacle harus hilang setelah stop');
|
||||
expect(find.byKey(const Key('startWalkGuideButton')), findsOneWidget,
|
||||
reason: 'Tombol Start harus muncul kembali');
|
||||
|
||||
print(
|
||||
'[PASS] 2.4 Stop WalkGuide → status TIDAK AKTIF & obstacle hilang');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'2.5 Tap tombol SOS dari WalkGuide screen → SOS screen terbuka',
|
||||
(tester) async {
|
||||
await loginAndOpenWalkGuide(tester);
|
||||
|
||||
await tester.tap(find.byKey(const Key('sosShortcutButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('SOS Darurat'), findsOneWidget,
|
||||
reason: 'Harus berpindah ke SOS screen');
|
||||
expect(find.byKey(const Key('sendSosButton')), findsOneWidget);
|
||||
|
||||
print('[PASS] 2.5 Tombol SOS dari WalkGuide → SOS screen terbuka');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'2.6 Konfirmasi SOS → SOS terkirim dan label "SOS TERKIRIM" muncul',
|
||||
(tester) async {
|
||||
await loginAndOpenWalkGuide(tester);
|
||||
await tester.tap(find.byKey(const Key('sosShortcutButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap Send SOS button
|
||||
await tester.tap(find.byKey(const Key('sendSosButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Dialog konfirmasi muncul
|
||||
expect(find.text('Konfirmasi SOS'), findsOneWidget,
|
||||
reason: 'Dialog konfirmasi harus muncul');
|
||||
expect(find.byKey(const Key('confirmSosButton')), findsOneWidget);
|
||||
expect(find.byKey(const Key('cancelSosButton')), findsOneWidget);
|
||||
|
||||
// Konfirmasi
|
||||
await tester.tap(find.byKey(const Key('confirmSosButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('sosSentLabel')), findsOneWidget,
|
||||
reason: '"SOS TERKIRIM" harus tampil setelah konfirmasi');
|
||||
expect(find.text('SOS TERKIRIM'), findsOneWidget);
|
||||
// Tombol send tidak lagi tampil setelah SOS terkirim
|
||||
expect(find.byKey(const Key('sendSosButton')), findsNothing,
|
||||
reason: 'Tombol KIRIM SOS tidak boleh tampil setelah SOS terkirim');
|
||||
|
||||
print('[PASS] 2.6 Konfirmasi SOS → "SOS TERKIRIM" muncul');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'2.7 Batal konfirmasi SOS → SOS tidak terkirim, screen tetap sama',
|
||||
(tester) async {
|
||||
await loginAndOpenWalkGuide(tester);
|
||||
await tester.tap(find.byKey(const Key('sosShortcutButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(const Key('sendSosButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap batal
|
||||
await tester.tap(find.byKey(const Key('cancelSosButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('sosSentLabel')), findsNothing,
|
||||
reason: 'SOS tidak boleh terkirim jika user menekan Batal');
|
||||
expect(find.byKey(const Key('sendSosButton')), findsOneWidget,
|
||||
reason: 'Tombol KIRIM SOS harus masih ada');
|
||||
|
||||
print('[PASS] 2.7 Batal konfirmasi → SOS tidak terkirim');
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
+568
@@ -0,0 +1,568 @@
|
||||
// integration_test/flow_3_notification_read_all_test.dart
|
||||
//
|
||||
// E2E Flow 3: Login → Notifikasi → Tandai Semua Dibaca → Kembali
|
||||
//
|
||||
// Alur yang diuji:
|
||||
// 1. Login berhasil → Dashboard
|
||||
// 2. Tap ikon notifikasi / tombol "Buka Notifikasi" → NotificationScreen
|
||||
// 3. Unread badge count ditampilkan dengan benar
|
||||
// 4. Tap "Tandai Semua Dibaca" → semua notifikasi berubah menjadi dibaca
|
||||
// 5. Unread badge menjadi 0
|
||||
// 6. Tap kembali → kembali ke Dashboard
|
||||
// 7. Badge di Dashboard juga ter-update menjadi 0
|
||||
//
|
||||
// Jalankan:
|
||||
// flutter test integration_test/flow_3_notification_read_all_test.dart
|
||||
|
||||
// ignore_for_file: avoid_print
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
// ─── Fake Credentials ────────────────────────────────────────────────────────
|
||||
|
||||
const _kEmail = 'user@walkguide.test';
|
||||
const _kPassword = 'Password123!';
|
||||
|
||||
// ─── Data Models ─────────────────────────────────────────────────────────────
|
||||
|
||||
class _Notification {
|
||||
final int id;
|
||||
final String content;
|
||||
final String sender;
|
||||
bool isRead;
|
||||
|
||||
_Notification({
|
||||
required this.id,
|
||||
required this.content,
|
||||
required this.sender,
|
||||
this.isRead = false,
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Stub State ──────────────────────────────────────────────────────────────
|
||||
|
||||
class _AppState extends ChangeNotifier {
|
||||
bool _loggedIn = false;
|
||||
String _screen = 'login'; // login | dashboard | notification
|
||||
|
||||
final List<_Notification> _notifications = [
|
||||
_Notification(
|
||||
id: 1,
|
||||
content: 'Hati-hati di persimpangan Jalan A',
|
||||
sender: 'Guardian Pak Budi'),
|
||||
_Notification(
|
||||
id: 2,
|
||||
content: 'Cuaca memburuk, segera pulang',
|
||||
sender: 'Guardian Pak Budi'),
|
||||
_Notification(
|
||||
id: 3,
|
||||
content: 'WalkGuide kamu sudah berjalan 2 jam',
|
||||
sender: 'Sistem'),
|
||||
_Notification(
|
||||
id: 4,
|
||||
content: 'Ini notifikasi yang sudah dibaca',
|
||||
sender: 'Sistem',
|
||||
isRead: true),
|
||||
];
|
||||
|
||||
bool get loggedIn => _loggedIn;
|
||||
String get screen => _screen;
|
||||
List<_Notification> get notifications => List.unmodifiable(_notifications);
|
||||
int get unreadCount => _notifications.where((n) => !n.isRead).length;
|
||||
|
||||
Future<bool> login(String email, String password) async {
|
||||
await Future.delayed(const Duration(milliseconds: 150));
|
||||
if (email == _kEmail && password == _kPassword) {
|
||||
_loggedIn = true;
|
||||
_screen = 'dashboard';
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void openNotifications() {
|
||||
_screen = 'notification';
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void goToDashboard() {
|
||||
_screen = 'dashboard';
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void markAsRead(int id) {
|
||||
final n = _notifications.firstWhere((n) => n.id == id);
|
||||
if (!n.isRead) {
|
||||
n.isRead = true;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> markAllAsRead() async {
|
||||
await Future.delayed(const Duration(milliseconds: 150));
|
||||
for (final n in _notifications) {
|
||||
n.isRead = true;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Stub App ─────────────────────────────────────────────────────────────────
|
||||
|
||||
class _StubApp extends StatefulWidget {
|
||||
const _StubApp();
|
||||
|
||||
@override
|
||||
State<_StubApp> createState() => _StubAppState();
|
||||
}
|
||||
|
||||
class _StubAppState extends State<_StubApp> {
|
||||
final _state = _AppState();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_state.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'WalkGuide Test',
|
||||
home: ListenableBuilder(
|
||||
listenable: _state,
|
||||
builder: (context, _) {
|
||||
switch (_state.screen) {
|
||||
case 'notification':
|
||||
return _NotificationScreen(state: _state);
|
||||
case 'dashboard':
|
||||
return _DashboardScreen(state: _state);
|
||||
default:
|
||||
return _LoginScreen(state: _state);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Login Screen ─────────────────────────────────────────────────────────────
|
||||
|
||||
class _LoginScreen extends StatefulWidget {
|
||||
final _AppState state;
|
||||
const _LoginScreen({required this.state});
|
||||
|
||||
@override
|
||||
State<_LoginScreen> createState() => _LoginScreenState();
|
||||
}
|
||||
|
||||
class _LoginScreenState extends State<_LoginScreen> {
|
||||
final _emailCtrl = TextEditingController();
|
||||
final _passwordCtrl = TextEditingController();
|
||||
bool _loading = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailCtrl.dispose();
|
||||
_passwordCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
setState(() => _loading = true);
|
||||
await widget.state.login(_emailCtrl.text.trim(), _passwordCtrl.text.trim());
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Login')),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(children: [
|
||||
TextField(
|
||||
key: const Key('emailField'),
|
||||
controller: _emailCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Email'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
key: const Key('passwordField'),
|
||||
controller: _passwordCtrl,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(labelText: 'Password'),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (_loading)
|
||||
const CircularProgressIndicator()
|
||||
else
|
||||
ElevatedButton(
|
||||
key: const Key('loginButton'),
|
||||
onPressed: _submit,
|
||||
child: const Text('Login'),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Dashboard Screen ─────────────────────────────────────────────────────────
|
||||
|
||||
class _DashboardScreen extends StatelessWidget {
|
||||
final _AppState state;
|
||||
const _DashboardScreen({required this.state});
|
||||
|
||||
@override
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Center(
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
const Text('Selamat datang di Dashboard'),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
key: const Key('openNotifButton'),
|
||||
onPressed: state.openNotifications,
|
||||
child: Text(
|
||||
unread > 0
|
||||
? 'Lihat Notifikasi ($unread belum dibaca)'
|
||||
: 'Lihat Notifikasi',
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Notification Screen ──────────────────────────────────────────────────────
|
||||
|
||||
class _NotificationScreen extends StatelessWidget {
|
||||
final _AppState state;
|
||||
const _NotificationScreen({required this.state});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final notifs = state.notifications;
|
||||
final unread = state.unreadCount;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Notifikasi'),
|
||||
leading: IconButton(
|
||||
key: const Key('notifBackButton'),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: state.goToDashboard,
|
||||
),
|
||||
actions: [
|
||||
if (unread > 0)
|
||||
TextButton(
|
||||
key: const Key('markAllReadButton'),
|
||||
onPressed: state.markAllAsRead,
|
||||
child: const Text(
|
||||
'Tandai Semua Dibaca',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(children: [
|
||||
// Unread count info
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(children: [
|
||||
Text(
|
||||
'$unread notifikasi belum dibaca',
|
||||
key: const Key('unreadCountLabel'),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (unread == 0)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 8),
|
||||
child: Icon(Icons.check_circle, color: Colors.green, size: 18),
|
||||
),
|
||||
]),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
|
||||
// Notification list
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
key: const Key('notifList'),
|
||||
itemCount: notifs.length,
|
||||
itemBuilder: (_, i) {
|
||||
final n = notifs[i];
|
||||
return ListTile(
|
||||
key: Key('notifItem_${n.id}'),
|
||||
leading: Icon(
|
||||
n.isRead ? Icons.mark_email_read : Icons.email,
|
||||
color: n.isRead ? Colors.grey : Colors.blue,
|
||||
),
|
||||
title: Text(n.content),
|
||||
subtitle: Text('Dari: ${n.sender}'),
|
||||
tileColor: n.isRead ? null : Colors.blue.shade50,
|
||||
trailing: n.isRead
|
||||
? const Text('Dibaca',
|
||||
key: Key('readLabel'),
|
||||
style: TextStyle(color: Colors.grey, fontSize: 12))
|
||||
: TextButton(
|
||||
onPressed: () => state.markAsRead(n.id),
|
||||
child: const Text('Tandai Dibaca'),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Flow 3 — Login → Notifikasi → Tandai Semua Dibaca → Kembali', () {
|
||||
/// Helper: login
|
||||
Future<void> loginAndGoToDashboard(WidgetTester tester) async {
|
||||
await tester.pumpWidget(const _StubApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(find.byKey(const Key('emailField')), _kEmail);
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('passwordField')), _kPassword);
|
||||
await tester.tap(find.byKey(const Key('loginButton')));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets(
|
||||
'3.1 Dashboard menampilkan badge jumlah notifikasi belum dibaca',
|
||||
(tester) async {
|
||||
await loginAndGoToDashboard(tester);
|
||||
|
||||
expect(find.text('Dashboard'), findsOneWidget);
|
||||
// Ada 3 notifikasi unread (id 1, 2, 3); id 4 sudah read
|
||||
expect(find.byKey(const Key('dashboardBadge')), findsOneWidget,
|
||||
reason: 'Badge harus muncul karena ada notif belum dibaca');
|
||||
expect(find.text('3'), findsOneWidget,
|
||||
reason: 'Badge harus menampilkan angka 3');
|
||||
|
||||
print(
|
||||
'[PASS] 3.1 Dashboard badge menampilkan jumlah unread yang benar');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'3.2 Tap ikon notifikasi → NotificationScreen terbuka dengan daftar notif',
|
||||
(tester) async {
|
||||
await loginAndGoToDashboard(tester);
|
||||
|
||||
await tester.tap(find.byKey(const Key('notifIconButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Notifikasi'), findsOneWidget,
|
||||
reason: 'Harus masuk ke NotificationScreen');
|
||||
expect(find.byKey(const Key('notifList')), findsOneWidget,
|
||||
reason: 'ListView notifikasi harus tampil');
|
||||
|
||||
// 4 notifikasi harus tampil
|
||||
expect(find.byKey(const Key('notifItem_1')), findsOneWidget);
|
||||
expect(find.byKey(const Key('notifItem_2')), findsOneWidget);
|
||||
expect(find.byKey(const Key('notifItem_3')), findsOneWidget);
|
||||
expect(find.byKey(const Key('notifItem_4')), findsOneWidget);
|
||||
|
||||
print('[PASS] 3.2 NotificationScreen terbuka dengan semua notifikasi');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'3.3 Label unread count tampil dengan angka yang benar di NotificationScreen',
|
||||
(tester) async {
|
||||
await loginAndGoToDashboard(tester);
|
||||
|
||||
await tester.tap(find.byKey(const Key('notifIconButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('unreadCountLabel')), findsOneWidget);
|
||||
expect(find.text('3 notifikasi belum dibaca'), findsOneWidget,
|
||||
reason: 'Harus menampilkan "3 notifikasi belum dibaca"');
|
||||
|
||||
print('[PASS] 3.3 Unread count label menampilkan angka yang benar');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'3.4 Tombol "Tandai Semua Dibaca" muncul saat ada notif belum dibaca',
|
||||
(tester) async {
|
||||
await loginAndGoToDashboard(tester);
|
||||
|
||||
await tester.tap(find.byKey(const Key('notifIconButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('markAllReadButton')), findsOneWidget,
|
||||
reason: 'Tombol "Tandai Semua Dibaca" harus tampil');
|
||||
|
||||
print('[PASS] 3.4 Tombol "Tandai Semua Dibaca" tampil');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'3.5 Tap "Tandai Semua Dibaca" → semua notif jadi dibaca, count jadi 0',
|
||||
(tester) async {
|
||||
await loginAndGoToDashboard(tester);
|
||||
|
||||
await tester.tap(find.byKey(const Key('notifIconButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Sebelum: 3 unread
|
||||
expect(find.text('3 notifikasi belum dibaca'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byKey(const Key('markAllReadButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Setelah: 0 unread
|
||||
expect(find.text('0 notifikasi belum dibaca'), findsOneWidget,
|
||||
reason: 'Setelah tandai semua, count harus 0');
|
||||
expect(find.byKey(const Key('markAllReadButton')), findsNothing,
|
||||
reason: 'Tombol "Tandai Semua" harus hilang saat tidak ada unread');
|
||||
|
||||
print('[PASS] 3.5 Tandai semua dibaca → count menjadi 0');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'3.6 Tandai satu notif dibaca → count berkurang 1',
|
||||
(tester) async {
|
||||
await loginAndGoToDashboard(tester);
|
||||
|
||||
await tester.tap(find.byKey(const Key('notifIconButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('3 notifikasi belum dibaca'), findsOneWidget);
|
||||
|
||||
// Scroll ke notif pertama yang unread dan tandai
|
||||
final markButtons = find.text('Tandai Dibaca');
|
||||
expect(markButtons, findsWidgets);
|
||||
await tester.tap(markButtons.first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('2 notifikasi belum dibaca'), findsOneWidget,
|
||||
reason: 'Count harus berkurang menjadi 2');
|
||||
|
||||
print('[PASS] 3.6 Tandai satu notif → count berkurang 1');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'3.7 Kembali ke Dashboard → badge ter-update menjadi 0 setelah tandai semua',
|
||||
(tester) async {
|
||||
await loginAndGoToDashboard(tester);
|
||||
|
||||
// Buka notifikasi
|
||||
await tester.tap(find.byKey(const Key('notifIconButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tandai semua dibaca
|
||||
await tester.tap(find.byKey(const Key('markAllReadButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Kembali ke dashboard
|
||||
await tester.tap(find.byKey(const Key('notifBackButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Dashboard'), findsOneWidget,
|
||||
reason: 'Harus kembali ke Dashboard');
|
||||
expect(find.byKey(const Key('dashboardBadge')), findsNothing,
|
||||
reason: 'Badge harus hilang karena semua sudah dibaca');
|
||||
expect(find.text('Lihat Notifikasi'), findsOneWidget,
|
||||
reason: 'Tombol notifikasi harus tanpa keterangan "belum dibaca"');
|
||||
|
||||
print('[PASS] 3.7 Kembali ke Dashboard → badge hilang');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'3.8 Buka notifikasi via tombol di body Dashboard juga berfungsi',
|
||||
(tester) async {
|
||||
await loginAndGoToDashboard(tester);
|
||||
|
||||
await tester.tap(find.byKey(const Key('openNotifButton')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Notifikasi'), findsOneWidget,
|
||||
reason:
|
||||
'Tombol di body Dashboard juga harus membuka NotificationScreen');
|
||||
|
||||
print('[PASS] 3.8 Tombol notif di body Dashboard berfungsi');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'3.9 Full round-trip: Login → Notif → Tandai Semua → Kembali → Badge = 0',
|
||||
(tester) async {
|
||||
await loginAndGoToDashboard(tester);
|
||||
|
||||
// Verify badge ada
|
||||
expect(find.byKey(const Key('dashboardBadge')), findsOneWidget);
|
||||
|
||||
// Buka notifikasi
|
||||
await tester.tap(find.byKey(const Key('notifIconButton')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Notifikasi'), findsOneWidget);
|
||||
|
||||
// Tandai semua
|
||||
await tester.tap(find.byKey(const Key('markAllReadButton')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('0 notifikasi belum dibaca'), findsOneWidget);
|
||||
|
||||
// Kembali
|
||||
await tester.tap(find.byKey(const Key('notifBackButton')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Dashboard'), findsOneWidget);
|
||||
expect(find.byKey(const Key('dashboardBadge')), findsNothing,
|
||||
reason: 'Setelah semua dibaca, badge harus hilang dari Dashboard');
|
||||
|
||||
print('[PASS] 3.9 Full round-trip notifikasi berhasil');
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user