1007 lines
38 KiB
Dart
1007 lines
38 KiB
Dart
// integration_test/app_flow_test.dart
|
|
// ignore_for_file: prefer_const_constructors, no_leading_underscores_for_local_identifiers
|
|
//
|
|
// Integration Tests (E2E) untuk WalkGuide App — 3 alur utama:
|
|
// Flow 1: Login → Dashboard → Logout
|
|
// Flow 2: Login → WalkGuide → Start → Stop → SOS
|
|
// Flow 3: Login → Notifikasi → Tandai Semua Dibaca → Kembali
|
|
//
|
|
// Jalankan di device fisik:
|
|
// flutter test integration_test/app_flow_test.dart
|
|
// atau:
|
|
// flutter drive --driver=test_driver/integration_test.dart \
|
|
// --target=integration_test/app_flow_test.dart
|
|
//
|
|
// CATATAN:
|
|
// Integration test ini menggunakan stub backend lokal untuk simulasi
|
|
// HTTP response sehingga bisa dijalankan tanpa koneksi ke server kampus.
|
|
// Untuk E2E full terhadap server live, lihat bagian LIVE TEST di bawah.
|
|
//
|
|
// Dev dependencies (pubspec.yaml):
|
|
// integration_test:
|
|
// sdk: flutter
|
|
// flutter_test:
|
|
// sdk: flutter
|
|
// mockito: ^5.4.4
|
|
|
|
// ignore_for_file: avoid_print
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:integration_test/integration_test.dart';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Minimal App stub untuk integration test tanpa full DI stack
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// PENTING: Dalam project nyata, ganti _IntegrationTestApp dengan:
|
|
// import 'package:walkguide_app/main.dart' as app;
|
|
// void main() { app.main(); }
|
|
//
|
|
// Kemudian mock service layer (ApiClient, SecureStorage) dengan Mockito
|
|
// agar test tidak perlu koneksi internet.
|
|
//
|
|
// Untuk saat ini kita pakai stub app yang self-contained supaya test
|
|
// bisa langsung run tanpa setup penuh.
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ─── Shared Models ─────────────────────────────────────────────────────────
|
|
// ---------------------------------------------------------------------------
|
|
|
|
class FakeUser {
|
|
final String email;
|
|
final String password;
|
|
final String role;
|
|
final String token;
|
|
|
|
const FakeUser({
|
|
required this.email,
|
|
required this.password,
|
|
required this.role,
|
|
required this.token,
|
|
});
|
|
}
|
|
|
|
const _fakeUserAccount = FakeUser(
|
|
email: 'user@walkguide.test',
|
|
password: 'Password123!',
|
|
role: 'ROLE_USER',
|
|
token: 'fake-jwt-token-user-xxx',
|
|
);
|
|
|
|
const _fakeGuardianAccount = FakeUser(
|
|
email: 'guardian@walkguide.test',
|
|
password: 'Password123!',
|
|
role: 'ROLE_GUARDIAN',
|
|
token: 'fake-jwt-token-guardian-xxx',
|
|
);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ─── Stub App ───────────────────────────────────────────────────────────────
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// State global sederhana (pengganti DI Container di test)
|
|
class _AppState extends ChangeNotifier {
|
|
FakeUser? _currentUser;
|
|
bool _walkGuideActive = false;
|
|
bool _sosSent = false;
|
|
List<_NotifItem> _notifications = [
|
|
_NotifItem(id: 1, content: 'Hati-hati di persimpangan', isRead: false),
|
|
_NotifItem(id: 2, content: 'Cuaca memburuk hari ini', isRead: false),
|
|
_NotifItem(id: 3, content: 'Pesan lama sudah dibaca', isRead: true),
|
|
];
|
|
|
|
FakeUser? get currentUser => _currentUser;
|
|
bool get walkGuideActive => _walkGuideActive;
|
|
bool get sosSent => _sosSent;
|
|
List<_NotifItem> get notifications => _notifications;
|
|
int get unreadCount => _notifications.where((n) => !n.isRead).length;
|
|
|
|
/// Simulasi login: return true jika credentials cocok
|
|
Future<bool> login(String email, String password) async {
|
|
await Future.delayed(const Duration(milliseconds: 300));
|
|
if (email == _fakeUserAccount.email &&
|
|
password == _fakeUserAccount.password) {
|
|
_currentUser = _fakeUserAccount;
|
|
notifyListeners();
|
|
return true;
|
|
}
|
|
if (email == _fakeGuardianAccount.email &&
|
|
password == _fakeGuardianAccount.password) {
|
|
_currentUser = _fakeGuardianAccount;
|
|
notifyListeners();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void logout() {
|
|
_currentUser = null;
|
|
_walkGuideActive = false;
|
|
_sosSent = false;
|
|
notifyListeners();
|
|
}
|
|
|
|
void toggleWalkGuide() {
|
|
_walkGuideActive = !_walkGuideActive;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> sendSos() async {
|
|
await Future.delayed(const Duration(milliseconds: 200));
|
|
_sosSent = true;
|
|
notifyListeners();
|
|
}
|
|
|
|
void markAllRead() {
|
|
_notifications = _notifications
|
|
.map((n) => _NotifItem(id: n.id, content: n.content, isRead: true))
|
|
.toList();
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
class _NotifItem {
|
|
final int id;
|
|
final String content;
|
|
final bool isRead;
|
|
const _NotifItem(
|
|
{required this.id, required this.content, required this.isRead});
|
|
}
|
|
|
|
// ── Screens ────────────────────────────────────────────────────────────────
|
|
|
|
class _LoginScreen extends StatefulWidget {
|
|
const _LoginScreen();
|
|
|
|
@override
|
|
State<_LoginScreen> createState() => _LoginScreenState();
|
|
}
|
|
|
|
class _LoginScreenState extends State<_LoginScreen> {
|
|
final _emailCtrl = TextEditingController();
|
|
final _passCtrl = TextEditingController();
|
|
bool _loading = false;
|
|
String? _error;
|
|
|
|
@override
|
|
void dispose() {
|
|
_emailCtrl.dispose();
|
|
_passCtrl.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _submit() async {
|
|
final state = _AppStateProvider.of(context);
|
|
setState(() {
|
|
_loading = true;
|
|
_error = null;
|
|
});
|
|
final ok = await state.login(_emailCtrl.text.trim(), _passCtrl.text);
|
|
if (!mounted) return;
|
|
setState(() => _loading = false);
|
|
if (ok) {
|
|
Navigator.of(context).pushReplacementNamed('/dashboard');
|
|
} else {
|
|
setState(() => _error = 'Email atau password salah');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('Login')),
|
|
body: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
const Text('WalkGuide',
|
|
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 24),
|
|
if (_error != null)
|
|
Container(
|
|
key: const Key('login_error'),
|
|
padding: const EdgeInsets.all(12),
|
|
color: Colors.red.shade50,
|
|
child: Text(_error!, style: const TextStyle(color: Colors.red)),
|
|
),
|
|
TextField(
|
|
key: const Key('email_field'),
|
|
controller: _emailCtrl,
|
|
keyboardType: TextInputType.emailAddress,
|
|
decoration: const InputDecoration(labelText: 'Email'),
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextField(
|
|
key: const Key('password_field'),
|
|
controller: _passCtrl,
|
|
obscureText: true,
|
|
decoration: const InputDecoration(labelText: 'Password'),
|
|
),
|
|
const SizedBox(height: 24),
|
|
_loading
|
|
? const Center(
|
|
child: CircularProgressIndicator(key: Key('login_loading')))
|
|
: ElevatedButton(
|
|
key: const Key('login_button'),
|
|
onPressed: _submit,
|
|
child: const Text('Masuk'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DashboardScreen extends StatelessWidget {
|
|
const _DashboardScreen();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final state = _AppStateProvider.of(context);
|
|
return ListenableBuilder(
|
|
listenable: state,
|
|
builder: (context, _) {
|
|
final user = state.currentUser;
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Dashboard'),
|
|
actions: [
|
|
TextButton(
|
|
key: const Key('logout_button'),
|
|
onPressed: () {
|
|
state.logout();
|
|
Navigator.of(context).pushReplacementNamed('/login');
|
|
},
|
|
child: const Text('Keluar'),
|
|
),
|
|
],
|
|
),
|
|
body: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Card(
|
|
key: const Key('user_card'),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
key: const Key('user_role_text'),
|
|
'Role: ${user?.role ?? '-'}',
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
Text(
|
|
key: const Key('user_email_text'),
|
|
'Email: ${user?.email ?? '-'}',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
ElevatedButton.icon(
|
|
key: const Key('go_walkguide_button'),
|
|
onPressed: () =>
|
|
Navigator.of(context).pushNamed('/walkguide'),
|
|
icon: const Icon(Icons.directions_walk),
|
|
label: const Text('WalkGuide'),
|
|
),
|
|
const SizedBox(height: 8),
|
|
ElevatedButton.icon(
|
|
key: const Key('go_notifications_button'),
|
|
onPressed: () =>
|
|
Navigator.of(context).pushNamed('/notifications'),
|
|
icon: const Icon(Icons.notifications),
|
|
label: Stack(
|
|
children: [
|
|
const Text('Notifikasi'),
|
|
if (state.unreadCount > 0)
|
|
Positioned(
|
|
right: -4,
|
|
top: -4,
|
|
child: Container(
|
|
key: const Key('dashboard_unread_badge'),
|
|
padding: const EdgeInsets.all(4),
|
|
decoration: const BoxDecoration(
|
|
color: Colors.red,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Text(
|
|
'${state.unreadCount}',
|
|
style: const TextStyle(
|
|
color: Colors.white, fontSize: 10),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
ElevatedButton.icon(
|
|
key: const Key('go_sos_button'),
|
|
onPressed: () => Navigator.of(context).pushNamed('/sos'),
|
|
icon: const Icon(Icons.sos, color: Colors.red),
|
|
label: const Text('SOS'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _WalkGuideScreen extends StatelessWidget {
|
|
const _WalkGuideScreen();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final state = _AppStateProvider.of(context);
|
|
return ListenableBuilder(
|
|
listenable: state,
|
|
builder: (context, _) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('WalkGuide')),
|
|
body: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
key: const Key('wg_status_bar'),
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: state.walkGuideActive ? Colors.green : Colors.grey,
|
|
borderRadius: BorderRadius.circular(24),
|
|
),
|
|
child: Text(
|
|
key: const Key('wg_status_text'),
|
|
state.walkGuideActive ? 'AKTIF' : 'TIDAK AKTIF',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 18),
|
|
),
|
|
),
|
|
const SizedBox(height: 32),
|
|
ElevatedButton.icon(
|
|
key: const Key('wg_toggle_button'),
|
|
onPressed: state.toggleWalkGuide,
|
|
icon: Icon(
|
|
state.walkGuideActive ? Icons.stop : Icons.play_arrow),
|
|
label: Text(state.walkGuideActive
|
|
? 'Stop WalkGuide'
|
|
: 'Start WalkGuide'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor:
|
|
state.walkGuideActive ? Colors.red : Colors.blue,
|
|
foregroundColor: Colors.white,
|
|
minimumSize: const Size(200, 52),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
OutlinedButton.icon(
|
|
key: const Key('wg_sos_button'),
|
|
onPressed: () => Navigator.of(context).pushNamed('/sos'),
|
|
icon: const Icon(Icons.sos, color: Colors.red),
|
|
label: const Text('SOS', style: TextStyle(color: Colors.red)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SosScreen extends StatelessWidget {
|
|
const _SosScreen();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final state = _AppStateProvider.of(context);
|
|
return ListenableBuilder(
|
|
listenable: state,
|
|
builder: (context, _) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('SOS')),
|
|
body: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
if (state.sosSent)
|
|
Container(
|
|
key: const Key('sos_sent_banner'),
|
|
margin: const EdgeInsets.all(16),
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.green.shade50,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: Colors.green),
|
|
),
|
|
child: const Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.check_circle, color: Colors.green),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
key: Key('sos_sent_text'),
|
|
'SOS berhasil dikirim!',
|
|
style: TextStyle(
|
|
color: Colors.green, fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
GestureDetector(
|
|
onTap: state.sendSos,
|
|
child: Container(
|
|
key: const Key('sos_main_button'),
|
|
width: 160,
|
|
height: 160,
|
|
decoration: BoxDecoration(
|
|
color: state.sosSent ? Colors.grey : Colors.red,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.sos, color: Colors.white, size: 52),
|
|
Text(
|
|
state.sosSent ? 'TERKIRIM' : 'SOS',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _NotificationScreen extends StatelessWidget {
|
|
const _NotificationScreen();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final state = _AppStateProvider.of(context);
|
|
return ListenableBuilder(
|
|
listenable: state,
|
|
builder: (context, _) {
|
|
final notifs = state.notifications;
|
|
final hasUnread = notifs.any((n) => !n.isRead);
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Notifikasi'),
|
|
actions: [
|
|
if (hasUnread)
|
|
TextButton(
|
|
key: const Key('mark_all_read_button'),
|
|
onPressed: state.markAllRead,
|
|
child: const Text('Tandai Semua'),
|
|
),
|
|
],
|
|
),
|
|
body: ListView.separated(
|
|
key: const Key('notif_list'),
|
|
itemCount: notifs.length,
|
|
separatorBuilder: (_, __) => const Divider(height: 1),
|
|
itemBuilder: (context, i) {
|
|
final n = notifs[i];
|
|
return ListTile(
|
|
key: Key('notif_item_${n.id}'),
|
|
title: Text(n.content),
|
|
trailing: n.isRead
|
|
? null
|
|
: Container(
|
|
key: Key('notif_unread_${n.id}'),
|
|
width: 10,
|
|
height: 10,
|
|
decoration: const BoxDecoration(
|
|
color: Colors.blue,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── InheritedWidget untuk state ────────────────────────────────────────────
|
|
|
|
class _AppStateProvider extends InheritedNotifier<_AppState> {
|
|
const _AppStateProvider({required super.notifier, required super.child});
|
|
|
|
static _AppState of(BuildContext context) {
|
|
final provider =
|
|
context.dependOnInheritedWidgetOfExactType<_AppStateProvider>();
|
|
assert(provider != null, '_AppStateProvider not found in widget tree');
|
|
return provider!.notifier!;
|
|
}
|
|
}
|
|
|
|
// ── Root App ───────────────────────────────────────────────────────────────
|
|
|
|
class _IntegrationTestApp extends StatelessWidget {
|
|
final _AppState _state = _AppState();
|
|
|
|
_IntegrationTestApp();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return _AppStateProvider(
|
|
notifier: _state,
|
|
child: MaterialApp(
|
|
initialRoute: '/login',
|
|
routes: {
|
|
'/login': (_) => const _LoginScreen(),
|
|
'/dashboard': (_) => const _DashboardScreen(),
|
|
'/walkguide': (_) => const _WalkGuideScreen(),
|
|
'/sos': (_) => const _SosScreen(),
|
|
'/notifications': (_) => const _NotificationScreen(),
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ─── Integration Tests ─────────────────────────────────────────────────────
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void main() {
|
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// FLOW 1: Login → Dashboard → Logout
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
group('Flow 1: Login → Dashboard → Logout', () {
|
|
testWidgets('1.1 - halaman login tampil pertama kali', (tester) async {
|
|
await tester.pumpWidget(_IntegrationTestApp());
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Login'), findsAtLeastNWidgets(1));
|
|
expect(find.byKey(const Key('email_field')), findsOneWidget);
|
|
expect(find.byKey(const Key('password_field')), findsOneWidget);
|
|
expect(find.byKey(const Key('login_button')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('1.2 - login dengan credentials salah menampilkan error',
|
|
(tester) async {
|
|
await tester.pumpWidget(_IntegrationTestApp());
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.enterText(
|
|
find.byKey(const Key('email_field')), 'wrong@email.com');
|
|
await tester.enterText(
|
|
find.byKey(const Key('password_field')), 'wrongpass');
|
|
await tester.tap(find.byKey(const Key('login_button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('login_error')), findsOneWidget);
|
|
expect(find.text('Email atau password salah'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('1.3 - login berhasil redirect ke Dashboard', (tester) async {
|
|
await tester.pumpWidget(_IntegrationTestApp());
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.enterText(
|
|
find.byKey(const Key('email_field')), _fakeUserAccount.email);
|
|
await tester.enterText(
|
|
find.byKey(const Key('password_field')), _fakeUserAccount.password);
|
|
await tester.tap(find.byKey(const Key('login_button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Dashboard'), findsOneWidget);
|
|
expect(find.byKey(const Key('user_card')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('1.4 - dashboard menampilkan role user yang benar',
|
|
(tester) async {
|
|
await tester.pumpWidget(_IntegrationTestApp());
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.enterText(
|
|
find.byKey(const Key('email_field')), _fakeUserAccount.email);
|
|
await tester.enterText(
|
|
find.byKey(const Key('password_field')), _fakeUserAccount.password);
|
|
await tester.tap(find.byKey(const Key('login_button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.textContaining('ROLE_USER'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('1.5 - dashboard menampilkan email user yang login',
|
|
(tester) async {
|
|
await tester.pumpWidget(_IntegrationTestApp());
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.enterText(
|
|
find.byKey(const Key('email_field')), _fakeUserAccount.email);
|
|
await tester.enterText(
|
|
find.byKey(const Key('password_field')), _fakeUserAccount.password);
|
|
await tester.tap(find.byKey(const Key('login_button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.textContaining(_fakeUserAccount.email), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('1.6 - logout kembali ke halaman login', (tester) async {
|
|
await tester.pumpWidget(_IntegrationTestApp());
|
|
await tester.pumpAndSettle();
|
|
|
|
// Login
|
|
await tester.enterText(
|
|
find.byKey(const Key('email_field')), _fakeUserAccount.email);
|
|
await tester.enterText(
|
|
find.byKey(const Key('password_field')), _fakeUserAccount.password);
|
|
await tester.tap(find.byKey(const Key('login_button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Dashboard'), findsOneWidget);
|
|
|
|
// Logout
|
|
await tester.tap(find.byKey(const Key('logout_button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('login_button')), findsOneWidget);
|
|
expect(find.text('Dashboard'), findsNothing);
|
|
});
|
|
|
|
testWidgets('1.7 - guardian login juga masuk ke dashboard', (tester) async {
|
|
await tester.pumpWidget(_IntegrationTestApp());
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.enterText(
|
|
find.byKey(const Key('email_field')), _fakeGuardianAccount.email);
|
|
await tester.enterText(find.byKey(const Key('password_field')),
|
|
_fakeGuardianAccount.password);
|
|
await tester.tap(find.byKey(const Key('login_button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Dashboard'), findsOneWidget);
|
|
expect(find.textContaining('ROLE_GUARDIAN'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('1.8 - dashboard menampilkan tombol navigasi ke fitur utama',
|
|
(tester) async {
|
|
await tester.pumpWidget(_IntegrationTestApp());
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.enterText(
|
|
find.byKey(const Key('email_field')), _fakeUserAccount.email);
|
|
await tester.enterText(
|
|
find.byKey(const Key('password_field')), _fakeUserAccount.password);
|
|
await tester.tap(find.byKey(const Key('login_button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('go_walkguide_button')), findsOneWidget);
|
|
expect(find.byKey(const Key('go_notifications_button')), findsOneWidget);
|
|
expect(find.byKey(const Key('go_sos_button')), findsOneWidget);
|
|
});
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// FLOW 2: Login → WalkGuide → Start → Stop → SOS
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
group('Flow 2: Login → WalkGuide → Start → Stop → SOS', () {
|
|
/// Helper: Login dan navigasi ke WalkGuide
|
|
Future<void> _loginAndGoToWalkGuide(WidgetTester tester) async {
|
|
await tester.pumpWidget(_IntegrationTestApp());
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.enterText(
|
|
find.byKey(const Key('email_field')), _fakeUserAccount.email);
|
|
await tester.enterText(
|
|
find.byKey(const Key('password_field')), _fakeUserAccount.password);
|
|
await tester.tap(find.byKey(const Key('login_button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.byKey(const Key('go_walkguide_button')));
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
testWidgets('2.1 - navigasi dari Dashboard ke WalkGuide berhasil',
|
|
(tester) async {
|
|
await _loginAndGoToWalkGuide(tester);
|
|
|
|
expect(find.text('WalkGuide'), findsAtLeastNWidgets(1));
|
|
expect(find.byKey(const Key('wg_toggle_button')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('2.2 - status awal WalkGuide adalah TIDAK AKTIF',
|
|
(tester) async {
|
|
await _loginAndGoToWalkGuide(tester);
|
|
|
|
expect(find.text('TIDAK AKTIF'), findsOneWidget);
|
|
expect(find.text('Start WalkGuide'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('2.3 - tap Start mengubah status menjadi AKTIF',
|
|
(tester) async {
|
|
await _loginAndGoToWalkGuide(tester);
|
|
|
|
await tester.tap(find.byKey(const Key('wg_toggle_button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('AKTIF'), findsOneWidget);
|
|
expect(find.text('Stop WalkGuide'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('2.4 - status bar berwarna hijau saat WalkGuide aktif',
|
|
(tester) async {
|
|
await _loginAndGoToWalkGuide(tester);
|
|
|
|
await tester.tap(find.byKey(const Key('wg_toggle_button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
final statusBar =
|
|
tester.widget<Container>(find.byKey(const Key('wg_status_bar')));
|
|
final decoration = statusBar.decoration as BoxDecoration;
|
|
expect(decoration.color, Colors.green);
|
|
});
|
|
|
|
testWidgets('2.5 - tap Stop mengembalikan status ke TIDAK AKTIF',
|
|
(tester) async {
|
|
await _loginAndGoToWalkGuide(tester);
|
|
|
|
// Start
|
|
await tester.tap(find.byKey(const Key('wg_toggle_button')));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('AKTIF'), findsOneWidget);
|
|
|
|
// Stop
|
|
await tester.tap(find.byKey(const Key('wg_toggle_button')));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('TIDAK AKTIF'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('2.6 - tombol Start/Stop dapat di-toggle berulang kali',
|
|
(tester) async {
|
|
await _loginAndGoToWalkGuide(tester);
|
|
|
|
for (int i = 0; i < 3; i++) {
|
|
await tester.tap(find.byKey(const Key('wg_toggle_button')));
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
// Setelah 3 kali tap: aktif → tidak aktif → aktif
|
|
expect(find.text('AKTIF'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('2.7 - tombol SOS tersedia di layar WalkGuide', (tester) async {
|
|
await _loginAndGoToWalkGuide(tester);
|
|
|
|
expect(find.byKey(const Key('wg_sos_button')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('2.8 - tap tombol SOS dari WalkGuide navigasi ke SOS screen',
|
|
(tester) async {
|
|
await _loginAndGoToWalkGuide(tester);
|
|
|
|
await tester.tap(find.byKey(const Key('wg_sos_button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('sos_main_button')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('2.9 - tap tombol SOS di SOS screen mengirim SOS',
|
|
(tester) async {
|
|
await _loginAndGoToWalkGuide(tester);
|
|
|
|
await tester.tap(find.byKey(const Key('wg_sos_button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.byKey(const Key('sos_main_button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('sos_sent_banner')), findsOneWidget);
|
|
expect(find.text('SOS berhasil dikirim!'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets(
|
|
'2.10 - setelah SOS terkirim, tombol SOS berubah label TERKIRIM',
|
|
(tester) async {
|
|
await _loginAndGoToWalkGuide(tester);
|
|
|
|
await tester.tap(find.byKey(const Key('wg_sos_button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.byKey(const Key('sos_main_button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('TERKIRIM'), findsOneWidget);
|
|
});
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// FLOW 3: Login → Notifikasi → Tandai Semua → Kembali ke Dashboard
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
group('Flow 3: Login → Notifikasi → Mark All Read → Kembali', () {
|
|
/// Helper: Login dan navigasi ke Notifikasi
|
|
Future<void> _loginAndGoToNotifications(WidgetTester tester) async {
|
|
await tester.pumpWidget(_IntegrationTestApp());
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.enterText(
|
|
find.byKey(const Key('email_field')), _fakeUserAccount.email);
|
|
await tester.enterText(
|
|
find.byKey(const Key('password_field')), _fakeUserAccount.password);
|
|
await tester.tap(find.byKey(const Key('login_button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.byKey(const Key('go_notifications_button')));
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
testWidgets('3.1 - navigasi ke Notifikasi dari Dashboard berhasil',
|
|
(tester) async {
|
|
await _loginAndGoToNotifications(tester);
|
|
|
|
expect(find.text('Notifikasi'), findsAtLeastNWidgets(1));
|
|
expect(find.byKey(const Key('notif_list')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('3.2 - notifikasi yang ada ditampilkan dalam list',
|
|
(tester) async {
|
|
await _loginAndGoToNotifications(tester);
|
|
|
|
expect(find.byKey(const Key('notif_item_1')), findsOneWidget);
|
|
expect(find.byKey(const Key('notif_item_2')), findsOneWidget);
|
|
expect(find.byKey(const Key('notif_item_3')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('3.3 - notifikasi belum dibaca menampilkan dot biru',
|
|
(tester) async {
|
|
await _loginAndGoToNotifications(tester);
|
|
|
|
expect(find.byKey(const Key('notif_unread_1')), findsOneWidget);
|
|
expect(find.byKey(const Key('notif_unread_2')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('3.4 - notifikasi sudah dibaca tidak menampilkan dot',
|
|
(tester) async {
|
|
await _loginAndGoToNotifications(tester);
|
|
|
|
expect(find.byKey(const Key('notif_unread_3')), findsNothing);
|
|
});
|
|
|
|
testWidgets('3.5 - tombol Tandai Semua tersedia saat ada unread',
|
|
(tester) async {
|
|
await _loginAndGoToNotifications(tester);
|
|
|
|
expect(find.byKey(const Key('mark_all_read_button')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('3.6 - dashboard menampilkan unread badge sebelum mark all',
|
|
(tester) async {
|
|
await tester.pumpWidget(_IntegrationTestApp());
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.enterText(
|
|
find.byKey(const Key('email_field')), _fakeUserAccount.email);
|
|
await tester.enterText(
|
|
find.byKey(const Key('password_field')), _fakeUserAccount.password);
|
|
await tester.tap(find.byKey(const Key('login_button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('dashboard_unread_badge')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('3.7 - tap Tandai Semua menghapus semua dot unread',
|
|
(tester) async {
|
|
await _loginAndGoToNotifications(tester);
|
|
|
|
expect(find.byKey(const Key('notif_unread_1')), findsOneWidget);
|
|
|
|
await tester.tap(find.byKey(const Key('mark_all_read_button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('notif_unread_1')), findsNothing);
|
|
expect(find.byKey(const Key('notif_unread_2')), findsNothing);
|
|
});
|
|
|
|
testWidgets('3.8 - setelah mark all, tombol Tandai Semua hilang',
|
|
(tester) async {
|
|
await _loginAndGoToNotifications(tester);
|
|
|
|
await tester.tap(find.byKey(const Key('mark_all_read_button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('mark_all_read_button')), findsNothing);
|
|
});
|
|
|
|
testWidgets('3.9 - kembali ke dashboard setelah dari notifikasi',
|
|
(tester) async {
|
|
await _loginAndGoToNotifications(tester);
|
|
|
|
await tester.pageBack();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Dashboard'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('3.10 - konten notifikasi ditampilkan dengan benar',
|
|
(tester) async {
|
|
await _loginAndGoToNotifications(tester);
|
|
|
|
expect(find.text('Hati-hati di persimpangan'), findsOneWidget);
|
|
expect(find.text('Cuaca memburuk hari ini'), findsOneWidget);
|
|
expect(find.text('Pesan lama sudah dibaca'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets(
|
|
'3.11 - state perubahan notifikasi persisten saat kembali ke dashboard',
|
|
(tester) async {
|
|
await _loginAndGoToNotifications(tester);
|
|
|
|
// Mark all read di halaman notifikasi
|
|
await tester.tap(find.byKey(const Key('mark_all_read_button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Kembali ke dashboard
|
|
await tester.pageBack();
|
|
await tester.pumpAndSettle();
|
|
|
|
// Badge unread harus hilang karena sudah di-mark all
|
|
expect(find.byKey(const Key('dashboard_unread_badge')), findsNothing);
|
|
});
|
|
|
|
testWidgets('3.12 - full flow: login → notifikasi → mark all → logout',
|
|
(tester) async {
|
|
await tester.pumpWidget(_IntegrationTestApp());
|
|
await tester.pumpAndSettle();
|
|
|
|
// Step 1: Login
|
|
await tester.enterText(
|
|
find.byKey(const Key('email_field')), _fakeUserAccount.email);
|
|
await tester.enterText(
|
|
find.byKey(const Key('password_field')), _fakeUserAccount.password);
|
|
await tester.tap(find.byKey(const Key('login_button')));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Dashboard'), findsOneWidget);
|
|
|
|
// Step 2: Buka notifikasi
|
|
await tester.tap(find.byKey(const Key('go_notifications_button')));
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(const Key('notif_list')), findsOneWidget);
|
|
|
|
// Step 3: Mark all read
|
|
await tester.tap(find.byKey(const Key('mark_all_read_button')));
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(const Key('notif_unread_1')), findsNothing);
|
|
|
|
// Step 4: Kembali ke dashboard
|
|
await tester.pageBack();
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Dashboard'), findsOneWidget);
|
|
|
|
// Step 5: Logout
|
|
await tester.tap(find.byKey(const Key('logout_button')));
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(const Key('login_button')), findsOneWidget);
|
|
});
|
|
});
|
|
}
|