style: dart format reflow (no behavior change)
This commit is contained in:
parent
cf5d302cc1
commit
e3ec855630
17 changed files with 197 additions and 139 deletions
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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'},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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'},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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([
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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())),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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')));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue