> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentsoflearning.com/llms.txt
> Use this file to discover all available pages before exploring further.

# TESTING GUIDE

# Testing Guide

## Overview

Comprehensive testing strategy for the AI-Powered Photo Journaling Flutter app covering unit tests, widget tests, integration tests, and manual testing procedures.

## Testing Philosophy

* **Test early, test often**: Write tests alongside features
* **Pyramid approach**: More unit tests, fewer integration tests
* **Test behavior, not implementation**: Focus on what, not how
* **Maintainable tests**: Keep tests simple and clear

## Test Coverage Goals

* **Unit Tests**: 80%+ coverage for business logic
* **Widget Tests**: 70%+ coverage for UI components
* **Integration Tests**: Critical user flows covered
* **Manual Tests**: All features tested on real devices

## Running Tests

### All Tests

```bash theme={null}
# Run all tests
flutter test

# Run with coverage
flutter test --coverage

# Generate HTML coverage report
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html
```

### Specific Tests

```bash theme={null}
# Run single test file
flutter test test/features/entry/entry_bloc_test.dart

# Run tests matching pattern
flutter test --name="EntryBloc"

# Run in watch mode (auto-rerun on changes)
flutter test --watch
```

### Device Testing

```bash theme={null}
# Run on connected iOS device
flutter test integration_test/app_test.dart

# Run with verbose logging
flutter test --verbose
```

## Unit Testing

### Testing Business Logic

**File**: `test/features/entry/entry_service_test.dart`

```dart theme={null}
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:app/features/entry/data/services/entry_service.dart';

void main() {
  group('EntryService', () {
    late EntryService service;

    setUp(() {
      service = EntryService();
    });

    tearDown(() {
      // Clean up
    });

    test('should create entry with valid data', () async {
      // Arrange
      final entry = JournalEntry(
        title: 'Test Entry',
        content: 'Test content',
      );

      // Act
      final result = await service.createEntry(entry);

      // Assert
      expect(result.id, isNotEmpty);
      expect(result.title, equals('Test Entry'));
    });

    test('should throw ValidationException with empty title', () async {
      // Arrange
      final entry = JournalEntry(
        title: '',
        content: 'Test content',
      );

      // Act & Assert
      expect(
        () => service.createEntry(entry),
        throwsA(isA<ValidationException>()),
      );
    });
  });
}
```

### Testing Utilities

**File**: `test/core/utils/date_utils_test.dart`

```dart theme={null}
import 'package:flutter_test/flutter_test.dart';
import 'package:app/core/utils/date_utils.dart';

void main() {
  group('DateUtils', () {
    test('should format date correctly', () {
      final date = DateTime(2025, 11, 15);
      final formatted = DateUtils.formatDate(date);
      expect(formatted, equals('Nov 15, 2025'));
    });

    test('should calculate days ago', () {
      final pastDate = DateTime.now().subtract(Duration(days: 5));
      final daysAgo = DateUtils.daysAgo(pastDate);
      expect(daysAgo, equals(5));
    });
  });
}
```

## Widget Testing

### Testing UI Components

**File**: `test/shared/widgets/buttons/primary_button_test.dart`

```dart theme={null}
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:app/shared/widgets/buttons/primary_button.dart';

void main() {
  testWidgets('PrimaryButton shows label', (WidgetTester tester) async {
    // Build widget
    await tester.pumpWidget(
      CupertinoApp(
        home: PrimaryButton(
          label: 'Test Button',
          onPressed: () {},
        ),
      ),
    );

    // Verify
    expect(find.text('Test Button'), findsOneWidget);
  });

  testWidgets('PrimaryButton calls onPressed', (WidgetTester tester) async {
    var pressed = false;

    await tester.pumpWidget(
      CupertinoApp(
        home: PrimaryButton(
          label: 'Test',
          onPressed: () => pressed = true,
        ),
      ),
    );

    // Tap button
    await tester.tap(find.text('Test'));
    await tester.pump();

    // Verify callback was called
    expect(pressed, isTrue);
  });

  testWidgets('PrimaryButton is disabled when onPressed is null',
      (WidgetTester tester) async {
    await tester.pumpWidget(
      const CupertinoApp(
        home: PrimaryButton(
          label: 'Test',
          onPressed: null,
        ),
      ),
    );

    // Find button
    final button = tester.widget<CupertinoButton>(
      find.byType(CupertinoButton),
    );

    // Verify disabled
    expect(button.enabled, isFalse);
  });
}
```

### Testing Screens

**File**: `test/features/timeline/presentation/screens/timeline_screen_test.dart`

```dart theme={null}
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:app/features/timeline/presentation/screens/timeline_screen.dart';
import 'package:app/features/timeline/presentation/bloc/timeline_bloc.dart';

void main() {
  group('TimelineScreen', () {
    late TimelineBloc bloc;

    setUp(() {
      bloc = TimelineBloc();
    });

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

    testWidgets('shows loading indicator initially',
        (WidgetTester tester) async {
      await tester.pumpWidget(
        CupertinoApp(
          home: BlocProvider.value(
            value: bloc,
            child: const TimelineScreen(),
          ),
        ),
      );

      expect(find.byType(CupertinoActivityIndicator), findsOneWidget);
    });

    testWidgets('shows empty state when no entries',
        (WidgetTester tester) async {
      // Emit empty state
      bloc.add(const TimelineLoadRequested());
      await tester.pumpWidget(
        CupertinoApp(
          home: BlocProvider.value(
            value: bloc,
            child: const TimelineScreen(),
          ),
        ),
      );
      await tester.pumpAndSettle();

      expect(find.byType(EmptyTimelineWidget), findsOneWidget);
    });
  });
}
```

## BLoC Testing

**File**: `test/features/entry/presentation/bloc/entry_bloc_test.dart`

```dart theme={null}
import 'package:flutter_test/flutter_test.dart';
import 'package:bloc_test/bloc_test.dart';
import 'package:app/features/entry/presentation/bloc/entry_bloc.dart';

void main() {
  group('EntryBloc', () {
    late EntryBloc bloc;

    setUp(() {
      bloc = EntryBloc();
    });

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

    test('initial state is EntryInitial', () {
      expect(bloc.state, equals(EntryInitial()));
    });

    blocTest<EntryBloc, EntryState>(
      'emits [EntryLoading, EntryLoaded] when CreateEntryRequested is added',
      build: () => bloc,
      act: (bloc) => bloc.add(CreateEntryRequested(
        title: 'Test',
        content: 'Test content',
      )),
      expect: () => [
        isA<EntryLoading>(),
        isA<EntryLoaded>(),
      ],
    );

    blocTest<EntryBloc, EntryState>(
      'emits [EntryLoading, EntryError] when creation fails',
      build: () => bloc,
      act: (bloc) => bloc.add(CreateEntryRequested(
        title: '', // Invalid
        content: 'Test',
      )),
      expect: () => [
        isA<EntryLoading>(),
        isA<EntryError>(),
      ],
    );
  });
}
```

## Integration Testing

### Setup

Create `integration_test/app_test.dart`:

```dart theme={null}
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('App Integration Tests', () {
    testWidgets('complete journal entry creation flow',
        (WidgetTester tester) async {
      // Start app
      app.main();
      await tester.pumpAndSettle();

      // Navigate to create entry
      await tester.tap(find.byIcon(CupertinoIcons.add));
      await tester.pumpAndSettle();

      // Capture photo (mock)
      await tester.tap(find.text('Capture Photo'));
      await tester.pumpAndSettle();

      // Wait for AI prompt generation
      await tester.pump(const Duration(seconds: 5));
      await tester.pumpAndSettle();

      // Select prompt
      await tester.tap(find.text('Select this prompt'));
      await tester.pumpAndSettle();

      // Add journal text
      await tester.enterText(
        find.byType(CupertinoTextField),
        'Test journal entry content',
      );

      // Save entry
      await tester.tap(find.text('Save'));
      await tester.pumpAndSettle();

      // Verify entry appears in timeline
      expect(find.text('Test journal entry content'), findsOneWidget);
    });
  });
}
```

### Running Integration Tests

```bash theme={null}
# Run integration tests
flutter test integration_test/app_test.dart

# Run on device
flutter test integration_test/app_test.dart --device-id=<device-id>
```

## Manual Testing Checklist

### Pre-Release Testing

#### Functional Testing

* [ ] **Entry Creation**
  * [ ] Capture photo from camera
  * [ ] Select photo from library
  * [ ] AI prompt generation works
  * [ ] Can select and edit prompt
  * [ ] Can add custom emotions
  * [ ] Entry saves successfully

* [ ] **Timeline**
  * [ ] Entries display correctly
  * [ ] Infinite scroll works
  * [ ] Pull-to-refresh works
  * [ ] Tap to view detail works
  * [ ] Photos load correctly

* [ ] **Search**
  * [ ] Text search works
  * [ ] Emotion filter works
  * [ ] Date range filter works
  * [ ] Results display correctly

* [ ] **Calendar**
  * [ ] Calendar displays correctly
  * [ ] Can navigate months
  * [ ] Entry indicators show
  * [ ] Tap date to filter works

* [ ] **Settings**
  * [ ] Profile updates save
  * [ ] Notification settings work
  * [ ] Privacy settings work
  * [ ] Data export works

#### Accessibility Testing

* [ ] **VoiceOver**
  * [ ] All buttons have labels
  * [ ] Images have alt text
  * [ ] Navigation works with VoiceOver
  * [ ] Forms are accessible

* [ ] **Dynamic Type**
  * [ ] Text scales correctly
  * [ ] Layouts don't break with large text
  * [ ] All text is readable

* [ ] **Color Contrast**
  * [ ] Meets WCAG AA standards
  * [ ] Works in dark mode
  * [ ] No color-only indicators

#### Performance Testing

* [ ] **App Launch**
  * [ ] Launches in \< 2 seconds
  * [ ] No visible lag

* [ ] **Scrolling**
  * [ ] Timeline scrolls at 60 FPS
  * [ ] No dropped frames
  * [ ] Smooth animations

* [ ] **Memory**
  * [ ] No memory leaks
  * [ ] Memory usage stable
  * [ ] No crashes after extended use

* [ ] **Network**
  * [ ] Works on WiFi
  * [ ] Works on cellular
  * [ ] Handles offline gracefully

#### Device Testing

Test on multiple devices:

* [ ] iPhone 12 (iOS 15)
* [ ] iPhone 14 Pro (iOS 16)
* [ ] iPhone 15 Pro Max (iOS 17)
* [ ] iPad (latest iOS)

#### Edge Cases

* [ ] **Empty States**
  * [ ] No entries yet
  * [ ] No search results
  * [ ] No notifications

* [ ] **Error States**
  * [ ] Network error
  * [ ] API error
  * [ ] Invalid input
  * [ ] Permission denied

* [ ] **Boundary Conditions**
  * [ ] Very long journal entry (10,000 characters)
  * [ ] Many entries (1,000+)
  * [ ] Large photo (25MB)
  * [ ] Very old date
  * [ ] Future date

## Mocking

### Mock Services

Create mocks for external dependencies:

```dart theme={null}
// test/mocks/mock_entry_service.dart
import 'package:mockito/annotations.dart';
import 'package:app/features/entry/data/services/entry_service.dart';

@GenerateMocks([EntryService])
void main() {}
```

Generate mocks:

```bash theme={null}
flutter pub run build_runner build
```

Use in tests:

```dart theme={null}
import 'package:mockito/mockito.dart';
import 'mocks/mock_entry_service.mocks.dart';

void main() {
  test('should use mocked service', () {
    final mockService = MockEntryService();
    when(mockService.getEntries()).thenAnswer(
      (_) async => [/* mock data */],
    );

    // Use mockService in test
  });
}
```

## Golden Tests

Test visual appearance:

```dart theme={null}
testWidgets('EntryCard matches golden', (WidgetTester tester) async {
  await tester.pumpWidget(
    CupertinoApp(
      home: EntryCard(entry: mockEntry),
    ),
  );

  await expectLater(
    find.byType(EntryCard),
    matchesGoldenFile('goldens/entry_card.png'),
  );
});
```

Update goldens:

```bash theme={null}
flutter test --update-goldens
```

## CI/CD Integration

**GitHub Actions** (`.github/workflows/test.yml`):

```yaml theme={null}
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v3

      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.16.0'

      - name: Install dependencies
        run: flutter pub get

      - name: Run analyzer
        run: flutter analyze

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

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

## Best Practices

1. **Test Naming**: Use descriptive names
   * ✅ `should create entry with valid data`
   * ❌ `test1`

2. **Arrange-Act-Assert**: Structure tests clearly
   ```dart theme={null}
   // Arrange: Setup
   final input = 'test';

   // Act: Execute
   final result = function(input);

   // Assert: Verify
   expect(result, equals('expected'));
   ```

3. **One Assertion Per Test**: Keep tests focused
   * Each test should verify one behavior
   * Use multiple tests for multiple behaviors

4. **Independent Tests**: Tests should not depend on each other
   * Each test should setup its own state
   * Use `setUp()` and `tearDown()`

5. **Readable Tests**: Write tests that document behavior
   * Test names explain what is being tested
   * Code is clean and easy to understand

## Troubleshooting

### Common Issues

**Issue**: Tests fail on CI but pass locally

* **Solution**: Ensure consistent Flutter version, check timezone dependencies

**Issue**: Widget tests fail with "No Material/Cupertino widget found"

* **Solution**: Wrap widget in `CupertinoApp` or `MaterialApp`

**Issue**: Async tests timeout

* **Solution**: Increase timeout, check for infinite loops

**Issue**: Flaky tests (sometimes pass, sometimes fail)

* **Solution**: Add proper waits with `pumpAndSettle()`, check for race conditions

***

**Last Updated**: 2025-11-15
**Maintained By**: QA Engineer / Frontend Developer
