Files
unionflow-mobile-apps/test/features/onboarding/bloc/onboarding_bloc_test.dart
dahoud 37db88672b feat: BLoC tests complets + sécurité production + freerasp 7.5.1 migration
## Tests BLoC (Task P2.4 Mobile)
- 25 nouveaux fichiers *_bloc_test.dart + mocks générés (build_runner)
- Features couvertes : authentication, admin_users, adhesions, backup,
  communication/messaging, contributions, dashboard, finance (approval/budget),
  events, explore/network, feed, logs_monitoring, notifications, onboarding,
  organizations (switcher/types/CRUD), profile, reports, settings, solidarity
- ~380 tests, > 80% coverage BLoCs

## Sécurité Production (Task P2.2)
- lib/core/security/app_integrity_service.dart (freerasp 7.5.1)
- Migration API breaking changes freerasp 7.5.1 :
  - onRootDetected → onPrivilegedAccess
  - onDebuggerDetected → onDebug
  - onSignatureDetected → onAppIntegrity
  - onHookDetected → onHooks
  - onEmulatorDetected → onSimulator
  - onUntrustedInstallationSourceDetected → onUnofficialStore
  - onDeviceBindingDetected → onDeviceBinding
  - onObfuscationIssuesDetected → onObfuscationIssues
  - Talsec.start() split → start() + attachListener()
  - const AndroidConfig/IOSConfig → final (constructors call ConfigVerifier)
  - supportedAlternativeStores → supportedStores

## Pubspec
- bloc_test: ^9.1.7 → ^10.0.0 (compat flutter_bloc ^9.0.0)
- freerasp 7.5.1

## Config
- android/app/build.gradle : ajustements release
- lib/core/config/environment.dart : URLs API actualisées
- lib/main.dart + app_router : intégrations sécurité/BLoC

## Cleanup
- Suppression docs intermédiaires (TACHES_*.md, TASK_*_COMPLETION_REPORT.md,
  TESTS_UNITAIRES_PROGRESS.md)
- .g.dart régénérés (json_serializable)
- .mocks.dart régénérés (mockito)

## Résultat
- 142 fichiers, +27 596 insertions
- Toutes les tâches P2 mobile complétées

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 12:42:35 +00:00

739 lines
26 KiB
Dart

