The Movement log (history + provenance DAG) was recorded but invisible. A new history sheet on every lot tile narrates the batch: movements, germination tests, storage checks and how it entered the collection (origin attached), most-recent first. Two one-tap chips record 'sown today' / 'harvested today' — the first UI door to the sown/harvested movement types. No schema change; one-shot reads only (no live subscriptions in a transient sheet).
153 lines
4.8 KiB
Dart
153 lines
4.8 KiB
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 '../support/test_support.dart';
|
|
|
|
void main() {
|
|
late AppDatabase db;
|
|
late VarietyRepository repo;
|
|
|
|
setUp(() {
|
|
db = newTestDatabase();
|
|
repo = newTestRepository(db);
|
|
});
|
|
|
|
tearDown(() async => db.close());
|
|
|
|
Future<String> newLot({String? originName, String? originPlace}) async {
|
|
final varietyId = await repo.addQuickVariety(label: 'Tomate rosa');
|
|
return repo.addLot(
|
|
varietyId: varietyId,
|
|
harvestYear: 2025,
|
|
originName: originName,
|
|
originPlace: originPlace,
|
|
);
|
|
}
|
|
|
|
group('recordCultivation', () {
|
|
test('records a sown movement with defaults', () async {
|
|
final lotId = await newLot();
|
|
final id = await repo.recordCultivation(
|
|
lotId: lotId,
|
|
type: MovementType.sown,
|
|
);
|
|
expect(id, isNotEmpty);
|
|
final history = await repo.lotHistory(lotId);
|
|
final sown = history.where(
|
|
(e) => e.movementType == MovementType.sown,
|
|
);
|
|
expect(sown, hasLength(1));
|
|
expect(sown.first.kind, LotHistoryKind.movement);
|
|
expect(sown.first.when, isNotNull);
|
|
});
|
|
|
|
test('records a harvested movement with a note and date', () async {
|
|
final lotId = await newLot();
|
|
await repo.recordCultivation(
|
|
lotId: lotId,
|
|
type: MovementType.harvested,
|
|
occurredOn: DateTime(2026, 7, 1).millisecondsSinceEpoch,
|
|
notes: 'great year',
|
|
);
|
|
final history = await repo.lotHistory(lotId);
|
|
final harvested = history.singleWhere(
|
|
(e) => e.movementType == MovementType.harvested,
|
|
);
|
|
expect(harvested.notes, 'great year');
|
|
expect(
|
|
harvested.when,
|
|
DateTime(2026, 7, 1).millisecondsSinceEpoch,
|
|
);
|
|
});
|
|
|
|
test('rejects non-cultivation movement types', () async {
|
|
final lotId = await newLot();
|
|
expect(
|
|
() => repo.recordCultivation(lotId: lotId, type: MovementType.given),
|
|
throwsA(isA<ArgumentError>()),
|
|
);
|
|
});
|
|
});
|
|
|
|
group('lotHistory', () {
|
|
test('starts with the lot creation entry carrying its origin', () async {
|
|
final lotId = await newLot(originName: 'María', originPlace: 'Aiako');
|
|
final history = await repo.lotHistory(lotId);
|
|
expect(history, isNotEmpty);
|
|
// Most-recent first: creation is the last entry.
|
|
final created = history.last;
|
|
expect(created.kind, LotHistoryKind.created);
|
|
expect(created.originName, 'María');
|
|
expect(created.originPlace, 'Aiako');
|
|
});
|
|
|
|
test('merges movements, germination tests and condition checks', () async {
|
|
final lotId = await newLot();
|
|
await repo.addGerminationTest(
|
|
lotId: lotId,
|
|
testedOn: DateTime(2026, 2, 1).millisecondsSinceEpoch,
|
|
sampleSize: 10,
|
|
germinatedCount: 8,
|
|
);
|
|
await repo.addConditionCheck(
|
|
lotId: lotId,
|
|
checkedOn: DateTime(2026, 3, 1).millisecondsSinceEpoch,
|
|
containerCount: 2,
|
|
desiccantState: DesiccantState.dry,
|
|
);
|
|
await repo.recordCultivation(
|
|
lotId: lotId,
|
|
type: MovementType.sown,
|
|
occurredOn: DateTime(2026, 4, 1).millisecondsSinceEpoch,
|
|
);
|
|
await repo.recordHandover(
|
|
lotId: lotId,
|
|
direction: HandoverDirection.iGave,
|
|
counterparty: 'Pepe',
|
|
);
|
|
|
|
final history = await repo.lotHistory(lotId);
|
|
expect(history.map((e) => e.kind).toSet(), {
|
|
LotHistoryKind.created,
|
|
LotHistoryKind.movement,
|
|
LotHistoryKind.germinationTest,
|
|
LotHistoryKind.conditionCheck,
|
|
});
|
|
|
|
final germination = history.singleWhere(
|
|
(e) => e.kind == LotHistoryKind.germinationTest,
|
|
);
|
|
expect(germination.germinationRate, closeTo(0.8, 1e-9));
|
|
|
|
final check = history.singleWhere(
|
|
(e) => e.kind == LotHistoryKind.conditionCheck,
|
|
);
|
|
expect(check.containerCount, 2);
|
|
|
|
// Most-recent first: the handover (today) leads, creation closes.
|
|
expect(history.first.movementType, MovementType.given);
|
|
expect(history.last.kind, LotHistoryKind.created);
|
|
|
|
// The middle entries are ordered by their explicit dates, descending.
|
|
final dated = history
|
|
.where((e) => e.kind != LotHistoryKind.created)
|
|
.toList();
|
|
for (var i = 0; i + 1 < dated.length; i++) {
|
|
expect(dated[i].when ?? 0, greaterThanOrEqualTo(dated[i + 1].when ?? 0));
|
|
}
|
|
});
|
|
|
|
test('ignores other lots', () async {
|
|
final lotId = await newLot();
|
|
final otherLot = await newLot();
|
|
await repo.recordCultivation(lotId: otherLot, type: MovementType.sown);
|
|
final history = await repo.lotHistory(lotId);
|
|
expect(
|
|
history.where((e) => e.kind == LotHistoryKind.movement),
|
|
isEmpty,
|
|
);
|
|
});
|
|
});
|
|
}
|