2026-05-17 18:40:03 +07:00

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);
});
});
});
}