Closes the physical↔digital loop the labels opened: the QR on a printed envelope (tane://seed, already encoded by seed_label_codec) can now be scanned from the inventory bar. A known label opens its variety; an unknown one asks before adding anything, then creates the variety (species linked by exact scientific name when bundled) with a lot carrying the label's year and origin. Scanner is pure ZXing (zxing_barcode_scanner, MIT) — no ML Kit, no Play Services, F-Droid-safe, the stack Ğ1nkgo ships. Mobile-only; other platforms don't show the button (OCR precedent). The post-scan flow is a plain function (handleScannedPayload) so it's widget-tested without a camera.
133 lines
4 KiB
Dart
133 lines
4 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:tane/data/export_import/seed_label_codec.dart';
|
|
import 'package:tane/data/species_repository.dart';
|
|
import 'package:tane/data/variety_repository.dart';
|
|
import 'package:tane/db/database.dart';
|
|
import 'package:tane/ui/qr_scan.dart';
|
|
|
|
import '../support/test_support.dart';
|
|
|
|
/// Hosts a button that feeds [payload] to [handleScannedPayload], standing in
|
|
/// for the camera — the handler is what carries the flow's logic.
|
|
class _ScanHost extends StatelessWidget {
|
|
const _ScanHost({
|
|
required this.repository,
|
|
required this.species,
|
|
required this.payload,
|
|
required this.opened,
|
|
});
|
|
|
|
final VarietyRepository repository;
|
|
final SpeciesRepository species;
|
|
final String payload;
|
|
final List<String> opened;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
body: Center(
|
|
child: ElevatedButton(
|
|
key: const Key('scanHost.fire'),
|
|
onPressed: () => handleScannedPayload(
|
|
context,
|
|
repository: repository,
|
|
species: species,
|
|
payload: payload,
|
|
openVariety: opened.add,
|
|
),
|
|
child: const Text('scan'),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
late AppDatabase db;
|
|
late VarietyRepository repo;
|
|
late SpeciesRepository species;
|
|
|
|
setUp(() {
|
|
db = newTestDatabase();
|
|
repo = newTestRepository(db);
|
|
species = newTestSpeciesRepository(db);
|
|
});
|
|
|
|
tearDown(() => db.close());
|
|
|
|
Future<List<String>> pumpAndFire(WidgetTester tester, String payload) async {
|
|
final opened = <String>[];
|
|
await tester.pumpWidget(
|
|
wrapScreen(
|
|
repository: repo,
|
|
child: _ScanHost(
|
|
repository: repo,
|
|
species: species,
|
|
payload: payload,
|
|
opened: opened,
|
|
),
|
|
),
|
|
);
|
|
await tester.tap(find.byKey(const Key('scanHost.fire')));
|
|
await tester.pumpAndSettle();
|
|
return opened;
|
|
}
|
|
|
|
testWidgets('a known label opens its record straight away', (tester) async {
|
|
final id = await repo.addQuickVariety(label: 'Tomate Rosa');
|
|
final payload = SeedLabelCodec.encode(
|
|
const SeedLabelData(varietyLabel: 'tomate rosa'),
|
|
);
|
|
final opened = await pumpAndFire(tester, payload);
|
|
expect(opened, [id]);
|
|
expect(find.byKey(const Key('scan.addPrompt')), findsNothing);
|
|
await disposeTree(tester);
|
|
});
|
|
|
|
testWidgets('an unknown label asks first, then adds and opens it', (
|
|
tester,
|
|
) async {
|
|
final payload = SeedLabelCodec.encode(
|
|
const SeedLabelData(
|
|
varietyLabel: 'Acelga de Perales',
|
|
year: 2024,
|
|
origin: 'María',
|
|
),
|
|
);
|
|
final opened = await pumpAndFire(tester, payload);
|
|
expect(find.byKey(const Key('scan.addPrompt')), findsOneWidget);
|
|
expect(opened, isEmpty); // nothing created before the person says so
|
|
expect(await db.select(db.varieties).get(), isEmpty);
|
|
|
|
await tester.tap(find.byKey(const Key('scan.add')));
|
|
await tester.pumpAndSettle();
|
|
|
|
final varieties = await db.select(db.varieties).get();
|
|
expect(varieties.single.label, 'Acelga de Perales');
|
|
final lots = await db.select(db.lots).get();
|
|
expect(lots.single.harvestYear, 2024);
|
|
expect(lots.single.originName, 'María');
|
|
expect(opened, [varieties.single.id]);
|
|
await disposeTree(tester);
|
|
});
|
|
|
|
testWidgets('declining the prompt adds nothing', (tester) async {
|
|
final payload = SeedLabelCodec.encode(
|
|
const SeedLabelData(varietyLabel: 'Acelga de Perales'),
|
|
);
|
|
final opened = await pumpAndFire(tester, payload);
|
|
await tester.tap(find.byKey(const Key('scan.cancel')));
|
|
await tester.pumpAndSettle();
|
|
expect(await db.select(db.varieties).get(), isEmpty);
|
|
expect(opened, isEmpty);
|
|
await disposeTree(tester);
|
|
});
|
|
|
|
testWidgets('a non-label QR just says so', (tester) async {
|
|
final opened = await pumpAndFire(tester, 'https://example.org/whatever');
|
|
expect(find.text('That code is not a seed label'), findsOneWidget);
|
|
expect(opened, isEmpty);
|
|
await disposeTree(tester);
|
|
});
|
|
}
|