import 'package:commons_core/commons_core.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:tane/data/variety_repository.dart'; import 'package:tane/db/database.dart'; import 'package:tane/db/enums.dart'; import 'package:tane/state/variety_detail_cubit.dart'; import '../support/test_support.dart'; /// Waits until the cubit's state satisfies [predicate], whether it already /// does or a later stream emission gets it there. Future waitFor( VarietyDetailCubit cubit, bool Function(VarietyDetailState) predicate, ) async { if (predicate(cubit.state)) return cubit.state; return cubit.stream.firstWhere(predicate).timeout(const Duration(seconds: 5)); } void main() { late AppDatabase db; late VarietyRepository repo; setUp(() { db = newTestDatabase(); repo = newTestRepository(db); }); tearDown(() => db.close()); Future<(VarietyDetailCubit, String)> newCubit() async { final id = await repo.addQuickVariety(label: 'Maize'); return (VarietyDetailCubit(repo, id), id); } test('streams the detail and leaves loading', () async { final (cubit, _) = await newCubit(); final state = await waitFor(cubit, (s) => s.detail != null); expect(state.detail!.label, 'Maize'); await cubit.close(); }); test('updateFields re-emits the edited detail', () async { final (cubit, _) = await newCubit(); await waitFor(cubit, (s) => s.detail != null); await cubit.updateFields(label: 'Corn', notes: 'From the fair'); final state = await waitFor(cubit, (s) => s.detail?.label == 'Corn'); expect(state.detail!.notes, 'From the fair'); await cubit.close(); }); test('addLot, updateLot and deleteLot flow through the stream', () async { final (cubit, _) = await newCubit(); await waitFor(cubit, (s) => s.detail != null); await cubit.addLot( year: 2024, quantity: const Quantity(kind: QuantityKind.cob, count: 2), ); var state = await waitFor(cubit, (s) => s.detail?.lots.isNotEmpty == true); final lot = state.detail!.lots.single; expect(lot.harvestYear, 2024); await cubit.updateLot(lotId: lot.id, type: LotType.seed, year: 2023); state = await waitFor( cubit, (s) => s.detail?.lots.single.harvestYear == 2023, ); await cubit.deleteLot(lot.id); state = await waitFor(cubit, (s) => s.detail?.lots.isEmpty == true); expect(state.detail!.lots, isEmpty); await cubit.close(); }); test('vernacular names can be added and removed', () async { final (cubit, _) = await newCubit(); await waitFor(cubit, (s) => s.detail != null); await cubit.addVernacularName('Millo'); var state = await waitFor( cubit, (s) => s.detail?.vernacularNames.isNotEmpty == true, ); expect(state.detail!.vernacularNames.single.name, 'Millo'); await cubit.removeVernacularName(state.detail!.vernacularNames.single.id); state = await waitFor( cubit, (s) => s.detail?.vernacularNames.isEmpty == true, ); expect(state.detail!.vernacularNames, isEmpty); await cubit.close(); }); test('deleteVariety flags deleted and the stream then yields null', () async { final (cubit, _) = await newCubit(); await waitFor(cubit, (s) => s.detail != null); await cubit.deleteVariety(); expect(cubit.state.deleted, isTrue); // The reactive watch notices the tombstone and re-emits null. final state = await waitFor(cubit, (s) => s.detail == null); expect(state.deleted, isTrue); await cubit.close(); }); }