# SkillPatch skill: flutter-testing-skill

This skill enables agents to generate Flutter widget tests, integration tests, unit tests, and golden tests in Dart. It provides a structured decision tree for selecting the appropriate test type and includes ready-to-use code patterns for WidgetTester, finder strategies, and user interaction actions. It also references support for TestMu AI cloud for real device testing.

You (the agent) were given this URL and asked to use this skill. This is a **public** skill — no SkillPatch account, API key, or prior setup is required. Two ways to use it:

**1. Use it right now** — the skill's complete file tree (SKILL.md + all reference files) is inlined below; read `SKILL.md` first, then follow it, consulting the other files as it directs.

**2. Install the exact package onto disk** (recommended if you can run a shell — this reproduces the skill byte-for-byte, including any binary assets that can't be inlined):

```bash
mkdir -p .claude/skills/flutter-testing-skill
curl -sSL https://skillpatch.dev/install_skill/flutter-testing-skill | tar -xz -C .claude/skills/
```

(`.claude/skills/` is Claude Code's convention; use whatever directory your agent loads skills from.)


---

## Skill files (3)

- `SKILL.md`
- `reference/advanced-patterns.md`
- `reference/playbook.md`


### `SKILL.md`

````markdown
---
name: flutter-testing-skill
description: >
  Generates Flutter widget tests, integration tests, and golden tests in Dart.
  Supports local execution and TestMu AI cloud for real device testing.
  Use when user mentions "Flutter", "widget test", "WidgetTester", "testWidgets",
  "flutter_test", "integration_test". Triggers on: "Flutter", "widget test",
  "Dart test", "testWidgets", "WidgetTester", "golden test".
languages:
  - Dart
category: mobile-testing
license: MIT
metadata:
  author: TestMu AI
  version: "1.0"
---

# Flutter Testing Skill

You are a senior Flutter developer specializing in testing.

## Step 1 — Test Type

```
├─ "unit test", "business logic", "model test"
│  └─ Unit test: test/ directory, flutter_test package
│
├─ "widget test", "component test", "UI test"
│  └─ Widget test: test/ directory, testWidgets()
│
├─ "integration test", "E2E", "full app test"
│  └─ Integration test: integration_test/ directory
│
├─ "golden test", "snapshot", "visual regression"
│  └─ Golden test: matchesGoldenFile()
│
└─ Ambiguous? → Widget test (most common)
```

## Core Patterns — Dart

### Widget Test (Most Common)

```dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/screens/login_screen.dart';

void main() {
  testWidgets('Login screen shows email and password fields', (WidgetTester tester) async {
    await tester.pumpWidget(const MaterialApp(home: LoginScreen()));

    // Verify fields exist
    expect(find.byType(TextField), findsNWidgets(2));
    expect(find.text('Email'), findsOneWidget);
    expect(find.text('Password'), findsOneWidget);
    expect(find.byType(ElevatedButton), findsOneWidget);
  });

  testWidgets('Login with valid credentials navigates to dashboard', (WidgetTester tester) async {
    await tester.pumpWidget(const MaterialApp(home: LoginScreen()));

    // Enter credentials
    await tester.enterText(find.byKey(const Key('emailField')), 'user@test.com');
    await tester.enterText(find.byKey(const Key('passwordField')), 'password123');

    // Tap login button
    await tester.tap(find.byKey(const Key('loginButton')));
    await tester.pumpAndSettle(); // Wait for animations and navigation

    // Verify navigation
    expect(find.text('Dashboard'), findsOneWidget);
  });

  testWidgets('Shows error for invalid credentials', (WidgetTester tester) async {
    await tester.pumpWidget(const MaterialApp(home: LoginScreen()));

    await tester.enterText(find.byKey(const Key('emailField')), 'wrong@test.com');
    await tester.enterText(find.byKey(const Key('passwordField')), 'wrong');
    await tester.tap(find.byKey(const Key('loginButton')));
    await tester.pumpAndSettle();

    expect(find.text('Invalid credentials'), findsOneWidget);
  });
}
```

### Finder Strategies

```dart
// By Key (best — explicit test identifiers)
find.byKey(const Key('loginButton'))
find.byKey(const ValueKey('email_input'))

// By Type
find.byType(ElevatedButton)
find.byType(TextField)
find.byType(LoginScreen)

// By Text
find.text('Login')
find.textContaining('Welcome')

// By Icon
find.byIcon(Icons.login)

// By Widget predicate
find.byWidgetPredicate((widget) => widget is Text && widget.data!.startsWith('Error'))

// Descendant/Ancestor
find.descendant(of: find.byType(AppBar), matching: find.text('Title'))
find.ancestor(of: find.text('Login'), matching: find.byType(Card))
```

### Actions

```dart
await tester.tap(finder);                    // Tap
await tester.longPress(finder);              // Long press
await tester.enterText(finder, 'text');      // Type text
await tester.drag(finder, const Offset(0, -300));  // Drag/scroll
await tester.fling(finder, const Offset(0, -500), 1000); // Fling/swipe

// CRITICAL: Always pump after actions
await tester.pump();                         // Single frame
await tester.pump(const Duration(seconds: 1)); // Advance time
await tester.pumpAndSettle();                // Wait for animations to finish
```

### Integration Test

```dart
// integration_test/app_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('Full login flow', (WidgetTester tester) async {
    app.main();
    await tester.pumpAndSettle();

    // Login
    await tester.enterText(find.byKey(const Key('emailField')), 'user@test.com');
    await tester.enterText(find.byKey(const Key('passwordField')), 'password123');
    await tester.tap(find.byKey(const Key('loginButton')));
    await tester.pumpAndSettle();

    // Verify dashboard
    expect(find.text('Dashboard'), findsOneWidget);

    // Navigate to settings
    await tester.tap(find.byIcon(Icons.settings));
    await tester.pumpAndSettle();
    expect(find.text('Settings'), findsOneWidget);
  });
}
```

### Golden Tests (Visual Regression)

```dart
testWidgets('Login screen matches golden', (WidgetTester tester) async {
  await tester.pumpWidget(const MaterialApp(home: LoginScreen()));
  await tester.pumpAndSettle();

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

```bash
# Generate golden files
flutter test --update-goldens

# Run golden comparison
flutter test
```

### Mocking Dependencies

```dart
// Using Mockito
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';

@GenerateMocks([AuthService])
void main() {
  late MockAuthService mockAuth;

  setUp(() {
    mockAuth = MockAuthService();
  });

  testWidgets('Login calls auth service', (tester) async {
    when(mockAuth.login(any, any)).thenAnswer((_) async => true);

    await tester.pumpWidget(MaterialApp(
      home: LoginScreen(authService: mockAuth),
    ));

    await tester.enterText(find.byKey(const Key('emailField')), 'user@test.com');
    await tester.enterText(find.byKey(const Key('passwordField')), 'pass123');
    await tester.tap(find.byKey(const Key('loginButton')));
    await tester.pumpAndSettle();

    verify(mockAuth.login('user@test.com', 'pass123')).called(1);
  });
}
```

### Anti-Patterns

| Bad | Good | Why |
|-----|------|-----|
| No `pumpAndSettle()` after action | Always pump after interactions | Animations not complete |
| `find.text()` for dynamic text | `find.byKey()` | Locale/text changes break tests |
| Testing implementation details | Test user-facing behavior | Brittle |
| No mocking in widget tests | Mock services, repos | Tests hit real APIs |

### TestMu AI Cloud (Integration Tests)

```bash
# Run integration tests on LambdaTest real devices
# 1. Build app for testing
flutter build apk --debug  # Android
flutter build ios --simulator  # iOS

# 2. Upload to LambdaTest
curl -u "$LT_USERNAME:$LT_ACCESS_KEY" \
  -X POST "https://manual-api.lambdatest.com/app/upload/realDevice" \
  -F "appFile=@build/app/outputs/flutter-apk/app-debug.apk"

# 3. Run via Appium (Flutter driver)
# Use appium-flutter-driver for element interaction
```

## Quick Reference

| Task | Command |
|------|---------|
| Run all tests | `flutter test` |
| Run specific file | `flutter test test/login_test.dart` |
| Run with coverage | `flutter test --coverage` |
| Run integration tests | `flutter test integration_test/` |
| Update goldens | `flutter test --update-goldens` |
| Generate mocks | `flutter pub run build_runner build` |
| Test specific platform | `flutter test --platform chrome` |

## pubspec.yaml

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

## Deep Patterns

For advanced patterns, debugging guides, CI/CD integration, and best practices,
see `reference/playbook.md`.

````


### `reference/advanced-patterns.md`

````markdown
# Flutter Testing — Advanced Patterns & Playbook

## Widget Testing with Mocks

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';

@GenerateMocks([UserRepository, AuthService])
void main() {
  late MockUserRepository mockRepo;
  late MockAuthService mockAuth;

  setUp(() {
    mockRepo = MockUserRepository();
    mockAuth = MockAuthService();
  });

  testWidgets('displays user list', (WidgetTester tester) async {
    when(mockRepo.getUsers()).thenAnswer((_) async => [
      User(id: 1, name: 'Alice'),
      User(id: 2, name: 'Bob'),
    ]);

    await tester.pumpWidget(MaterialApp(
      home: ProviderScope(
        overrides: [userRepoProvider.overrideWithValue(mockRepo)],
        child: const UserListScreen(),
      ),
    ));

    await tester.pumpAndSettle();

    expect(find.text('Alice'), findsOneWidget);
    expect(find.text('Bob'), findsOneWidget);
    expect(find.byType(ListTile), findsNWidgets(2));
  });

  testWidgets('handles error state', (tester) async {
    when(mockRepo.getUsers()).thenThrow(Exception('Network error'));
    await tester.pumpWidget(/* ... */);
    await tester.pumpAndSettle();
    expect(find.text('Something went wrong'), findsOneWidget);
    expect(find.byIcon(Icons.refresh), findsOneWidget);
  });
}
```

## Golden Tests (Visual Regression)

```dart
testWidgets('matches golden file', (tester) async {
  await tester.pumpWidget(MaterialApp(
    home: Scaffold(body: ProductCard(product: testProduct)),
  ));
  await expectLater(
    find.byType(ProductCard),
    matchesGoldenFile('goldens/product_card.png'),
  );
});

// Update goldens: flutter test --update-goldens
```

## Integration Testing

```dart
// integration_test/app_test.dart
import 'package:integration_test/integration_test.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('full login flow', (tester) async {
    app.main();
    await tester.pumpAndSettle();

    await tester.enterText(find.byKey(Key('email')), 'user@test.com');
    await tester.enterText(find.byKey(Key('password')), 'password');
    await tester.tap(find.byKey(Key('login-btn')));
    await tester.pumpAndSettle(const Duration(seconds: 3));

    expect(find.text('Welcome'), findsOneWidget);

    // Scroll and interact
    await tester.drag(find.byType(ListView), const Offset(0, -500));
    await tester.pumpAndSettle();
    expect(find.text('Product 10'), findsOneWidget);
  });
}
```

## Bloc Testing

```dart
import 'package:bloc_test/bloc_test.dart';

blocTest<UserBloc, UserState>(
  'emits [loading, loaded] when FetchUsers is added',
  build: () {
    when(() => mockRepo.getUsers()).thenAnswer((_) async => [User(name: 'Alice')]);
    return UserBloc(mockRepo);
  },
  act: (bloc) => bloc.add(FetchUsers()),
  expect: () => [
    const UserState.loading(),
    isA<UserLoaded>().having((s) => s.users.length, 'user count', 1),
  ],
  verify: (_) { verify(() => mockRepo.getUsers()).called(1); },
);
```

## Anti-Patterns

- ❌ `await tester.pump()` without `pumpAndSettle()` for animations — incomplete renders
- ❌ `find.text()` for dynamic/localized strings — use `find.byKey(Key('...'))`
- ❌ Missing `setUp`/`tearDown` for mocks — stale state between tests
- ❌ Integration tests without `IntegrationTestWidgetsFlutterBinding.ensureInitialized()`

````


### `reference/playbook.md`

````markdown
# Flutter Testing — Advanced Implementation Playbook

## §1 Project Setup & Configuration

### pubspec.yaml — Test Dependencies
```yaml
dev_dependencies:
  test: ^1.24.0
  flutter_test:
    sdk: flutter
  integration_test:
    sdk: flutter
  mockito: ^5.4.0
  build_runner: ^2.4.0
  bloc_test: ^9.1.0          # if using Bloc
  mocktail: ^1.0.0            # alternative no-codegen mocks
  golden_toolkit: ^0.15.0     # enhanced golden tests
  network_image_mock: ^2.1.0  # mock network images in tests
  fake_async: ^1.3.0          # time-travel in async tests

flutter:
  fonts:
    - family: Roboto
      fonts:
        - asset: assets/fonts/Roboto-Regular.ttf
```

### dart_test.yaml — Test Runner Config
```yaml
platforms: [vm]
concurrency: 4
timeout: 30s
retry: 1
tags:
  slow:
    timeout: 120s
  integration:
    timeout: 300s
```

---

## §2 Unit Tests — Advanced Patterns

### Testing with Mockito (Code Generation)
```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';

@GenerateMocks([ApiClient, AuthRepository, CacheManager])
import 'user_service_test.mocks.dart';

void main() {
  late MockApiClient mockApi;
  late MockCacheManager mockCache;
  late UserService service;

  setUp(() {
    mockApi = MockApiClient();
    mockCache = MockCacheManager();
    service = UserService(api: mockApi, cache: mockCache);
  });

  group('UserService.getUser', () {
    test('returns cached user when available', () async {
      when(mockCache.get('user:1'))
          .thenReturn(User(id: 1, name: 'Alice'));

      final user = await service.getUser(1);
      expect(user.name, equals('Alice'));
      verifyNever(mockApi.get(any));
    });

    test('fetches from API when cache miss', () async {
      when(mockCache.get('user:1')).thenReturn(null);
      when(mockApi.get('/users/1')).thenAnswer(
        (_) async => Response('{"id":1,"name":"Alice"}', 200),
      );

      final user = await service.getUser(1);
      expect(user.name, equals('Alice'));
      verify(mockCache.set('user:1', any)).called(1);
    });

    test('throws NetworkException on connection failure', () async {
      when(mockCache.get(any)).thenReturn(null);
      when(mockApi.get(any)).thenThrow(SocketException('No Internet'));

      expect(() => service.getUser(1), throwsA(isA<NetworkException>()));
    });
  });
}
```

### Testing with Mocktail (No Code Generation)
```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

class MockAuthRepo extends Mock implements AuthRepository {}

void main() {
  late MockAuthRepo mockAuth;

  setUpAll(() {
    registerFallbackValue(LoginRequest(email: '', password: ''));
  });

  setUp(() => mockAuth = MockAuthRepo());

  test('login returns token', () async {
    when(() => mockAuth.login(any()))
        .thenAnswer((_) async => AuthToken('abc123'));

    final token = await mockAuth.login(
      LoginRequest(email: 'user@test.com', password: 'pass'),
    );
    expect(token.value, 'abc123');
    verify(() => mockAuth.login(any())).called(1);
  });
}
```

### Stream & Async Testing
```dart
group('AuthBloc streams', () {
  test('emits states in correct order', () {
    final bloc = AuthBloc(mockAuth);
    expectLater(
      bloc.stream,
      emitsInOrder([isA<AuthLoading>(), isA<AuthAuthenticated>()]),
    );
    bloc.add(LoginEvent('user@test.com', 'password'));
  });

  test('debounced search emits after delay', () {
    fakeAsync((async) {
      final controller = SearchController();
      final results = <String>[];
      controller.results.listen((r) => results.add(r));

      controller.query('flu');
      controller.query('flutt');
      controller.query('flutter');

      async.elapse(Duration(milliseconds: 500));
      expect(results, hasLength(1));
      expect(results.first, contains('flutter'));
    });
  });
});
```

---

## §3 Widget Tests — Production Patterns

### Pump Helpers & Test Wrappers
```dart
Widget makeTestable(Widget child, {GoRouter? router}) {
  return MaterialApp.router(
    routerConfig: router ?? _defaultRouter(),
    localizationsDelegates: AppLocalizations.localizationsDelegates,
    supportedLocales: AppLocalizations.supportedLocales,
    theme: AppTheme.light,
    home: child,
  );
}

Widget makeProviderTestable(Widget child, {
  required AuthNotifier auth,
  required CartNotifier cart,
}) {
  return MultiProvider(
    providers: [
      ChangeNotifierProvider.value(value: auth),
      ChangeNotifierProvider.value(value: cart),
    ],
    child: MaterialApp(home: child),
  );
}
```

### Comprehensive Widget Test
```dart
testWidgets('LoginScreen validates input and navigates on success',
    (tester) async {
  final mockAuth = MockAuthNotifier();
  when(mockAuth.login(any, any)).thenAnswer((_) async => true);

  await tester.pumpWidget(makeProviderTestable(
    LoginScreen(), auth: mockAuth, cart: MockCartNotifier(),
  ));

  // Submit without filling — shows validation errors
  await tester.tap(find.byKey(Key('sign_in_button')));
  await tester.pump();
  expect(find.text('Email is required'), findsOneWidget);
  expect(find.text('Password is required'), findsOneWidget);

  // Enter invalid email
  await tester.enterText(find.byKey(Key('email_field')), 'invalid');
  await tester.tap(find.byKey(Key('sign_in_button')));
  await tester.pump();
  expect(find.text('Invalid email format'), findsOneWidget);

  // Enter valid credentials
  await tester.enterText(find.byKey(Key('email_field')), 'user@test.com');
  await tester.enterText(find.byKey(Key('password_field')), 'password123');
  await tester.tap(find.byKey(Key('sign_in_button')));
  await tester.pumpAndSettle();
  verify(mockAuth.login('user@test.com', 'password123')).called(1);
});

testWidgets('swipe to delete removes item', (tester) async {
  await tester.pumpWidget(makeTestable(CartScreen()));
  await tester.pumpAndSettle();

  final item = find.byKey(Key('cart_item_0'));
  expect(item, findsOneWidget);

  await tester.drag(item, Offset(-300, 0));
  await tester.pumpAndSettle();
  await tester.tap(find.text('Delete'));
  await tester.pumpAndSettle();
  expect(find.byKey(Key('cart_item_0')), findsNothing);
});
```

---

## §4 Golden Tests (Visual Regression)

### Basic Golden Tests
```dart
testWidgets('ProductCard matches golden', (tester) async {
  await tester.pumpWidget(MaterialApp(
    theme: AppTheme.light,
    home: Scaffold(
      body: ProductCard(
        product: Product(name: 'Widget Pro', price: 29.99, rating: 4.5),
      ),
    ),
  ));
  await tester.pumpAndSettle();

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

### Multi-Device Goldens with golden_toolkit
```dart
testGoldens('LoginScreen across devices', (tester) async {
  final builder = DeviceBuilder()
    ..overrideDevicesForAllScenarios(devices: [
      Device.phone, Device.iphone11, Device.tabletLandscape,
    ])
    ..addScenario(name: 'empty', widget: LoginScreen())
    ..addScenario(name: 'filled', widget: LoginScreen());

  await tester.pumpDeviceBuilder(builder,
      wrapper: materialAppWrapper(theme: AppTheme.light));
  await screenMatchesGolden(tester, 'login_screen_multidevice');
});
```

```bash
flutter test --update-goldens
flutter test --update-goldens --tags=golden
```

---

## §5 Integration Tests

```dart
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('end-to-end: purchase flow', () {
    testWidgets('login → browse → add to cart → checkout', (tester) async {
      app.main();
      await tester.pumpAndSettle(Duration(seconds: 3));

      // Login
      await tester.enterText(find.byKey(Key('email')), 'user@test.com');
      await tester.enterText(find.byKey(Key('password')), 'password');
      await tester.tap(find.byKey(Key('sign_in_button')));
      await tester.pumpAndSettle(Duration(seconds: 5));
      expect(find.text('Welcome'), findsOneWidget);

      // Add to cart
      await tester.tap(find.byKey(Key('product_0_add')));
      await tester.pumpAndSettle();
      expect(find.byKey(Key('cart_badge')), findsOneWidget);

      // Checkout
      await tester.tap(find.byKey(Key('cart_icon')));
      await tester.pumpAndSettle();
      await tester.tap(find.text('Checkout'));
      await tester.pumpAndSettle();
      expect(find.text('Order Confirmed'), findsOneWidget);

      // Screenshot for CI
      final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
      await binding.takeScreenshot('order_confirmed');
    });
  });
}
```

```bash
# iOS simulator
flutter test integration_test/app_test.dart

# Android device
flutter test integration_test/app_test.dart -d emulator-5554

# Firebase Test Lab
gcloud firebase test android run \
  --type instrumentation \
  --app build/app/outputs/apk/debug/app-debug.apk \
  --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk
```

---

## §6 Bloc Testing (with bloc_test)

```dart
import 'package:bloc_test/bloc_test.dart';

void main() {
  late MockAuthRepository mockAuth;
  setUp(() => mockAuth = MockAuthRepository());

  blocTest<AuthBloc, AuthState>(
    'emits [loading, authenticated] on successful login',
    build: () {
      when(mockAuth.login(any, any))
          .thenAnswer((_) async => User(id: 1, name: 'Alice'));
      return AuthBloc(mockAuth);
    },
    act: (bloc) => bloc.add(LoginRequested('user@test.com', 'pass')),
    expect: () => [AuthLoading(), AuthAuthenticated(User(id: 1, name: 'Alice'))],
    verify: (_) => verify(mockAuth.login('user@test.com', 'pass')).called(1),
  );

  blocTest<AuthBloc, AuthState>(
    'emits [loading, error] on failed login',
    build: () {
      when(mockAuth.login(any, any)).thenThrow(AuthException('Invalid'));
      return AuthBloc(mockAuth);
    },
    act: (bloc) => bloc.add(LoginRequested('bad@test.com', 'wrong')),
    expect: () => [AuthLoading(), AuthError('Invalid')],
  );
}
```

---

## §7 HTTP Mocking & Provider Testing

### Mock HTTP Client
```dart
import 'package:http/testing.dart';

test('ApiClient parses user list', () async {
  final mockClient = MockClient((request) async {
    if (request.url.path == '/api/users') {
      return Response(
        '[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]', 200,
        headers: {'content-type': 'application/json'},
      );
    }
    return Response('Not Found', 404);
  });

  final api = ApiClient(client: mockClient);
  final users = await api.fetchUsers();
  expect(users, hasLength(2));
  expect(users.first.name, 'Alice');
});
```

### Riverpod Provider Testing
```dart
testWidgets('CounterScreen shows incremented value', (tester) async {
  await tester.pumpWidget(
    ProviderScope(
      overrides: [
        counterProvider.overrideWith((ref) => CounterNotifier()..state = 5),
      ],
      child: MaterialApp(home: CounterScreen()),
    ),
  );

  expect(find.text('5'), findsOneWidget);
  await tester.tap(find.byIcon(Icons.add));
  await tester.pump();
  expect(find.text('6'), findsOneWidget);
});
```

---

## §8 CI/CD Integration

```yaml
name: Flutter CI
on:
  push: { branches: [main, develop] }
  pull_request: { branches: [main] }

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with: { flutter-version: '3.19.0', cache: true }
      - run: flutter pub get
      - run: dart run build_runner build --delete-conflicting-outputs
      - run: flutter analyze --no-fatal-infos
      - run: flutter test --coverage --reporter=github
      - uses: codecov/codecov-action@v4
        with: { file: coverage/lcov.info }

  golden:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with: { flutter-version: '3.19.0', cache: true }
      - run: flutter pub get
      - run: dart run build_runner build --delete-conflicting-outputs
      - run: flutter test --tags=golden
      - uses: actions/upload-artifact@v4
        if: failure()
        with: { name: golden-failures, path: test/failures/ }

  integration:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with: { flutter-version: '3.19.0', cache: true }
      - run: flutter pub get
      - name: Integration tests (iOS Simulator)
        run: flutter test integration_test/
```

---

## §9 Debugging Table

| # | Problem | Cause | Fix |
|---|---------|-------|-----|
| 1 | `MissingPluginException` in test | Plugin not mocked | Use `setMockMethodCallHandler` or `mocktail` to stub platform channels |
| 2 | `pumpAndSettle` times out | Infinite animation (e.g. `CircularProgressIndicator`) | Use `pump(Duration)` instead; or hide animated widget in test mode |
| 3 | Golden test fails on CI | Different OS font rendering | Run golden tests on macOS only; use `golden_toolkit` for font loading |
| 4 | `No MediaQuery widget ancestor` | Missing `MaterialApp` wrapper | Wrap widget in `MaterialApp` or use `makeTestable()` helper |
| 5 | Mock not generating `.mocks.dart` | Missing `build_runner` step | Run `dart run build_runner build --delete-conflicting-outputs` |
| 6 | `A Timer is still pending` | Unawaited timer/debounce | Use `fakeAsync` + `async.elapse()` or `tester.pump(duration)` |
| 7 | `setState() called after dispose` | Async callback fires after widget removed | Check `mounted` before `setState`; cancel subscriptions in `dispose` |
| 8 | Integration test finds no widgets | App not fully loaded | Increase timeout: `pumpAndSettle(Duration(seconds: 10))` |
| 9 | `HTTP request failed` in widget test | Real HTTP calls in test | Inject `MockClient` from `package:http/testing.dart` |
| 10 | `find.byKey` returns nothing | Key not set on widget | Add `Key('identifier')` to widget; prefer `testID`-style naming |
| 11 | Bloc test expects wrong state order | Missing intermediate states | Use `blocTest` with exact emission sequence in `expect` |
| 12 | Riverpod override not applied | Provider created before override | Use `ProviderScope(overrides: [...])` as root widget in test |

---

## §10 Best Practices Checklist

1. ✅ Use `Key` widgets on all interactive/assertable elements for stable finders
2. ✅ Generate mocks with `build_runner` — run before every test suite
3. ✅ Use `pumpAndSettle()` after interactions; `pump(duration)` for animations
4. ✅ Wrap widgets in `MaterialApp` + providers via a `makeTestable()` helper
5. ✅ Run golden tests on a single OS (macOS) for font consistency
6. ✅ Use `mocktail` for simpler mocking without code generation
7. ✅ Test Bloc/Cubit with `bloc_test` — verify state emission sequences
8. ✅ Mock HTTP via `MockClient` — never make real network calls in unit/widget tests
9. ✅ Use `fakeAsync` for time-dependent logic (debounce, timers, polling)
10. ✅ Tag slow/integration tests and run separately: `flutter test --tags=integration`
11. ✅ Use `integration_test` package with `takeScreenshot()` for CI evidence
12. ✅ Set coverage thresholds: `flutter test --coverage` + `lcov` reporting
13. ✅ Keep test files adjacent: `lib/src/auth/` → `test/src/auth/`
14. ✅ Use `setUp` / `tearDown` for clean state — never share mutable state between tests

````
