tane/apps/app_seeds/test/ui/qr_scan_test.dart
vjrj f1aa8f8e66 feat(app): scan a printed seed label back into its record
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.
2026-07-18 12:26:27 +02:00

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);
});
}