style: dart format reflow (no behavior change)

This commit is contained in:
vjrj 2026-07-09 22:19:43 +02:00
parent cf5d302cc1
commit e3ec855630
17 changed files with 197 additions and 139 deletions

View file

@ -18,35 +18,39 @@ void main() {
Uint8List photo(int seed) => Uint8List.fromList([seed, seed, seed]);
test('a captured draft is hidden from the list but shown in the tray',
() async {
final id = await repo.addDraftVariety(photo(1));
test(
'a captured draft is hidden from the list but shown in the tray',
() async {
final id = await repo.addDraftVariety(photo(1));
// Not in the main inventory
expect(await repo.watchInventory().first, isEmpty);
// Not in the main inventory
expect(await repo.watchInventory().first, isEmpty);
// but in the "to catalogue" tray, with its photo and no name yet.
final drafts = await repo.watchDrafts().first;
expect(drafts, hasLength(1));
expect(drafts.single.id, id);
expect(drafts.single.isDraft, isTrue);
expect(drafts.single.label, '');
expect(drafts.single.photo, isNotNull);
});
// but in the "to catalogue" tray, with its photo and no name yet.
final drafts = await repo.watchDrafts().first;
expect(drafts, hasLength(1));
expect(drafts.single.id, id);
expect(drafts.single.isDraft, isTrue);
expect(drafts.single.label, '');
expect(drafts.single.photo, isNotNull);
},
);
test('naming a draft promotes it into the list and out of the tray',
() async {
final id = await repo.addDraftVariety(photo(2));
await repo.nameDraft(id, 'Grandma tomato');
test(
'naming a draft promotes it into the list and out of the tray',
() async {
final id = await repo.addDraftVariety(photo(2));
await repo.nameDraft(id, 'Grandma tomato');
expect(await repo.watchDrafts().first, isEmpty);
final list = await repo.watchInventory().first;
expect(list.single.id, id);
expect(list.single.label, 'Grandma tomato');
expect(list.single.isDraft, isFalse);
// The captured photo survived the promotion.
expect(list.single.photo, isNotNull);
});
expect(await repo.watchDrafts().first, isEmpty);
final list = await repo.watchInventory().first;
expect(list.single.id, id);
expect(list.single.label, 'Grandma tomato');
expect(list.single.isDraft, isFalse);
// The captured photo survived the promotion.
expect(list.single.photo, isNotNull);
},
);
test('the tray is ordered newest-capture first', () async {
final first = await repo.addDraftVariety(photo(1));

View file

@ -22,7 +22,8 @@ void main() {
await newTestSpeciesRepository(db).seedBundled(const [tomato]);
final repo = newTestRepository(db);
const csv = 'variety_label,category,species_scientific_name,'
const csv =
'variety_label,category,species_scientific_name,'
'cultivar_name,lot_type,harvest_year,quantity_kind,quantity_precise\r\n'
'Grandma tomato,Solanaceae,Solanum lycopersicum,Marmande,seed,2024,cob,3\r\n';
final summary = await repo.importCsv(codec.decode(csv));
@ -44,7 +45,8 @@ void main() {
test('an unmatched species name leaves speciesId null', () async {
final repo = newTestRepository(db); // no bundled catalog
const csv = 'variety_label,species_scientific_name\r\n'
const csv =
'variety_label,species_scientific_name\r\n'
'Mystery bean,Phaseolus nonexistus\r\n';
await repo.importCsv(codec.decode(csv));
final variety = await db.select(db.varieties).getSingle();
@ -60,19 +62,26 @@ void main() {
expect(varieties, hasLength(2));
});
test('stores vernacular names and links attached to the new variety', () async {
final repo = newTestRepository(db);
const csv = 'variety_label,vernacular_names,external_links\r\n'
'Tomato,"tomate (es); granny tomato","Wiki|https://example.org"\r\n';
await repo.importCsv(codec.decode(csv));
test(
'stores vernacular names and links attached to the new variety',
() async {
final repo = newTestRepository(db);
const csv =
'variety_label,vernacular_names,external_links\r\n'
'Tomato,"tomate (es); granny tomato","Wiki|https://example.org"\r\n';
await repo.importCsv(codec.decode(csv));
final variety = await db.select(db.varieties).getSingle();
final names = await db.select(db.varietyVernacularNames).get();
expect(names.map((n) => n.name), containsAll(['tomate', 'granny tomato']));
expect(names.every((n) => n.varietyId == variety.id), isTrue);
final variety = await db.select(db.varieties).getSingle();
final names = await db.select(db.varietyVernacularNames).get();
expect(
names.map((n) => n.name),
containsAll(['tomate', 'granny tomato']),
);
expect(names.every((n) => n.varietyId == variety.id), isTrue);
final links = await db.select(db.externalLinks).get();
expect(links.single.url, 'https://example.org');
expect(links.single.parentId, variety.id);
});
final links = await db.select(db.externalLinks).get();
expect(links.single.url, 'https://example.org');
expect(links.single.parentId, variety.id);
},
);
}

View file

@ -50,14 +50,16 @@ void main() {
seedbankId: null,
);
List<String> rowsOf(String csv) =>
csv.trimRight().split('\r\n');
List<String> rowsOf(String csv) => csv.trimRight().split('\r\n');
test('emits a header and one row per lot', () {
final csv = codec.encode(
InventorySnapshot(
varieties: [variety()],
lots: [lot(), lot(id: 'lot-2')],
lots: [
lot(),
lot(id: 'lot-2'),
],
speciesNamesById: const {'sp-1': 'Solanum lycopersicum'},
),
);

View file

@ -21,7 +21,8 @@ void main() {
});
test('skips rows without a variety_label', () {
final csv = 'variety_label,category\r\n,Solanaceae\r\nTomato,Solanaceae\r\n';
final csv =
'variety_label,category\r\n,Solanaceae\r\nTomato,Solanaceae\r\n';
final result = codec.decode(csv);
expect(result.skippedRows, 1);
expect(result.varieties, hasLength(1));
@ -29,7 +30,8 @@ void main() {
});
test('maps columns by header name regardless of order', () {
final csv = 'category,variety_label,harvest_year,lot_type\r\n'
final csv =
'category,variety_label,harvest_year,lot_type\r\n'
'Solanaceae,Tomato,2024,seed\r\n';
final v = codec.decode(csv).varieties.single;
expect(v.label, 'Tomato');
@ -46,7 +48,8 @@ void main() {
test('unknown enums fall back: lot_type→seed, offer_status→private, '
'quantity_kind dropped', () {
final csv = 'variety_label,lot_type,offer_status,quantity_kind\r\n'
final csv =
'variety_label,lot_type,offer_status,quantity_kind\r\n'
'Tomato,spaceship,gift,truckload\r\n';
final lot = codec.decode(csv).varieties.single.lots.single;
expect(lot.type, LotType.seed);
@ -55,7 +58,8 @@ void main() {
});
test('keeps a valid quantity_kind and parses precise/label', () {
final csv = 'variety_label,quantity_kind,quantity_precise,quantity_label\r\n'
final csv =
'variety_label,quantity_kind,quantity_precise,quantity_label\r\n'
'Maize,cob,3,a handful\r\n';
final lot = codec.decode(csv).varieties.single.lots.single;
expect(lot.quantityKind, 'cob');
@ -63,16 +67,20 @@ void main() {
expect(lot.quantityLabel, 'a handful');
});
test('rows sharing a variety_id collapse into one variety with many lots', () {
final csv = 'variety_id,variety_label,lot_type,harvest_year\r\n'
'v1,Tomato,seed,2023\r\n'
'v1,Tomato,seed,2024\r\n'
'v2,Carrot,seed,2024\r\n';
final result = codec.decode(csv);
expect(result.varieties, hasLength(2));
final tomato = result.varieties.firstWhere((v) => v.label == 'Tomato');
expect(tomato.lots.map((l) => l.harvestYear), [2023, 2024]);
});
test(
'rows sharing a variety_id collapse into one variety with many lots',
() {
final csv =
'variety_id,variety_label,lot_type,harvest_year\r\n'
'v1,Tomato,seed,2023\r\n'
'v1,Tomato,seed,2024\r\n'
'v2,Carrot,seed,2024\r\n';
final result = codec.decode(csv);
expect(result.varieties, hasLength(2));
final tomato = result.varieties.firstWhere((v) => v.label == 'Tomato');
expect(tomato.lots.map((l) => l.harvestYear), [2023, 2024]);
},
);
test('rows without a variety_id each become their own variety', () {
final csv = 'variety_label,lot_type\r\nTomato,seed\r\nTomato,plant\r\n';
@ -81,7 +89,8 @@ void main() {
});
test('parses quoted fields with commas, doubled quotes and newlines', () {
final csv = 'variety_label,variety_notes\r\n'
final csv =
'variety_label,variety_notes\r\n'
'"Bean, ""the best""","line one\nline two"\r\n';
final v = codec.decode(csv).varieties.single;
expect(v.label, 'Bean, "the best"');
@ -89,7 +98,8 @@ void main() {
});
test('splits vernacular names and links back out', () {
final csv = 'variety_label,vernacular_names,external_links\r\n'
final csv =
'variety_label,vernacular_names,external_links\r\n'
'Tomato,"tomate (es); granny tomato","Wiki|https://example.org"\r\n';
final v = codec.decode(csv).varieties.single;
expect(v.vernacularNames.map((n) => n.name), ['tomate', 'granny tomato']);
@ -152,7 +162,10 @@ void main() {
final csv = codec.encode(
InventorySnapshot(
varieties: [variety()],
lots: [lot(), lot(id: 'lot-2')],
lots: [
lot(),
lot(id: 'lot-2'),
],
speciesNamesById: const {'sp-1': 'Solanum lycopersicum'},
),
);

View file

@ -170,7 +170,8 @@ void main() {
test('unknown fields are ignored (forward compatibility, §5.2)', () {
final root = jsonDecode(codec.encode(snapshot)) as Map<String, dynamic>;
root['someFutureSection'] = {'x': 1};
((root['varieties'] as List).single as Map<String, dynamic>)['futureField'] =
((root['varieties'] as List).single
as Map<String, dynamic>)['futureField'] =
'ignored';
final decoded = codec.decode(jsonEncode(root));
expect(decoded.varieties, [variety]);

View file

@ -114,8 +114,12 @@ void main() {
final sourceRows = await source.exportInventory();
final targetRows = await target.exportInventory();
expect(
targetRows.varieties.map((v) => v.copyWith(speciesId: const Value(null))),
sourceRows.varieties.map((v) => v.copyWith(speciesId: const Value(null))),
targetRows.varieties.map(
(v) => v.copyWith(speciesId: const Value(null)),
),
sourceRows.varieties.map(
(v) => v.copyWith(speciesId: const Value(null)),
),
);
expect(targetRows.lots, sourceRows.lots);
expect(targetRows.vernacularNames, sourceRows.vernacularNames);
@ -130,9 +134,9 @@ void main() {
expect(targetRows.varieties.single.speciesId, targetSpecies.id);
// Photo bytes and cover order survived.
final detail = await target.watchVariety(
targetRows.varieties.single.id,
).first;
final detail = await target
.watchVariety(targetRows.varieties.single.id)
.first;
expect(detail!.photos.first.bytes, [40, 50, 60]);
expect(detail.photos.last.bytes, [10, 20, 30]);
},
@ -229,10 +233,7 @@ void main() {
final written = await (targetDb.select(
targetDb.varieties,
)..where((v) => v.label.equals('Written after import'))).getSingle();
expect(
Hlc.parse(written.updatedAt).compareTo(maxImported),
greaterThan(0),
);
expect(Hlc.parse(written.updatedAt).compareTo(maxImported), greaterThan(0));
});
test('unmatched species leaves speciesId null instead of dangling', () async {

View file

@ -49,13 +49,15 @@ void main() {
await db.close();
});
test('upgrades v5 → v8 (adds Variety.isDraft) and matches the fresh schema',
() async {
final connection = await verifier.startAt(5);
final db = AppDatabase(connection);
await verifier.migrateAndValidate(db, 8);
await db.close();
});
test(
'upgrades v5 → v8 (adds Variety.isDraft) and matches the fresh schema',
() async {
final connection = await verifier.startAt(5);
final db = AppDatabase(connection);
await verifier.migrateAndValidate(db, 8);
await db.close();
},
);
test('upgrades v6 → v8 (adds Variety.isOrganic, Species.viabilityYears) and '
'matches the fresh schema', () async {

View file

@ -12,21 +12,24 @@ String page(List<String> words) =>
void main() {
group('pickLabelFromHocr picks the largest print, not the longest line', () {
test('Aubergine Black Beauty packet → the variety name, not boilerplate', () {
// "Organic Product" (14 letters) is longer than "Black Beauty" (11) but
// printed smaller the old longest-line heuristic wrongly picked it.
final hocr = page([
word('BonPrime', 120, 300, 300, 335), // brand, tiny
word('Organic', 130, 1050, 320, 1095), // boilerplate, small
word('Product', 130, 1100, 340, 1145),
word('AUBERGINE', 380, 1180, 900, 1300), // the title (tallest)
word('BLACK', 380, 1330, 620, 1410),
word('BEAUTY', 640, 1330, 900, 1410),
word('HERB', 430, 1470, 560, 1510), // boilerplate, small
word('SEEDS', 580, 1470, 740, 1510),
]);
expect(pickLabelFromHocr(hocr), 'Aubergine Black Beauty');
});
test(
'Aubergine Black Beauty packet → the variety name, not boilerplate',
() {
// "Organic Product" (14 letters) is longer than "Black Beauty" (11) but
// printed smaller the old longest-line heuristic wrongly picked it.
final hocr = page([
word('BonPrime', 120, 300, 300, 335), // brand, tiny
word('Organic', 130, 1050, 320, 1095), // boilerplate, small
word('Product', 130, 1100, 340, 1145),
word('AUBERGINE', 380, 1180, 900, 1300), // the title (tallest)
word('BLACK', 380, 1330, 620, 1410),
word('BEAUTY', 640, 1330, 900, 1410),
word('HERB', 430, 1470, 560, 1510), // boilerplate, small
word('SEEDS', 580, 1470, 740, 1510),
]);
expect(pickLabelFromHocr(hocr), 'Aubergine Black Beauty');
},
);
test('Cucumber Supermarketer packet → the variety name', () {
final hocr = page([

View file

@ -63,10 +63,7 @@ void main() {
// Explanation text and kanji are shown above the fold.
expect(find.text('種まき'), findsOneWidget);
expect(
find.textContaining('helps people and collectives'),
findsOneWidget,
);
expect(find.textContaining('helps people and collectives'), findsOneWidget);
expect(find.textContaining('digital Plantare'), findsOneWidget);
// License and version live below the fold; scroll them into view.

View file

@ -48,27 +48,30 @@ void main() {
0x42, 0x60, 0x82,
]);
testWidgets('a supported engine offers a suggestion that pre-fills the name',
(tester) async {
await tester.pumpWidget(
_host(
NameDraftDialog(
photo: photo,
extractor: _FakeExtractor(result: 'Aubergine Black Beauty'),
testWidgets(
'a supported engine offers a suggestion that pre-fills the name',
(tester) async {
await tester.pumpWidget(
_host(
NameDraftDialog(
photo: photo,
extractor: _FakeExtractor(result: 'Aubergine Black Beauty'),
),
),
),
);
await tester.pumpAndSettle();
);
await tester.pumpAndSettle();
expect(find.byKey(const Key('draft.suggestFromPhoto')), findsOneWidget);
await tester.tap(find.byKey(const Key('draft.suggestFromPhoto')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('draft.suggestFromPhoto')), findsOneWidget);
await tester.tap(find.byKey(const Key('draft.suggestFromPhoto')));
await tester.pumpAndSettle();
expect(find.text('Aubergine Black Beauty'), findsOneWidget);
});
expect(find.text('Aubergine Black Beauty'), findsOneWidget);
},
);
testWidgets('an unsupported engine hides the suggestion button',
(tester) async {
testWidgets('an unsupported engine hides the suggestion button', (
tester,
) async {
await tester.pumpWidget(
_host(
NameDraftDialog(
@ -81,8 +84,9 @@ void main() {
expect(find.byKey(const Key('draft.suggestFromPhoto')), findsNothing);
});
testWidgets('a null suggestion leaves the field untouched (no crash)',
(tester) async {
testWidgets('a null suggestion leaves the field untouched (no crash)', (
tester,
) async {
await tester.pumpWidget(
_host(NameDraftDialog(photo: photo, extractor: _FakeExtractor())),
);

View file

@ -40,7 +40,10 @@ void main() {
// The two captures are in the tray, not the main list.
expect(find.byKey(const Key('inventory.triageBanner')), findsOneWidget);
expect(find.text('2 to catalogue'), findsOneWidget);
expect(find.text('No seeds yet. Tap + to add your first.'), findsOneWidget);
expect(
find.text('No seeds yet. Tap + to add your first.'),
findsOneWidget,
);
// Open the tray and name the first draft.
await tester.tap(find.byKey(const Key('inventory.triageBanner')));