// 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 SizedBox _gap(double h) => SizedBox(height: h); // --------------------------------------------------------------------------- // 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( 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( 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( 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(loginBtn).onPressed, isNotNull); }); testWidgets('field email menggunakan keyboard type email', (tester) async { await tester.pumpWidget(makeTestable(const _StubLoginScreen())); final textField = tester.widget( 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); }); }); }); }