The note growers most wish they had two seasons later — did it do well, was it worth keeping — never had a home. New append-only GardenOutcomes table (lotId, season year, three-face rating, free note); the question appears ONCE, inline, right after recording a harvest in the batch story, and is skippable without a trace. The answer shows as one more story line. Rides backups/sync like every mutable table (JSON codec, LWW import, HLC clock absorb). Migration v12→v13 guarded + verified from every historical version.
170 lines
5.4 KiB
Dart
170 lines
5.4 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('includes garden outcomes as story lines', () async {
|
|
final lotId = await newLot();
|
|
await repo.addGardenOutcome(
|
|
lotId: lotId,
|
|
year: 2026,
|
|
rating: GardenOutcomeRating.good,
|
|
notes: 'true to type, keeping it',
|
|
);
|
|
final history = await repo.lotHistory(lotId);
|
|
final outcome = history.singleWhere(
|
|
(e) => e.kind == LotHistoryKind.gardenOutcome,
|
|
);
|
|
expect(outcome.rating, GardenOutcomeRating.good);
|
|
expect(outcome.year, 2026);
|
|
expect(outcome.notes, 'true to type, keeping it');
|
|
});
|
|
|
|
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,
|
|
);
|
|
});
|
|
});
|
|
}
|