import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/onboarding/bloc/onboarding_bloc.dart';
import 'package:unionflow_mobile_apps/features/onboarding/data/datasources/souscription_datasource.dart';
import 'package:unionflow_mobile_apps/features/onboarding/data/models/formule_model.dart';
import 'package:unionflow_mobile_apps/features/onboarding/data/models/souscription_status_model.dart';
@GenerateMocks([SouscriptionDatasource])
import 'onboarding_bloc_test.mocks.dart';
// ─── Fixtures ────────────────────────────────────────────────────────────────
List<FormuleModel> _formules() => [
const FormuleModel(
code: 'BASIC',
libelle: 'Basic',
plage: 'PETITE',
plageLibelle: '1-20 membres',
minMembres: 1,
maxMembres: 20,
prixMensuel: 5000,
prixAnnuel: 50000,
ordreAffichage: 1,
),
const FormuleModel(
code: 'STANDARD',
libelle: 'Standard',
plage: 'MOYENNE',
plageLibelle: '21-100 membres',
minMembres: 21,
maxMembres: 100,
prixMensuel: 15000,
prixAnnuel: 150000,
ordreAffichage: 2,
),
];
SouscriptionStatusModel _souscription({String? waveLaunchUrl}) =>
SouscriptionStatusModel(
souscriptionId: 'sosc-1',
statutValidation: 'EN_ATTENTE_PAIEMENT',
typeFormule: 'BASIC',
plageMembres: 'PETITE',
plageLibelle: '1-20 membres',
typePeriode: 'MENSUEL',
typeOrganisation: 'ASSOCIATION',
montantTotal: 5000,
waveLaunchUrl: waveLaunchUrl,
organisationId: 'org-1',
);
// ─── Tests ───────────────────────────────────────────────────────────────────
void main() {
late OnboardingBloc bloc;
late MockSouscriptionDatasource mockDatasource;
setUp(() {
mockDatasource = MockSouscriptionDatasource();
bloc = OnboardingBloc(mockDatasource);
});
tearDown(() => bloc.close());
// ─── Initial state ──────────────────────────────────────────────────────────
test('initial state is OnboardingInitial', () {
expect(bloc.state, isA<OnboardingInitial>());
});
// ─── OnboardingStarted ──────────────────────────────────────────────────────
group('OnboardingStarted — NO_SUBSCRIPTION', () {
blocTest<OnboardingBloc, OnboardingState>(
'emits [OnboardingLoading, OnboardingStepFormule] with loaded formulas',
build: () {
when(mockDatasource.getFormules())
.thenAnswer((_) async => _formules());
return bloc;
},
act: (b) => b.add(const OnboardingStarted(
initialState: 'NO_SUBSCRIPTION',
typeOrganisation: 'ASSOCIATION',
organisationId: 'org-1',
)),
expect: () => [
isA<OnboardingLoading>(),
isA<OnboardingStepFormule>()
.having((s) => s.formules.length, 'formules count', 2),
],
);
blocTest<OnboardingBloc, OnboardingState>(
'emits [OnboardingLoading, OnboardingError] when getFormules throws',
build: () {
when(mockDatasource.getFormules())
.thenThrow(Exception('Network error'));
return bloc;
},
act: (b) => b.add(const OnboardingStarted(initialState: 'NO_SUBSCRIPTION')),
expect: () => [
isA<OnboardingLoading>(),
isA<OnboardingError>().having(
(s) => s.message,
'message',
contains('Impossible de charger'),
),
],
);
blocTest<OnboardingBloc, OnboardingState>(
'falls back to OnboardingStepFormule for unknown initialState',
build: () {
when(mockDatasource.getFormules())
.thenAnswer((_) async => _formules());
return bloc;
},
act: (b) => b.add(const OnboardingStarted(initialState: 'UNKNOWN_STATE')),
expect: () => [isA<OnboardingLoading>(), isA<OnboardingStepFormule>()],
);
});
group('OnboardingStarted — AWAITING_PAYMENT', () {
blocTest<OnboardingBloc, OnboardingState>(
'emits OnboardingStepSummary when getMaSouscription returns a souscription',
build: () {
when(mockDatasource.getFormules())
.thenAnswer((_) async => _formules());
when(mockDatasource.getMaSouscription())
.thenAnswer((_) async => _souscription());
return bloc;
},
act: (b) => b.add(const OnboardingStarted(initialState: 'AWAITING_PAYMENT')),
expect: () => [
isA<OnboardingLoading>(),
isA<OnboardingStepSummary>().having(
(s) => s.souscription.souscriptionId,
'souscriptionId',
'sosc-1',
),
],
);
blocTest<OnboardingBloc, OnboardingState>(
'falls back to OnboardingStepFormule when getMaSouscription returns null',
build: () {
when(mockDatasource.getFormules())
.thenAnswer((_) async => _formules());
when(mockDatasource.getMaSouscription())
.thenAnswer((_) async => null);
return bloc;
},
act: (b) => b.add(const OnboardingStarted(initialState: 'AWAITING_PAYMENT')),
expect: () => [isA<OnboardingLoading>(), isA<OnboardingStepFormule>()],
);
});
group('OnboardingStarted — PAYMENT_INITIATED', () {
blocTest<OnboardingBloc, OnboardingState>(
'emits OnboardingStepPaiement when souscription has waveLaunchUrl',
build: () {
when(mockDatasource.getFormules())
.thenAnswer((_) async => _formules());
when(mockDatasource.getMaSouscription()).thenAnswer(
(_) async => _souscription(waveLaunchUrl: 'https://wave.com/pay/xyz'),
);
return bloc;
},
act: (b) =>
b.add(const OnboardingStarted(initialState: 'PAYMENT_INITIATED')),
expect: () => [
isA<OnboardingLoading>(),
isA<OnboardingStepPaiement>()
.having((s) => s.waveLaunchUrl, 'waveLaunchUrl', 'https://wave.com/pay/xyz'),
],
);
blocTest<OnboardingBloc, OnboardingState>(
'emits OnboardingStepAttente when souscription has no waveLaunchUrl',
build: () {
when(mockDatasource.getFormules())
.thenAnswer((_) async => _formules());
when(mockDatasource.getMaSouscription())
.thenAnswer((_) async => _souscription()); // no waveLaunchUrl
return bloc;
},
act: (b) =>
b.add(const OnboardingStarted(initialState: 'PAYMENT_INITIATED')),
expect: () => [isA<OnboardingLoading>(), isA<OnboardingStepAttente>()],
);
});
group('OnboardingStarted — AWAITING_VALIDATION', () {
blocTest<OnboardingBloc, OnboardingState>(
'emits OnboardingStepAttente with the fetched souscription',
build: () {
when(mockDatasource.getFormules())
.thenAnswer((_) async => _formules());
when(mockDatasource.getMaSouscription())
.thenAnswer((_) async => _souscription());
return bloc;
},
act: (b) =>
b.add(const OnboardingStarted(initialState: 'AWAITING_VALIDATION')),
expect: () => [
isA<OnboardingLoading>(),
isA<OnboardingStepAttente>().having(
(s) => s.souscription?.souscriptionId,
'souscriptionId',
'sosc-1',
),
],
);
});
group('OnboardingStarted — VALIDATED', () {
blocTest<OnboardingBloc, OnboardingState>(
'emits OnboardingStepAttente for edge case where activation not yet effective',
build: () {
when(mockDatasource.getFormules())
.thenAnswer((_) async => _formules());
when(mockDatasource.getMaSouscription())
.thenAnswer((_) async => _souscription());
return bloc;
},
act: (b) => b.add(const OnboardingStarted(initialState: 'VALIDATED')),
expect: () => [isA<OnboardingLoading>(), isA<OnboardingStepAttente>()],
);
});
group('OnboardingStarted — REJECTED', () {
blocTest<OnboardingBloc, OnboardingState>(
'emits OnboardingRejected with commentaire from statutValidation',
build: () {
when(mockDatasource.getFormules())
.thenAnswer((_) async => _formules());
when(mockDatasource.getMaSouscription()).thenAnswer((_) async =>
SouscriptionStatusModel(
souscriptionId: 'sosc-2',
statutValidation: 'Documents manquants',
typeFormule: 'BASIC',
plageMembres: 'PETITE',
plageLibelle: '1-20',
typePeriode: 'MENSUEL',
typeOrganisation: 'ASSOCIATION',
organisationId: 'org-1',
));
return bloc;
},
act: (b) => b.add(const OnboardingStarted(initialState: 'REJECTED')),
expect: () => [
isA<OnboardingLoading>(),
isA<OnboardingRejected>()
.having((s) => s.commentaire, 'commentaire', 'Documents manquants'),
],
);
});
// ─── OnboardingFormuleSelected ─────────────────────────────────────────────
group('OnboardingFormuleSelected', () {
blocTest<OnboardingBloc, OnboardingState>(
'emits OnboardingStepPeriode with selected formule and plage',
build: () {
// Pre-load formulas by starting first
when(mockDatasource.getFormules())
.thenAnswer((_) async => _formules());
return bloc;
},
seed: () => OnboardingStepFormule(_formules()),
act: (b) => b.add(const OnboardingFormuleSelected(
codeFormule: 'BASIC',
plage: 'PETITE',
)),
expect: () => [
isA<OnboardingStepPeriode>()
.having((s) => s.codeFormule, 'codeFormule', 'BASIC')
.having((s) => s.plage, 'plage', 'PETITE'),
],
);
blocTest<OnboardingBloc, OnboardingState>(
'emits OnboardingStepPeriode with STANDARD/GRANDE',
build: () => bloc,
seed: () => OnboardingStepFormule(_formules()),
act: (b) => b.add(const OnboardingFormuleSelected(
codeFormule: 'STANDARD',
plage: 'GRANDE',
)),
expect: () => [
isA<OnboardingStepPeriode>()
.having((s) => s.codeFormule, 'codeFormule', 'STANDARD')
.having((s) => s.plage, 'plage', 'GRANDE'),
],
);
});
// ─── OnboardingPeriodeSelected ─────────────────────────────────────────────
group('OnboardingPeriodeSelected', () {
blocTest<OnboardingBloc, OnboardingState>(
'does not emit a new state (only updates internal data)',
build: () => bloc,
act: (b) => b.add(const OnboardingPeriodeSelected(
typePeriode: 'MENSUEL',
typeOrganisation: 'ASSOCIATION',
organisationId: 'org-1',
)),
expect: () => [],
);
});
// ─── OnboardingDemandeConfirmee ────────────────────────────────────────────
group('OnboardingDemandeConfirmee', () {
blocTest<OnboardingBloc, OnboardingState>(
'emits OnboardingError when required data is missing',
build: () => bloc,
// No formule, plage, periode, organisationId set → missing data
act: (b) => b.add(const OnboardingDemandeConfirmee()),
expect: () => [
isA<OnboardingError>().having(
(s) => s.message,
'message',
contains('Données manquantes'),
),
],
);
blocTest<OnboardingBloc, OnboardingState>(
'emits [OnboardingLoading, OnboardingStepSummary] on successful creation',
build: () {
when(mockDatasource.creerDemande(
typeFormule: anyNamed('typeFormule'),
plageMembres: anyNamed('plageMembres'),
typePeriode: anyNamed('typePeriode'),
typeOrganisation: anyNamed('typeOrganisation'),
organisationId: anyNamed('organisationId'),
)).thenAnswer((_) async => _souscription());
return bloc;
},
// Seed internal state: first select formule, then periode
act: (b) async {
when(mockDatasource.getFormules())
.thenAnswer((_) async => _formules());
b.add(const OnboardingStarted(
initialState: 'NO_SUBSCRIPTION',
typeOrganisation: 'ASSOCIATION',
organisationId: 'org-1',
));
await Future.delayed(const Duration(milliseconds: 50));
b.add(const OnboardingFormuleSelected(
codeFormule: 'BASIC',
plage: 'PETITE',
));
b.add(const OnboardingPeriodeSelected(
typePeriode: 'MENSUEL',
typeOrganisation: 'ASSOCIATION',
organisationId: 'org-1',
));
b.add(const OnboardingDemandeConfirmee());
},
expect: () => [
isA<OnboardingLoading>(), // from OnboardingStarted
isA<OnboardingStepFormule>(), // after getFormules
isA<OnboardingStepPeriode>(), // after OnboardingFormuleSelected
// OnboardingPeriodeSelected emits nothing
isA<OnboardingLoading>(), // from OnboardingDemandeConfirmee
isA<OnboardingStepSummary>()
.having((s) => s.souscription.souscriptionId, 'id', 'sosc-1'),
],
);
blocTest<OnboardingBloc, OnboardingState>(
'emits [OnboardingLoading, OnboardingError] when creerDemande returns null',
build: () {
when(mockDatasource.creerDemande(
typeFormule: anyNamed('typeFormule'),
plageMembres: anyNamed('plageMembres'),
typePeriode: anyNamed('typePeriode'),
typeOrganisation: anyNamed('typeOrganisation'),
organisationId: anyNamed('organisationId'),
)).thenAnswer((_) async => null);
return bloc;
},
act: (b) async {
when(mockDatasource.getFormules())
.thenAnswer((_) async => _formules());
b.add(const OnboardingStarted(
initialState: 'NO_SUBSCRIPTION',
typeOrganisation: 'ASSOCIATION',
organisationId: 'org-1',
));
await Future.delayed(const Duration(milliseconds: 50));
b.add(const OnboardingFormuleSelected(
codeFormule: 'BASIC',
plage: 'PETITE',
));
b.add(const OnboardingPeriodeSelected(
typePeriode: 'MENSUEL',
typeOrganisation: 'ASSOCIATION',
organisationId: 'org-1',
));
b.add(const OnboardingDemandeConfirmee());
},
expect: () => [
isA<OnboardingLoading>(),
isA<OnboardingStepFormule>(),
isA<OnboardingStepPeriode>(),
isA<OnboardingLoading>(),
isA<OnboardingError>().having(
(s) => s.message,
'message',
contains('Erreur lors de la création'),
),
],
);
blocTest<OnboardingBloc, OnboardingState>(
'emits [OnboardingLoading, OnboardingError] when creerDemande throws',
build: () {
when(mockDatasource.creerDemande(
typeFormule: anyNamed('typeFormule'),
plageMembres: anyNamed('plageMembres'),
typePeriode: anyNamed('typePeriode'),
typeOrganisation: anyNamed('typeOrganisation'),
organisationId: anyNamed('organisationId'),
)).thenThrow(Exception('API unavailable'));
return bloc;
},
act: (b) async {
when(mockDatasource.getFormules())
.thenAnswer((_) async => _formules());
b.add(const OnboardingStarted(
initialState: 'NO_SUBSCRIPTION',
typeOrganisation: 'ASSOCIATION',
organisationId: 'org-1',
));
await Future.delayed(const Duration(milliseconds: 50));
b.add(const OnboardingFormuleSelected(
codeFormule: 'BASIC',
plage: 'PETITE',
));
b.add(const OnboardingPeriodeSelected(
typePeriode: 'MENSUEL',
typeOrganisation: 'ASSOCIATION',
organisationId: 'org-1',
));
b.add(const OnboardingDemandeConfirmee());
},
expect: () => [
isA<OnboardingLoading>(),
isA<OnboardingStepFormule>(),
isA<OnboardingStepPeriode>(),
isA<OnboardingLoading>(),
isA<OnboardingError>(),
],
);
});
// ─── OnboardingChoixPaiementOuvert ─────────────────────────────────────────
group('OnboardingChoixPaiementOuvert', () {
blocTest<OnboardingBloc, OnboardingState>(
'emits OnboardingStepChoixPaiement when souscription is loaded',
build: () {
when(mockDatasource.getFormules())
.thenAnswer((_) async => _formules());
when(mockDatasource.getMaSouscription())
.thenAnswer((_) async => _souscription());
return bloc;
},
act: (b) async {
b.add(const OnboardingStarted(initialState: 'AWAITING_PAYMENT'));
await Future.delayed(const Duration(milliseconds: 50));
b.add(const OnboardingChoixPaiementOuvert());
},
expect: () => [
isA<OnboardingLoading>(),
isA<OnboardingStepSummary>(),
isA<OnboardingStepChoixPaiement>().having(
(s) => s.souscription.souscriptionId,
'souscriptionId',
'sosc-1',
),
],
);
blocTest<OnboardingBloc, OnboardingState>(
'does nothing when no souscription is loaded',
build: () => bloc,
act: (b) => b.add(const OnboardingChoixPaiementOuvert()),
expect: () => [],
);
});
// ─── OnboardingPaiementInitie ──────────────────────────────────────────────
group('OnboardingPaiementInitie', () {
blocTest<OnboardingBloc, OnboardingState>(
'emits OnboardingError when no souscription loaded',
build: () => bloc,
act: (b) => b.add(const OnboardingPaiementInitie()),
expect: () => [
isA<OnboardingError>().having(
(s) => s.message,
'message',
contains('Souscription introuvable'),
),
],
);
blocTest<OnboardingBloc, OnboardingState>(
'emits [OnboardingLoading, OnboardingStepPaiement] when initierPaiement succeeds',
build: () {
when(mockDatasource.getFormules())
.thenAnswer((_) async => _formules());
when(mockDatasource.getMaSouscription())
.thenAnswer((_) async => _souscription());
when(mockDatasource.initierPaiement('sosc-1')).thenAnswer(
(_) async => _souscription(waveLaunchUrl: 'https://wave.com/pay/abc'),
);
return bloc;
},
act: (b) async {
b.add(const OnboardingStarted(initialState: 'AWAITING_PAYMENT'));
await Future.delayed(const Duration(milliseconds: 50));
b.add(const OnboardingPaiementInitie());
},
expect: () => [
isA<OnboardingLoading>(),
isA<OnboardingStepSummary>(),
isA<OnboardingLoading>(),
isA<OnboardingStepPaiement>()
.having((s) => s.waveLaunchUrl, 'waveLaunchUrl', 'https://wave.com/pay/abc'),
],
);
blocTest<OnboardingBloc, OnboardingState>(
'emits [OnboardingLoading, OnboardingError] when initierPaiement returns null',
build: () {
when(mockDatasource.getFormules())
.thenAnswer((_) async => _formules());
when(mockDatasource.getMaSouscription())
.thenAnswer((_) async => _souscription());
when(mockDatasource.initierPaiement('sosc-1'))
.thenAnswer((_) async => null);
return bloc;
},
act: (b) async {
b.add(const OnboardingStarted(initialState: 'AWAITING_PAYMENT'));
await Future.delayed(const Duration(milliseconds: 50));
b.add(const OnboardingPaiementInitie());
},
expect: () => [
isA<OnboardingLoading>(),
isA<OnboardingStepSummary>(),
isA<OnboardingLoading>(),
isA<OnboardingError>().having(
(s) => s.message,
'message',
contains('paiement Wave'),
),
],
);
blocTest<OnboardingBloc, OnboardingState>(
'emits [OnboardingLoading, OnboardingError] when initierPaiement throws',
build: () {
when(mockDatasource.getFormules())
.thenAnswer((_) async => _formules());
when(mockDatasource.getMaSouscription())
.thenAnswer((_) async => _souscription());
when(mockDatasource.initierPaiement('sosc-1'))
.thenThrow(Exception('Wave timeout'));
return bloc;
},
act: (b) async {
b.add(const OnboardingStarted(initialState: 'AWAITING_PAYMENT'));
await Future.delayed(const Duration(milliseconds: 50));
b.add(const OnboardingPaiementInitie());
},
expect: () => [
isA<OnboardingLoading>(),
isA<OnboardingStepSummary>(),
isA<OnboardingLoading>(),
isA<OnboardingError>(),
],
);
});
// ─── OnboardingRetourDepuisWave ────────────────────────────────────────────
group('OnboardingRetourDepuisWave', () {
blocTest<OnboardingBloc, OnboardingState>(
'emits [OnboardingLoading, OnboardingError] when no souscription loaded',
build: () => bloc,
act: (b) => b.add(const OnboardingRetourDepuisWave()),
expect: () => [
isA<OnboardingLoading>(),
isA<OnboardingError>().having(
(s) => s.message,
'message',
contains('Souscription introuvable'),
),
],
);
blocTest<OnboardingBloc, OnboardingState>(
'emits [OnboardingLoading, OnboardingPaiementConfirme] when confirmerPaiement returns true',
build: () {
when(mockDatasource.getFormules())
.thenAnswer((_) async => _formules());
when(mockDatasource.getMaSouscription())
.thenAnswer((_) async =>
_souscription(waveLaunchUrl: 'https://wave.com/pay/abc'));
when(mockDatasource.initierPaiement('sosc-1')).thenAnswer(
(_) async => _souscription(waveLaunchUrl: 'https://wave.com/pay/abc'),
);
when(mockDatasource.confirmerPaiement('sosc-1'))
.thenAnswer((_) async => true);
return bloc;
},
act: (b) async {
b.add(const OnboardingStarted(initialState: 'PAYMENT_INITIATED'));
await Future.delayed(const Duration(milliseconds: 50));
b.add(const OnboardingRetourDepuisWave());
},
expect: () => [
isA<OnboardingLoading>(),
isA<OnboardingStepPaiement>(),
isA<OnboardingLoading>(),
isA<OnboardingPaiementConfirme>(),
],
);
blocTest<OnboardingBloc, OnboardingState>(
'emits [OnboardingLoading, OnboardingPaiementEchoue] when confirmerPaiement returns false',
build: () {
when(mockDatasource.getFormules())
.thenAnswer((_) async => _formules());
when(mockDatasource.getMaSouscription()).thenAnswer(
(_) async =>
_souscription(waveLaunchUrl: 'https://wave.com/pay/abc'),
);
when(mockDatasource.confirmerPaiement('sosc-1'))
.thenAnswer((_) async => false);
return bloc;
},
act: (b) async {
b.add(const OnboardingStarted(initialState: 'PAYMENT_INITIATED'));
await Future.delayed(const Duration(milliseconds: 50));
b.add(const OnboardingRetourDepuisWave());
},
expect: () => [
isA<OnboardingLoading>(),
isA<OnboardingStepPaiement>(),
isA<OnboardingLoading>(),
isA<OnboardingPaiementEchoue>().having(
(s) => s.message,
'message',
contains('confirmé'),
),
],
);
blocTest<OnboardingBloc, OnboardingState>(
'emits [OnboardingLoading, OnboardingPaiementEchoue] when confirmerPaiement throws',
build: () {
when(mockDatasource.getFormules())
.thenAnswer((_) async => _formules());
when(mockDatasource.getMaSouscription()).thenAnswer(
(_) async =>
_souscription(waveLaunchUrl: 'https://wave.com/pay/abc'),
);
when(mockDatasource.confirmerPaiement('sosc-1'))
.thenThrow(Exception('Timeout'));
return bloc;
},
act: (b) async {
b.add(const OnboardingStarted(initialState: 'PAYMENT_INITIATED'));
await Future.delayed(const Duration(milliseconds: 50));
b.add(const OnboardingRetourDepuisWave());
},
expect: () => [
isA<OnboardingLoading>(),
isA<OnboardingStepPaiement>(),
isA<OnboardingLoading>(),
isA<OnboardingPaiementEchoue>().having(
(s) => s.message,
'message',
contains('Erreur lors de la confirmation'),
),
],
);
});
// ─── State equality ────────────────────────────────────────────────────────
group('OnboardingState equality', () {
test('OnboardingError with same message are equal', () {
const s1 = OnboardingError('err');
const s2 = OnboardingError('err');
expect(s1, equals(s2));
});
test('OnboardingStepFormule props contains formulas list', () {
final formulas = _formules();
final state = OnboardingStepFormule(formulas);
expect(state.props, contains(formulas));
});
test('OnboardingStepPeriode props are correct', () {
final formulas = _formules();
const state = OnboardingStepPeriode(
codeFormule: 'BASIC',
plage: 'PETITE',
formules: [],
);
expect(state.codeFormule, 'BASIC');
expect(state.plage, 'PETITE');
});
test('OnboardingPaiementEchoue props are correct', () {
final sosc = _souscription();
final state = OnboardingPaiementEchoue(
message: 'Error',
souscription: sosc,
waveLaunchUrl: 'https://wave.com',
);
expect(state.props,
containsAll(['Error', sosc, 'https://wave.com']));
});
});
}