**Prepared by:** Oditha

**Date:** 23 January 2026

**Version:** 1.0

**Reference:** [FRD - HoReCa Field Operations & Workflow Management](https://www.notion.so/FRD-HoReCa-Field-Operations-Workflow-Management-2f7cd2773bc548c9a2595d856f15ac15?pvs=21)

---

## Overview

This document provides comprehensive unit testing guidelines and test cases for the **Mobile Application** (Flutter).

---

## Testing Framework

### Packages

-   **flutter_test:** Built-in testing framework
-   **mockito:** Mocking dependencies
-   **bloc_test:** Testing BLoC/Cubit state management
-   **integration_test:** Integration and E2E tests

### Setup

**pubspec.yaml:**

```yaml
dev_dependencies:
    flutter_test:
        sdk: flutter
    mockito: ^5.4.0
    build_runner: ^2.4.0
    bloc_test: ^9.1.0
    integration_test:
        sdk: flutter
```

---

## Test Structure

```
test/
├── unit/
│   ├── models/
│   │   ├── user_model_test.dart
│   │   ├── outlet_model_test.dart
│   │   └── workflow_model_test.dart
│   ├── repositories/
│   │   ├── auth_repository_test.dart
│   │   ├── outlet_repository_test.dart
│   │   └── sync_repository_test.dart
│   ├── usecases/
│   │   ├── login_usecase_test.dart
│   │   ├── check_in_usecase_test.dart
│   │   └── execute_workflow_usecase_test.dart
│   ├── blocs/
│   │   ├── auth_bloc_test.dart
│   │   ├── outlet_bloc_test.dart
│   │   └── workflow_bloc_test.dart
│   └── services/
│       ├── location_service_test.dart
│       └── photo_service_test.dart
├── widget/
│   ├── pages/
│   │   ├── login_page_test.dart
│   │   └── workflow_execution_page_test.dart
│   └── widgets/
│       ├── outlet_card_test.dart
│       └── workflow_step_widget_test.dart
├── integration/
│   ├── login_flow_test.dart
│   ├── workflow_execution_flow_test.dart
│   └── offline_sync_test.dart
└── fixtures/
    ├── user.json
    ├── outlets.json
    └── workflow.json
```

---

## Unit Tests

### 1. Model Tests

#### Test: User Model Serialization

**File:** `test/unit/models/user_model_test.dart`

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:horeca_app/data/models/user_model.dart';
import 'dart:convert';

void main() {
  group('UserModel', () {
    final tUserJson = {
      'id': 1,
      'name': 'John Doe',
      'email': 'john@example.com',
      'role': 'lsr',
      'territory_id': 5,
      'region_id': 2,
    };

    test('should deserialize from JSON correctly', () {
      // Arrange
      final jsonString = json.encode(tUserJson);

      // Act
      final result = UserModel.fromJson(json.decode(jsonString));

      // Assert
      expect(result.id, 1);
      expect(result.name, 'John Doe');
      expect(result.email, 'john@example.com');
      expect(result.role, 'lsr');
      expect(result.territoryId, 5);
      expect(result.regionId, 2);
    });

    test('should serialize to JSON correctly', () {
      // Arrange
      final userModel = UserModel(
        id: 1,
        name: 'John Doe',
        email: 'john@example.com',
        role: 'lsr',
        territoryId: 5,
        regionId: 2,
      );

      // Act
      final result = userModel.toJson();

      // Assert
      expect(result, tUserJson);
    });
  });
}
```

---

### 2. Repository Tests

#### Test: Auth Repository

**File:** `test/unit/repositories/auth_repository_test.dart`

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'package:horeca_app/data/repositories/auth_repository.dart';
import 'package:horeca_app/data/datasources/remote/auth_remote_datasource.dart';
import 'package:horeca_app/data/models/user_model.dart';
import 'auth_repository_test.mocks.dart';

@GenerateMocks([AuthRemoteDataSource])
void main() {
  late AuthRepository repository;
  late MockAuthRemoteDataSource mockRemoteDataSource;

  setUp(() {
    mockRemoteDataSource = MockAuthRemoteDataSource();
    repository = AuthRepository(mockRemoteDataSource);
  });

  group('login', () {
    final tEmail = 'test@example.com';
    final tPassword = 'password123';
    final tDeviceName = 'Test Device';
    final tToken = 'test_token_12345';
    final tUser = UserModel(
      id: 1,
      name: 'Test User',
      email: tEmail,
      role: 'lsr',
    );

    test('should return token and user when login is successful', () async {
      // Arrange
      when(mockRemoteDataSource.login(
        email: anyNamed('email'),
        password: anyNamed('password'),
        deviceName: anyNamed('deviceName'),
      )).thenAnswer((_) async => {
            'token': tToken,
            'user': tUser,
          });

      // Act
      final result = await repository.login(
        email: tEmail,
        password: tPassword,
        deviceName: tDeviceName,
      );

      // Assert
      expect(result['token'], tToken);
      expect(result['user'], tUser);
      verify(mockRemoteDataSource.login(
        email: tEmail,
        password: tPassword,
        deviceName: tDeviceName,
      ));
      verifyNoMoreInteractions(mockRemoteDataSource);
    });

    test('should throw exception when login fails', () async {
      // Arrange
      when(mockRemoteDataSource.login(
        email: anyNamed('email'),
        password: anyNamed('password'),
        deviceName: anyNamed('deviceName'),
      )).thenThrow(Exception('Invalid credentials'));

      // Act
      final call = repository.login;

      // Assert
      expect(
        () => call(
          email: tEmail,
          password: tPassword,
          deviceName: tDeviceName,
        ),
        throwsException,
      );
    });
  });
}
```

---

### 3. Use Case Tests

#### Test: Check-in Use Case

**File:** `test/unit/usecases/check_in_usecase_test.dart`

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'package:horeca_app/domain/usecases/outlet/check_in_usecase.dart';
import 'package:horeca_app/data/repositories/outlet_repository.dart';
import 'package:horeca_app/core/services/location_service.dart';
import 'check_in_usecase_test.mocks.dart';

@GenerateMocks([OutletRepository, LocationService])
void main() {
  late CheckInUseCase useCase;
  late MockOutletRepository mockRepository;
  late MockLocationService mockLocationService;

  setUp(() {
    mockRepository = MockOutletRepository();
    mockLocationService = MockLocationService();
    useCase = CheckInUseCase(mockRepository, mockLocationService);
  });

  group('execute', () {
    final tOutletId = 101;
    final tUserLat = 6.927079;
    final tUserLng = 79.861244;
    final tOutletLat = 6.927100;
    final tOutletLng = 79.861250;
    final tVisitId = 789;

    test('should successfully check-in when within geofence', () async {
      // Arrange
      when(mockLocationService.calculateDistance(
        tUserLat,
        tUserLng,
        tOutletLat,
        tOutletLng,
      )).thenReturn(25.0); // 25 meters

      when(mockRepository.checkIn(
        outletId: anyNamed('outletId'),
        latitude: anyNamed('latitude'),
        longitude: anyNamed('longitude'),
      )).thenAnswer((_) async => {
            'visit_id': tVisitId,
            'within_geofence': true,
          });

      // Act
      final result = await useCase.execute(
        outletId: tOutletId,
        userLatitude: tUserLat,
        userLongitude: tUserLng,
        outletLatitude: tOutletLat,
        outletLongitude: tOutletLng,
      );

      // Assert
      expect(result['visit_id'], tVisitId);
      expect(result['within_geofence'], true);
      verify(mockLocationService.calculateDistance(
        tUserLat,
        tUserLng,
        tOutletLat,
        tOutletLng,
      ));
      verify(mockRepository.checkIn(
        outletId: tOutletId,
        latitude: tUserLat,
        longitude: tUserLng,
      ));
    });

    test('should require remote check-in reason when outside geofence',
        () async {
      // Arrange
      when(mockLocationService.calculateDistance(
        tUserLat,
        tUserLng,
        tOutletLat,
        tOutletLng,
      )).thenReturn(150.0); // 150 meters

      // Act & Assert
      expect(
        () => useCase.execute(
          outletId: tOutletId,
          userLatitude: tUserLat,
          userLongitude: tUserLng,
          outletLatitude: tOutletLat,
          outletLongitude: tOutletLng,
          remoteCheckInReason: null, // Missing reason
        ),
        throwsA(isA<ValidationException>()),
      );
    });
  });
}
```

---

### 4. BLoC Tests

#### Test: Workflow BLoC

**File:** `test/unit/blocs/workflow_bloc_test.dart`

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:bloc_test/bloc_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'package:horeca_app/presentation/blocs/workflow/workflow_bloc.dart';
import 'package:horeca_app/domain/usecases/workflow/execute_workflow_usecase.dart';
import 'workflow_bloc_test.mocks.dart';

@GenerateMocks([ExecuteWorkflowUseCase])
void main() {
  late WorkflowBloc bloc;
  late MockExecuteWorkflowUseCase mockExecuteWorkflowUseCase;

  setUp(() {
    mockExecuteWorkflowUseCase = MockExecuteWorkflowUseCase();
    bloc = WorkflowBloc(mockExecuteWorkflowUseCase);
  });

  tearDown(() {
    bloc.close();
  });

  group('LoadWorkflowEvent', () {
    final tWorkflowId = 1;
    final tOutletId = 101;
    final tWorkflow = Workflow(
      id: tWorkflowId,
      name: 'Impulse Workflow',
      steps: [],
    );

    blocTest<WorkflowBloc, WorkflowState>(
      'should emit [Loading, Loaded] when workflow loads successfully',
      build: () {
        when(mockExecuteWorkflowUseCase.loadWorkflow(
          workflowId: anyNamed('workflowId'),
          outletId: anyNamed('outletId'),
        )).thenAnswer((_) async => tWorkflow);
        return bloc;
      },
      act: (bloc) => bloc.add(LoadWorkflowEvent(
        workflowId: tWorkflowId,
        outletId: tOutletId,
      )),
      expect: () => [
        WorkflowLoadingState(),
        WorkflowLoadedState(workflow: tWorkflow),
      ],
      verify: (_) {
        verify(mockExecuteWorkflowUseCase.loadWorkflow(
          workflowId: tWorkflowId,
          outletId: tOutletId,
        ));
      },
    );

    blocTest<WorkflowBloc, WorkflowState>(
      'should emit [Loading, Error] when workflow load fails',
      build: () {
        when(mockExecuteWorkflowUseCase.loadWorkflow(
          workflowId: anyNamed('workflowId'),
          outletId: anyNamed('outletId'),
        )).thenThrow(Exception('Failed to load workflow'));
        return bloc;
      },
      act: (bloc) => bloc.add(LoadWorkflowEvent(
        workflowId: tWorkflowId,
        outletId: tOutletId,
      )),
      expect: () => [
        WorkflowLoadingState(),
        WorkflowErrorState(message: 'Failed to load workflow'),
      ],
    );
  });
}
```

---

### 5. Service Tests

#### Test: Location Service

**File:** `test/unit/services/location_service_test.dart`

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:horeca_app/core/services/location_service.dart';

void main() {
  late LocationService locationService;

  setUp(() {
    locationService = LocationService();
  });

  group('calculateDistance', () {
    test('should calculate distance between two points correctly', () {
      // Arrange
      final lat1 = 6.927079;
      final lon1 = 79.861244;
      final lat2 = 6.927100;
      final lon2 = 79.861250;

      // Act
      final distance = locationService.calculateDistance(
        lat1,
        lon1,
        lat2,
        lon2,
      );

      // Assert
      expect(distance, greaterThan(0));
      expect(distance, lessThan(50)); // Should be less than 50 meters
    });

    test('should return 0 when both points are the same', () {
      // Arrange
      final lat = 6.927079;
      final lon = 79.861244;

      // Act
      final distance = locationService.calculateDistance(lat, lon, lat, lon);

      // Assert
      expect(distance, 0);
    });
  });

  group('isWithinGeofence', () {
    test('should return true when within geofence radius', () {
      // Arrange
      final userLat = 6.927079;
      final userLon = 79.861244;
      final outletLat = 6.927100;
      final outletLon = 79.861250;
      final radiusMeters = 100.0;

      // Act
      final result = locationService.isWithinGeofence(
        userLat,
        userLon,
        outletLat,
        outletLon,
        radiusMeters,
      );

      // Assert
      expect(result, true);
    });

    test('should return false when outside geofence radius', () {
      // Arrange
      final userLat = 6.927079;
      final userLon = 79.861244;
      final outletLat = 6.930000; // About 350 meters away
      final outletLon = 79.865000;
      final radiusMeters = 100.0;

      // Act
      final result = locationService.isWithinGeofence(
        userLat,
        userLon,
        outletLat,
        outletLon,
        radiusMeters,
      );

      // Assert
      expect(result, false);
    });
  });
}
```

---

## Widget Tests

### Test: Login Page

**File:** `test/widget/pages/login_page_test.dart`

```dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:mockito/mockito.dart';
import 'package:horeca_app/presentation/pages/auth/login_page.dart';
import 'package:horeca_app/presentation/blocs/auth/auth_bloc.dart';

class MockAuthBloc extends Mock implements AuthBloc {}

void main() {
  late MockAuthBloc mockAuthBloc;

  setUp(() {
    mockAuthBloc = MockAuthBloc();
  });

  Widget makeTestableWidget(Widget child) {
    return MaterialApp(
      home: BlocProvider<AuthBloc>.value(
        value: mockAuthBloc,
        child: child,
      ),
    );
  }

  testWidgets('should show email and password fields', (tester) async {
    // Arrange
    when(mockAuthBloc.state).thenReturn(AuthInitialState());
    when(mockAuthBloc.stream)
        .thenAnswer((_) => Stream.value(AuthInitialState()));

    // Act
    await tester.pumpWidget(makeTestableWidget(LoginPage()));

    // Assert
    expect(find.byKey(Key('email_field')), findsOneWidget);
    expect(find.byKey(Key('password_field')), findsOneWidget);
    expect(find.byKey(Key('login_button')), findsOneWidget);
  });

  testWidgets('should trigger login event when button pressed',
      (tester) async {
    // Arrange
    when(mockAuthBloc.state).thenReturn(AuthInitialState());
    when(mockAuthBloc.stream)
        .thenAnswer((_) => Stream.value(AuthInitialState()));

    await tester.pumpWidget(makeTestableWidget(LoginPage()));

    // Act
    await tester.enterText(find.byKey(Key('email_field')), 'test@example.com');
    await tester.enterText(find.byKey(Key('password_field')), 'password123');
    await tester.tap(find.byKey(Key('login_button')));
    await tester.pump();

    // Assert
    verify(mockAuthBloc.add(any)).called(1);
  });

  testWidgets('should show loading indicator when logging in', (tester) async {
    // Arrange
    when(mockAuthBloc.state).thenReturn(AuthLoadingState());
    when(mockAuthBloc.stream)
        .thenAnswer((_) => Stream.value(AuthLoadingState()));

    // Act
    await tester.pumpWidget(makeTestableWidget(LoginPage()));
    await tester.pump();

    // Assert
    expect(find.byType(CircularProgressIndicator), findsOneWidget);
  });

  testWidgets('should show error message when login fails', (tester) async {
    // Arrange
    final errorMessage = 'Invalid credentials';
    when(mockAuthBloc.state).thenReturn(AuthErrorState(message: errorMessage));
    when(mockAuthBloc.stream)
        .thenAnswer((_) => Stream.value(AuthErrorState(message: errorMessage)));

    // Act
    await tester.pumpWidget(makeTestableWidget(LoginPage()));
    await tester.pump();

    // Assert
    expect(find.text(errorMessage), findsOneWidget);
  });
}
```

---

## Integration Tests

### Test: Complete Workflow Execution Flow

**File:** `integration_test/workflow_execution_flow_test.dart`

```dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:horeca_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('Workflow Execution Flow', () {
    testWidgets('should complete full workflow execution', (tester) async {
      // Start app
      app.main();
      await tester.pumpAndSettle();

      // Login
      await tester.enterText(
        find.byKey(Key('email_field')),
        'test@example.com',
      );
      await tester.enterText(
        find.byKey(Key('password_field')),
        'password123',
      );
      await tester.tap(find.byKey(Key('login_button')));
      await tester.pumpAndSettle(Duration(seconds: 3));

      // Verify home page loaded
      expect(find.text('Outlets'), findsOneWidget);

      // Navigate to outlets
      await tester.tap(find.text('Outlets'));
      await tester.pumpAndSettle();

      // Select first outlet
      await tester.tap(find.byType(OutletCard).first);
      await tester.pumpAndSettle();

      // Check-in
      await tester.tap(find.text('Check In'));
      await tester.pumpAndSettle(Duration(seconds: 2));

      // Verify checked in
      expect(find.text('Checked In'), findsOneWidget);

      // Start workflow
      await tester.tap(find.text('Start Impulse Workflow'));
      await tester.pumpAndSettle();

      // Answer first step
      await tester.tap(find.text('Yes'));
      await tester.tap(find.text('Next'));
      await tester.pumpAndSettle();

      // Capture photo
      await tester.tap(find.byIcon(Icons.camera));
      await tester.pumpAndSettle(Duration(seconds: 2));

      // Continue to end
      for (int i = 0; i < 5; i++) {
        // Answer remaining steps
        if (find.text('Next').evaluate().isNotEmpty) {
          await tester.tap(find.text('Next'));
          await tester.pumpAndSettle();
        }
      }

      // Submit workflow
      await tester.tap(find.text('Submit'));
      await tester.pumpAndSettle(Duration(seconds: 3));

      // Verify completion
      expect(find.text('Workflow Completed'), findsOneWidget);
    });
  });
}
```

---

## Test Execution

### Run All Tests

```bash
flutter test
```

### Run Specific Test File

```bash
flutter test test/unit/services/location_service_test.dart
```

### Run with Coverage

```bash
flutter test --coverage
```

### View Coverage Report

```bash
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html
```

### Run Integration Tests

```bash
flutter test integration_test/
```

---

## Test Coverage Goals

### Minimum Coverage Requirements

-   **Overall:** 80%
-   **Business Logic (Use Cases):** 95%
-   **Repositories:** 90%
-   **BLoCs:** 85%
-   **Services:** 90%
-   **Models:** 100%
-   **Widgets:** 70%

---

## Continuous Integration

### GitHub Actions Workflow

**File:** `.github/workflows/test.yml`

```yaml
name: Flutter Tests

on:
    push:
        branches: [main, develop]
    pull_request:
        branches: [main, develop]

jobs:
    test:
        runs-on: ubuntu-latest

        steps:
            - uses: actions/checkout@v3

            - name: Setup Flutter
              uses: subosito/flutter-action@v2
              with:
                  flutter-version: "3.16.0"

            - name: Install dependencies
              run: flutter pub get

            - name: Run tests
              run: flutter test --coverage

            - name: Upload coverage to Codecov
              uses: codecov/codecov-action@v3
              with:
                  file: coverage/lcov.info
```

---

**Document Status:** Final

**Next Steps:** Share with mobile QA team for test execution
