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

@ -267,7 +267,10 @@ class InventoryCsvCodec {
); );
} }
CsvImportLot? _parseLot(List<String> row, String? Function(List<String>, String) cell) { CsvImportLot? _parseLot(
List<String> row,
String? Function(List<String>, String) cell,
) {
final typeCell = cell(row, 'lot_type'); final typeCell = cell(row, 'lot_type');
final year = cell(row, 'harvest_year'); final year = cell(row, 'harvest_year');
final month = cell(row, 'harvest_month'); final month = cell(row, 'harvest_month');
@ -452,7 +455,8 @@ class InventoryCsvCodec {
i++; i++;
} }
row.add(field.toString()); row.add(field.toString());
final trailingEmpty = rows.isNotEmpty && row.length == 1 && row.first.isEmpty; final trailingEmpty =
rows.isNotEmpty && row.length == 1 && row.first.isEmpty;
if (!trailingEmpty) rows.add(row); if (!trailingEmpty) rows.add(row);
return rows; return rows;
} }

View file

@ -51,11 +51,7 @@ class InventorySnapshot {
/// newer incoming version (LWW), and rows skipped (already present and at /// newer incoming version (LWW), and rows skipped (already present and at
/// least as recent, or not decodable). /// least as recent, or not decodable).
class ImportSummary { class ImportSummary {
const ImportSummary({ const ImportSummary({this.inserted = 0, this.updated = 0, this.skipped = 0});
this.inserted = 0,
this.updated = 0,
this.skipped = 0,
});
final int inserted; final int inserted;
final int updated; final int updated;

View file

@ -65,13 +65,11 @@ class SpeciesRepository {
if (existing != null) { if (existing != null) {
// Backfill bundled reference data added after the row was seeded. // Backfill bundled reference data added after the row was seeded.
if (existing.viabilityYears == null && seed.viabilityYears != null) { if (existing.viabilityYears == null && seed.viabilityYears != null) {
await (_db.update(_db.species) await (_db.update(
..where((s) => s.id.equals(existing.id))) _db.species,
.write( )..where((s) => s.id.equals(existing.id))).write(
SpeciesCompanion( SpeciesCompanion(viabilityYears: Value(seed.viabilityYears)),
viabilityYears: Value(seed.viabilityYears), );
),
);
} }
continue; continue;
} }

View file

@ -77,9 +77,24 @@ class TesseractLabelExtractor implements LabelTextExtractor {
/// Words in an hOCR document ignored as packet boilerplate (lower-cased, /// Words in an hOCR document ignored as packet boilerplate (lower-cased,
/// accent-stripped compare). Kept deliberately small and generic. /// accent-stripped compare). Kept deliberately small and generic.
const _boilerplate = <String>{ const _boilerplate = <String>{
'organic', 'product', 'products', 'herb', 'herbs', 'seed', 'seeds', 'organic',
'bio', 'net', 'weight', 'wt', 'www', 'com', 'gr', 'grs', 'grams', 'product',
'variety', 'quality', 'products',
'herb',
'herbs',
'seed',
'seeds',
'bio',
'net',
'weight',
'wt',
'www',
'com',
'gr',
'grs',
'grams',
'variety',
'quality',
}; };
/// Selects the variety name from Tesseract [hocr]: keep the words whose glyph /// Selects the variety name from Tesseract [hocr]: keep the words whose glyph
@ -112,7 +127,13 @@ HocrLabel? readHocrLabel(String hocr, {int minConfidence = 20}) {
final text = _unescape(m.group(6)!).trim(); final text = _unescape(m.group(6)!).trim();
if (text.isEmpty) continue; if (text.isEmpty) continue;
words.add( words.add(
_Word(x0: x0, y0: y0, height: y1 - y0, text: text, confidence: confidence), _Word(
x0: x0,
y0: y0,
height: y1 - y0,
text: text,
confidence: confidence,
),
); );
} }
if (words.isEmpty) return null; if (words.isEmpty) return null;

View file

@ -130,6 +130,8 @@ class QuickAddCubit extends Cubit<QuickAddState> {
quantity: state.quantity, quantity: state.quantity,
photoBytes: state.photoBytes, photoBytes: state.photoBytes,
); );
emit(QuickAddState(lotType: state.lotType, addedCount: state.addedCount + 1)); emit(
QuickAddState(lotType: state.lotType, addedCount: state.addedCount + 1),
);
} }
} }

View file

@ -76,10 +76,8 @@ class _TriageSheet extends StatelessWidget {
child: ListView.builder( child: ListView.builder(
shrinkWrap: true, shrinkWrap: true,
itemCount: drafts.length, itemCount: drafts.length,
itemBuilder: (context, i) => _DraftTile( itemBuilder: (context, i) =>
draft: drafts[i], _DraftTile(draft: drafts[i], repository: repository),
repository: repository,
),
), ),
), ),
], ],

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -48,27 +48,30 @@ void main() {
0x42, 0x60, 0x82, 0x42, 0x60, 0x82,
]); ]);
testWidgets('a supported engine offers a suggestion that pre-fills the name', testWidgets(
(tester) async { 'a supported engine offers a suggestion that pre-fills the name',
await tester.pumpWidget( (tester) async {
_host( await tester.pumpWidget(
NameDraftDialog( _host(
photo: photo, NameDraftDialog(
extractor: _FakeExtractor(result: 'Aubergine Black Beauty'), photo: photo,
extractor: _FakeExtractor(result: 'Aubergine Black Beauty'),
),
), ),
), );
); await tester.pumpAndSettle();
await tester.pumpAndSettle();
expect(find.byKey(const Key('draft.suggestFromPhoto')), findsOneWidget); expect(find.byKey(const Key('draft.suggestFromPhoto')), findsOneWidget);
await tester.tap(find.byKey(const Key('draft.suggestFromPhoto'))); await tester.tap(find.byKey(const Key('draft.suggestFromPhoto')));
await tester.pumpAndSettle(); 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', testWidgets('an unsupported engine hides the suggestion button', (
(tester) async { tester,
) async {
await tester.pumpWidget( await tester.pumpWidget(
_host( _host(
NameDraftDialog( NameDraftDialog(
@ -81,8 +84,9 @@ void main() {
expect(find.byKey(const Key('draft.suggestFromPhoto')), findsNothing); expect(find.byKey(const Key('draft.suggestFromPhoto')), findsNothing);
}); });
testWidgets('a null suggestion leaves the field untouched (no crash)', testWidgets('a null suggestion leaves the field untouched (no crash)', (
(tester) async { tester,
) async {
await tester.pumpWidget( await tester.pumpWidget(
_host(NameDraftDialog(photo: photo, extractor: _FakeExtractor())), _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. // The two captures are in the tray, not the main list.
expect(find.byKey(const Key('inventory.triageBanner')), findsOneWidget); expect(find.byKey(const Key('inventory.triageBanner')), findsOneWidget);
expect(find.text('2 to catalogue'), 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. // Open the tray and name the first draft.
await tester.tap(find.byKey(const Key('inventory.triageBanner'))); await tester.tap(find.byKey(const Key('inventory.triageBanner')));