test(state): cover InventoryCubit and VarietyDetailCubit; refresh block1 status doc

This commit is contained in:
vjrj 2026-07-09 22:03:36 +02:00
parent a96d82add9
commit cf5d302cc1
4 changed files with 292 additions and 47 deletions

View file

@ -0,0 +1,134 @@
import 'dart:typed_data';
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/inventory_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<InventoryState> waitFor(
InventoryCubit cubit,
bool Function(InventoryState) 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;
late InventoryCubit cubit;
setUp(() {
db = newTestDatabase();
repo = newTestRepository(db);
cubit = InventoryCubit(repo);
});
tearDown(() async {
await cubit.close();
await db.close();
});
test('subscribes to the inventory and leaves loading', () async {
await repo.addQuickVariety(label: 'Maize');
final state = await waitFor(cubit, (s) => !s.loading && s.items.isNotEmpty);
expect(state.items.single.label, 'Maize');
});
test('drafts stay in the tray and never mix into the list', () async {
await repo.addDraftVariety(Uint8List.fromList([1, 2, 3]));
var state = await waitFor(cubit, (s) => s.drafts.isNotEmpty);
expect(state.items, isEmpty);
// Naming the draft promotes it: tray empties, list gains the item.
await repo.nameDraft(state.drafts.single.id, 'Named at last');
state = await waitFor(cubit, (s) => s.items.isNotEmpty && s.drafts.isEmpty);
expect(state.items.single.label, 'Named at last');
});
test('search narrows visibleItems by label, case-insensitively', () async {
await repo.addQuickVariety(label: 'Tomate rosa');
await repo.addQuickVariety(label: 'Lechuga');
await waitFor(cubit, (s) => s.items.length == 2);
cubit.search('TOMA');
expect(cubit.state.visibleItems.single.label, 'Tomate rosa');
cubit.search('');
expect(cubit.state.visibleItems, hasLength(2));
});
test('toggleCategory adds then removes the filter', () async {
await repo.addQuickVariety(label: 'Maize', category: 'Poaceae');
await repo.addQuickVariety(label: 'Bean', category: 'Fabaceae');
await waitFor(cubit, (s) => s.items.length == 2);
cubit.toggleCategory('Poaceae');
expect(cubit.state.visibleItems.single.label, 'Maize');
cubit.toggleCategory('Poaceae');
expect(cubit.state.visibleItems, hasLength(2));
});
test('toggleType keeps only varieties holding a lot of that form', () async {
final seedId = await repo.addQuickVariety(label: 'Seedy');
await repo.addLot(varietyId: seedId, type: LotType.seed);
final plantId = await repo.addQuickVariety(label: 'Planty');
await repo.addLot(varietyId: plantId, type: LotType.plant);
await waitFor(
cubit,
(s) => s.items.length == 2 && s.items.every((i) => i.lotTypes.isNotEmpty),
);
cubit.toggleType(LotType.plant);
expect(cubit.state.visibleItems.single.label, 'Planty');
});
test('organic and needs-reproduction filters', () async {
final ecoId = await repo.addQuickVariety(label: 'Eco');
await repo.updateVariety(id: ecoId, isOrganic: true);
final tiredId = await repo.addQuickVariety(label: 'Tired');
await repo.updateVariety(id: tiredId, needsReproduction: true);
await waitFor(cubit, (s) => s.hasOrganic && s.hasNeedsReproduction);
cubit.toggleOrganicOnly();
expect(cubit.state.visibleItems.single.label, 'Eco');
cubit.toggleOrganicOnly();
cubit.toggleNeedsReproductionOnly();
expect(cubit.state.visibleItems.single.label, 'Tired');
});
test('clearFilters resets every filter but keeps the search query', () async {
await repo.addQuickVariety(label: 'Maize', category: 'Poaceae');
await waitFor(cubit, (s) => s.items.isNotEmpty);
cubit
..search('mai')
..toggleCategory('Poaceae')
..toggleType(LotType.seed)
..toggleOrganicOnly()
..toggleNeedsReproductionOnly();
cubit.clearFilters();
expect(cubit.state.categoryFilter, isEmpty);
expect(cubit.state.typeFilter, isEmpty);
expect(cubit.state.organicOnly, isFalse);
expect(cubit.state.needsReproductionOnly, isFalse);
expect(cubit.state.query, 'mai');
});
test('categories dedupes and preserves first-seen order', () async {
await repo.addQuickVariety(label: 'A', category: 'Poaceae');
await repo.addQuickVariety(label: 'B', category: 'Fabaceae');
await repo.addQuickVariety(label: 'C', category: 'Poaceae');
final state = await waitFor(cubit, (s) => s.items.length == 3);
expect(state.categories, hasLength(2));
expect(state.categories.toSet(), {'Poaceae', 'Fabaceae'});
});
}

View file

@ -19,35 +19,39 @@ void main() {
await db.close();
});
test('submitAndAddAnother with an empty label errors and saves nothing',
() async {
await cubit.submitAndAddAnother();
expect(cubit.state.showLabelError, isTrue);
expect(cubit.state.addedCount, 0);
expect(await db.select(db.varieties).get(), isEmpty);
});
test(
'submitAndAddAnother with an empty label errors and saves nothing',
() async {
await cubit.submitAndAddAnother();
expect(cubit.state.showLabelError, isTrue);
expect(cubit.state.addedCount, 0);
expect(await db.select(db.varieties).get(), isEmpty);
},
);
test('submitAndAddAnother saves, resets the form and keeps the lot type',
() async {
cubit
..setLotType(LotType.plant)
..labelChanged('Basil')
..setQuantity(const Quantity(kind: QuantityKind.plant, count: 3));
test(
'submitAndAddAnother saves, resets the form and keeps the lot type',
() async {
cubit
..setLotType(LotType.plant)
..labelChanged('Basil')
..setQuantity(const Quantity(kind: QuantityKind.plant, count: 3));
await cubit.submitAndAddAnother();
await cubit.submitAndAddAnother();
// Saved to the DB.
final varieties = await db.select(db.varieties).get();
expect(varieties.single.label, 'Basil');
// Saved to the DB.
final varieties = await db.select(db.varieties).get();
expect(varieties.single.label, 'Basil');
// Form reset for the next one, but the chosen lot type is kept and the
// sheet is NOT told to close (submitted stays false).
expect(cubit.state.label, '');
expect(cubit.state.quantity, isNull);
expect(cubit.state.lotType, LotType.plant);
expect(cubit.state.submitted, isFalse);
expect(cubit.state.addedCount, 1);
});
// Form reset for the next one, but the chosen lot type is kept and the
// sheet is NOT told to close (submitted stays false).
expect(cubit.state.label, '');
expect(cubit.state.quantity, isNull);
expect(cubit.state.lotType, LotType.plant);
expect(cubit.state.submitted, isFalse);
expect(cubit.state.addedCount, 1);
},
);
test('addedCount accumulates across several saves', () async {
cubit.labelChanged('One');

View file

@ -0,0 +1,108 @@
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<VarietyDetailState> 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();
});
}