feat(app): show each batch's story as a composite timeline

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).
This commit is contained in:
vjrj 2026-07-18 06:25:55 +02:00
parent 11cbdf3022
commit 92fd84590b
10 changed files with 835 additions and 2 deletions

View file

@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tane/db/database.dart';
import 'package:tane/db/enums.dart';
import '../support/test_support.dart';
void main() {
late AppDatabase db;
setUp(() => db = newTestDatabase());
tearDown(() => db.close());
testWidgets('the lot tile opens the batch story, closed by its origin', (
tester,
) async {
final repo = newTestRepository(db);
final id = await repo.addQuickVariety(label: 'Maize');
final lotId = await repo.addLot(
varietyId: id,
originName: 'María',
originPlace: 'Aiako',
);
await repo.addGerminationTest(
lotId: lotId,
sampleSize: 10,
germinatedCount: 9,
);
await tester.pumpWidget(
wrapDetail(
repository: repo,
varietyId: id,
species: newTestSpeciesRepository(db),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(Key('lot.history.$lotId')));
await tester.pumpAndSettle();
// The story: germination test (90%) first, creation-with-origin last.
expect(find.text('Germination test — 90%'), findsOneWidget);
expect(find.text('Added to your collection'), findsOneWidget);
expect(find.text('From María · Aiako'), findsOneWidget);
await disposeTree(tester);
});
testWidgets('one tap records sown today and it shows up in the story', (
tester,
) async {
final repo = newTestRepository(db);
final id = await repo.addQuickVariety(label: 'Maize');
final lotId = await repo.addLot(varietyId: id);
await tester.pumpWidget(
wrapDetail(
repository: repo,
varietyId: id,
species: newTestSpeciesRepository(db),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(Key('lot.history.$lotId')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('history.sowToday')));
await tester.pumpAndSettle();
// One-shot .get() (NOT watch().first hangs under the fake-async clock).
final movements = await db.select(db.movements).get();
expect(movements.single.type, MovementType.sown);
expect(movements.single.lotId, lotId);
expect(movements.single.occurredOn, isNotNull);
// And the story refreshed to show it.
expect(find.text('Sown'), findsOneWidget);
await disposeTree(tester);
});
testWidgets('a plant lot offers harvest but not sowing', (tester) async {
final repo = newTestRepository(db);
final id = await repo.addQuickVariety(label: 'Rosemary');
final lotId = await repo.addLot(varietyId: id, type: LotType.plant);
await tester.pumpWidget(
wrapDetail(
repository: repo,
varietyId: id,
species: newTestSpeciesRepository(db),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(Key('lot.history.$lotId')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('history.sowToday')), findsNothing);
expect(find.byKey(const Key('history.harvestToday')), findsOneWidget);
await disposeTree(tester);
});
}