399 lines
15 KiB
Dart
399 lines
15 KiB
Dart
// test/widget/login_screen_test.dart
|
|
//
|
|
// Widget tests untuk LoginScreen.
|
|
// Jalankan: flutter test test/widget/login_screen_test.dart
|
|
//
|
|
// Dev dependencies yang diperlukan (pubspec.yaml):
|
|
// flutter_test: sdk: flutter
|
|
// mockito: ^5.4.4
|
|
// build_runner: ^2.4.6
|
|
// bloc_test: ^9.1.5
|
|
// network_image_mock: ^2.1.1
|
|
|
|
// ignore_for_file: avoid_print
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Minimal stubs — diperlukan supaya test bisa compile tanpa full app context
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Stub MaterialApp wrapper yang tidak memerlukan seluruh DI container.
|
|
Widget makeTestable(Widget child) {
|
|
return MaterialApp(
|
|
home: child,
|
|
);
|
|
}
|
|
|
|
/// Stub sederhana LoginScreen yang mereplikasi struktur UI asli.
|
|
/// Ganti dengan import asli saat DI sudah bisa di-mock seluruhnya:
|
|
/// import 'package:walkguide_app/features/auth/login_screen.dart';
|
|
class _StubLoginScreen extends StatefulWidget {
|
|
const _StubLoginScreen();
|
|
|
|
@override
|
|
State<_StubLoginScreen> createState() => _StubLoginScreenState();
|
|
}
|
|
|
|
class _StubLoginScreenState extends State<_StubLoginScreen> {
|
|
final _emailController = TextEditingController();
|
|
final _passwordController = TextEditingController();
|
|
bool _loading = false;
|
|
bool _showPassword = false;
|
|
String? _errorMessage;
|
|
|
|
void _submit() {
|
|
if (_emailController.text.isEmpty || _passwordController.text.isEmpty) {
|
|
setState(() => _errorMessage = 'Email dan password wajib diisi');
|
|
return;
|
|
}
|
|
if (!_emailController.text.contains('@')) {
|
|
setState(() => _errorMessage = 'Format email tidak valid');
|
|
return;
|
|
}
|
|
setState(() {
|
|
_loading = true;
|
|
_errorMessage = null;
|
|
});
|
|
// Simulate async login
|
|
Future.delayed(const Duration(milliseconds: 100), () {
|
|
if (mounted) setState(() => _loading = false);
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_emailController.dispose();
|
|
_passwordController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('Login')),
|
|
body: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
const Text('WalkGuide', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 8),
|
|
const Text('Masuk ke akun Anda'),
|
|
const SizedBox(height: 24),
|
|
if (_errorMessage != null) ...[
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.red.shade50,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: Colors.red.shade200),
|
|
),
|
|
child: Text(
|
|
_errorMessage!,
|
|
key: const Key('error_message'),
|
|
style: const TextStyle(color: Colors.red),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
],
|
|
TextFormField(
|
|
key: const Key('email_field'),
|
|
controller: _emailController,
|
|
keyboardType: TextInputType.emailAddress,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Email',
|
|
hintText: 'Masukkan email Anda',
|
|
prefixIcon: Icon(Icons.email_outlined),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextFormField(
|
|
key: const Key('password_field'),
|
|
controller: _passwordController,
|
|
obscureText: !_showPassword,
|
|
decoration: InputDecoration(
|
|
labelText: 'Password',
|
|
hintText: 'Masukkan password',
|
|
prefixIcon: const Icon(Icons.lock_outline),
|
|
suffixIcon: IconButton(
|
|
key: const Key('toggle_password'),
|
|
icon: Icon(_showPassword ? Icons.visibility_off : Icons.visibility),
|
|
onPressed: () => setState(() => _showPassword = !_showPassword),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
_loading
|
|
? const Center(child: CircularProgressIndicator(key: Key('loading_indicator')))
|
|
: ElevatedButton(
|
|
key: const Key('login_button'),
|
|
onPressed: _submit,
|
|
child: const Text('Masuk'),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextButton(
|
|
key: const Key('register_link'),
|
|
onPressed: () {},
|
|
child: const Text('Belum punya akun? Daftar'),
|
|
),
|
|
TextButton(
|
|
key: const Key('forgot_password_link'),
|
|
onPressed: () {},
|
|
child: const Text('Lupa password?'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Workaround untuk SizedBox(height(12)) yang error — gunakan helper
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void main() {
|
|
group('LoginScreen Widget Tests', () {
|
|
// ── Rendering ──────────────────────────────────────────────────────────
|
|
|
|
group('Rendering awal', () {
|
|
testWidgets('menampilkan judul WalkGuide', (tester) async {
|
|
await tester.pumpWidget(makeTestable(const _StubLoginScreen()));
|
|
|
|
expect(find.text('WalkGuide'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('menampilkan field email', (tester) async {
|
|
await tester.pumpWidget(makeTestable(const _StubLoginScreen()));
|
|
|
|
expect(find.byKey(const Key('email_field')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('menampilkan field password', (tester) async {
|
|
await tester.pumpWidget(makeTestable(const _StubLoginScreen()));
|
|
|
|
expect(find.byKey(const Key('password_field')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('menampilkan tombol Login', (tester) async {
|
|
await tester.pumpWidget(makeTestable(const _StubLoginScreen()));
|
|
|
|
expect(find.byKey(const Key('login_button')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('menampilkan link ke halaman Register', (tester) async {
|
|
await tester.pumpWidget(makeTestable(const _StubLoginScreen()));
|
|
|
|
expect(find.byKey(const Key('register_link')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('menampilkan link Lupa Password', (tester) async {
|
|
await tester.pumpWidget(makeTestable(const _StubLoginScreen()));
|
|
|
|
expect(find.byKey(const Key('forgot_password_link')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('tidak menampilkan error message saat pertama render', (tester) async {
|
|
await tester.pumpWidget(makeTestable(const _StubLoginScreen()));
|
|
|
|
expect(find.byKey(const Key('error_message')), findsNothing);
|
|
});
|
|
|
|
testWidgets('tidak menampilkan loading indicator saat pertama render', (tester) async {
|
|
await tester.pumpWidget(makeTestable(const _StubLoginScreen()));
|
|
|
|
expect(find.byKey(const Key('loading_indicator')), findsNothing);
|
|
});
|
|
});
|
|
|
|
// ── Validasi input ─────────────────────────────────────────────────────
|
|
|
|
group('Validasi input', () {
|
|
testWidgets('menampilkan error saat submit dengan email kosong', (tester) async {
|
|
await tester.pumpWidget(makeTestable(const _StubLoginScreen()));
|
|
|
|
await tester.tap(find.byKey(const Key('login_button')));
|
|
await tester.pump();
|
|
|
|
expect(find.byKey(const Key('error_message')), findsOneWidget);
|
|
expect(find.text('Email dan password wajib diisi'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('menampilkan error saat email diisi tapi password kosong', (tester) async {
|
|
await tester.pumpWidget(makeTestable(const _StubLoginScreen()));
|
|
|
|
await tester.enterText(find.byKey(const Key('email_field')), 'test@example.com');
|
|
await tester.tap(find.byKey(const Key('login_button')));
|
|
await tester.pump();
|
|
|
|
expect(find.byKey(const Key('error_message')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('menampilkan error format email tidak valid', (tester) async {
|
|
await tester.pumpWidget(makeTestable(const _StubLoginScreen()));
|
|
|
|
await tester.enterText(find.byKey(const Key('email_field')), 'bukan-email');
|
|
await tester.enterText(find.byKey(const Key('password_field')), 'password123');
|
|
await tester.tap(find.byKey(const Key('login_button')));
|
|
await tester.pump();
|
|
|
|
expect(find.text('Format email tidak valid'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('menerima input email valid', (tester) async {
|
|
await tester.pumpWidget(makeTestable(const _StubLoginScreen()));
|
|
|
|
await tester.enterText(find.byKey(const Key('email_field')), 'user@example.com');
|
|
|
|
expect(
|
|
(tester.widget(find.byKey(const Key('email_field'))) as TextFormField).controller?.text,
|
|
'user@example.com',
|
|
);
|
|
});
|
|
});
|
|
|
|
// ── Interaksi toggle password ──────────────────────────────────────────
|
|
|
|
group('Toggle visibility password', () {
|
|
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);
|
|
});
|
|
|
|
testWidgets('tap toggle mengubah obscureText menjadi false', (tester) async {
|
|
await tester.pumpWidget(makeTestable(const _StubLoginScreen()));
|
|
|
|
await tester.tap(find.byKey(const Key('toggle_password')));
|
|
await tester.pump();
|
|
|
|
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 dua kali mengembalikan obscureText ke true', (tester) async {
|
|
await tester.pumpWidget(makeTestable(const _StubLoginScreen()));
|
|
|
|
await tester.tap(find.byKey(const Key('toggle_password')));
|
|
await tester.pump();
|
|
await tester.tap(find.byKey(const Key('toggle_password')));
|
|
await tester.pump();
|
|
|
|
final editableText = tester.widget<EditableText>(
|
|
find.descendant(
|
|
of: find.byKey(const Key('password_field')),
|
|
matching: find.byType(EditableText),
|
|
),
|
|
);
|
|
expect(editableText.obscureText, isTrue);
|
|
});
|
|
});
|
|
|
|
// ── Loading state ─────────────────────────────────────────────────────
|
|
|
|
group('Loading state', () {
|
|
testWidgets('menampilkan loading indicator saat submit valid', (tester) async {
|
|
await tester.pumpWidget(makeTestable(const _StubLoginScreen()));
|
|
|
|
await tester.enterText(find.byKey(const Key('email_field')), 'user@example.com');
|
|
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);
|
|
});
|
|
|
|
testWidgets('menyembunyikan tombol login saat loading', (tester) async {
|
|
await tester.pumpWidget(makeTestable(const _StubLoginScreen()));
|
|
|
|
await tester.enterText(find.byKey(const Key('email_field')), 'user@example.com');
|
|
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);
|
|
});
|
|
|
|
testWidgets('loading selesai setelah async operation', (tester) async {
|
|
await tester.pumpWidget(makeTestable(const _StubLoginScreen()));
|
|
|
|
await tester.enterText(find.byKey(const Key('email_field')), 'user@example.com');
|
|
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('loading_indicator')), findsOneWidget);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('loading_indicator')), findsNothing);
|
|
});
|
|
});
|
|
|
|
// ── Aksesibilitas ──────────────────────────────────────────────────────
|
|
|
|
group('Aksesibilitas & semantics', () {
|
|
testWidgets('semua interactive widget memiliki semantics label', (tester) async {
|
|
await tester.pumpWidget(makeTestable(const _StubLoginScreen()));
|
|
|
|
final loginBtn = find.byKey(const Key('login_button'));
|
|
expect(loginBtn, findsOneWidget);
|
|
|
|
// Pastikan tombol bisa di-tap (tersedia di tree)
|
|
expect(tester.widget<ElevatedButton>(loginBtn).onPressed, isNotNull);
|
|
});
|
|
|
|
testWidgets('field email menggunakan keyboard type email', (tester) async {
|
|
await tester.pumpWidget(makeTestable(const _StubLoginScreen()));
|
|
|
|
final textField = tester.widget<TextField>(
|
|
find.descendant(
|
|
of: find.byKey(const Key('email_field')),
|
|
matching: find.byType(TextField),
|
|
),
|
|
);
|
|
expect(textField.keyboardType, TextInputType.emailAddress);
|
|
});
|
|
});
|
|
|
|
// ── Scroll & overflow ─────────────────────────────────────────────────
|
|
|
|
group('Layout', () {
|
|
testWidgets('tidak overflow pada layar kecil (360x640)', (tester) async {
|
|
tester.view.physicalSize = const Size(360, 640);
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
|
|
await tester.pumpWidget(makeTestable(const _StubLoginScreen()));
|
|
|
|
// Tidak boleh ada RenderFlex overflow exception
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('tidak overflow pada layar besar (1280x800)', (tester) async {
|
|
tester.view.physicalSize = const Size(1280, 800);
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
|
|
await tester.pumpWidget(makeTestable(const _StubLoginScreen()));
|
|
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
});
|
|
});
|
|
}
|