tane/apps/app_seeds/test/ui/draft_triage_test.dart
vjrj 6809dc6143 feat(inventory): photo-first drafts + on-device OCR (digitization R2+R4)
Lower the bulk-digitization cliff with two more routes on top of the
already-landed CSV import and "save and add another":

- Photo-first drafts (capture now, catalogue later): burst-capture
  photos (camera or multi-gallery) into unnamed draft varieties, shown
  in a "to catalogue" tray, hidden from the main list until named.
  Adds Variety.isDraft (schema), addDraftVariety/watchDrafts/nameDraft,
  the triage sheet and the inventory banner.
- On-device OCR label suggestion (Tesseract, offline, no Google): a
  "Suggest name from photo" button in the naming dialog behind a
  LabelTextExtractor interface (Tesseract on Android/iOS, no-op
  elsewhere). Reads the largest print via hOCR bounding boxes, drops
  boilerplate/low-confidence noise, preprocesses (grayscale, contrast,
  upscale) and sweeps rotations (0-315 deg) so tilted packets still
  read. Bundles tessdata_fast eng+spa; validated on-device against real
  packets. The photo is written to a temp file deleted immediately in a
  finally block (the plugin needs a path) - a bounded, documented
  exception to no-plaintext-at-rest.

This commit also carries the co-developed schema evolution v5 to v8 that
shares these files (organic flag, species viability years, crop
calendar, lot provenance/abundance/preservation format, condition
checks) plus their exports/migrations and i18n.

Tests: CSV/draft/OCR unit + widget + migration green in isolation.
Note: the full widget suite currently hangs (>10 min) - under investigation.
2026-07-09 21:23:46 +02:00

72 lines
2.6 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tane/i18n/strings.g.dart';
import 'package:tane/ui/inventory_list_screen.dart';
import '../support/test_support.dart';
void main() {
// A valid 1×1 PNG so Image.memory decodes it in the widget test (the seed
// only nudges a trailing byte to keep captures distinct).
Uint8List photo(int seed) => Uint8List.fromList([
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, //
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4,
0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41,
0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00,
0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00,
0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE,
0x42, 0x60, 0x82, seed,
]);
testWidgets(
'drafts show a triage banner, stay out of the list, and can be named',
(tester) async {
LocaleSettings.setLocaleSync(AppLocale.en);
final db = newTestDatabase();
addTearDown(db.close);
final repo = newTestRepository(db);
await repo.addDraftVariety(photo(1));
await repo.addDraftVariety(photo(2));
await tester.pumpWidget(
wrapScreen(repository: repo, child: const InventoryListScreen()),
);
await tester.pumpAndSettle();
// 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);
// Open the tray and name the first draft.
await tester.tap(find.byKey(const Key('inventory.triageBanner')));
await tester.pumpAndSettle();
expect(find.text('Unnamed'), findsNWidgets(2));
await tester.tap(find.text('Unnamed').first);
await tester.pumpAndSettle();
await tester.enterText(
find.byKey(const Key('draft.nameField')),
'Grandma tomato',
);
await tester.tap(find.byKey(const Key('draft.nameSave')));
await tester.pumpAndSettle();
// One draft left in the tray…
expect(find.text('Unnamed'), findsOneWidget);
// …and close the sheet: the named seed is now in the inventory, the
// banner shows one remaining.
await tester.tapAt(const Offset(20, 20));
await tester.pumpAndSettle();
expect(find.text('Grandma tomato'), findsOneWidget);
expect(find.text('1 to catalogue'), findsOneWidget);
await disposeTree(tester);
},
);
}