tane/apps/app_seeds/test/ui/plantare_propose_sheet_test.dart
vjrj 621902612d feat(plantare): tie the propose form to a real seed
The propose sheet used a free-text seed name, unlinked from the
inventory. Now the seed is picked from your catalogued varieties so the
Plantaré is always tied to a real Variety (tap-through to its detail, and
it shows in that seed's commitments):

- Seed field is an Autocomplete over the inventory; on send the typed name
  resolves to the matching Variety, or creates a quick one if it's new —
  so varietyId is never null. Sheet takes the VarietyRepository; chat
  passes it.
- New repo one-shot `varietyLabels()` (id + label, no joins) feeds the
  picker. Deliberately a Future, not `watchInventory()` — a transient
  sheet must not hold a live Drift subscription (that hung the widget test
  ~36 min). Tests use it too and now run in ~2s with bounded pumps + a
  tall surface, no pumpAndSettle.

Tests: a new name creates & links a Variety; a matching name links the
existing one without duplicating. ui/services/data plantare suites green.
2026-07-15 01:24:50 +02:00

159 lines
5.8 KiB
Dart

import 'package:commons_core/commons_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tane/data/variety_repository.dart';
import 'package:tane/db/database.dart';
import 'package:tane/db/enums.dart';
import 'package:tane/i18n/strings.g.dart';
import 'package:tane/services/plantare_service.dart';
import 'package:tane/ui/plantare_propose_sheet.dart';
import '../support/test_support.dart';
/// A records-only transport so the sheet can be driven without a relay.
class _CapturingTransport implements PlantareTransport {
final proposed = <PlantarePledge>[];
@override
Future<void> propose(PlantarePledge pledge) async => proposed.add(pledge);
@override
Future<void> accept(PlantarePledge pledge) async {}
@override
Future<void> decline({
required String toPubkey,
required String pledgeId,
String reason = '',
}) async {}
@override
Stream<PlantareEnvelope> incoming() => const Stream.empty();
@override
Future<void> close() async {}
}
void main() {
// NEVER pumpAndSettle these: the sheet subscribes to a live Drift stream
// (watchInventory) and a focused TextField blinks its cursor forever, so
// pumpAndSettle never returns (see CLAUDE.md testing note). Use a tall surface
// so the whole sheet fits without scrolling, and bounded pumps only.
PlantareService serviceFor(AppDatabase db, _CapturingTransport tx) =>
PlantareService(
repo: newTestRepository(db),
selfPubkey: 'a' * 64,
selfSecretKey:
'0000000000000000000000000000000000000000000000000000000000000001',
)..bindTransport(tx);
Widget host(PlantareService service, VarietyRepository repo) =>
TranslationProvider(
child: MaterialApp(
locale: const Locale('en'),
supportedLocales: AppLocaleUtils.supportedLocales,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: Builder(
builder: (context) => Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () => showProposePlantareSheet(
context,
service: service,
repository: repo,
peerPubkey: 'b' * 64,
peerName: 'Ana',
),
child: const Text('open'),
),
),
),
),
),
);
Future<void> useTallSurface(WidgetTester tester) async {
tester.view.physicalSize = const Size(1400, 3200);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
}
/// Opens the sheet and lets the slide-in + inventory load settle (bounded).
Future<void> openSheet(WidgetTester tester) async {
await tester.tap(find.text('open'));
await tester.pump(); // start the sheet route
await tester.pump(const Duration(milliseconds: 400)); // slide-in + load
await tester.pump(const Duration(milliseconds: 200));
}
/// Taps send and lets the async propose (DB write + sign + pop) drain.
Future<void> tapSend(WidgetTester tester) async {
await tester.tap(find.byKey(const Key('propose.send')));
for (var i = 0; i < 6; i++) {
await tester.pump(const Duration(milliseconds: 100));
}
}
testWidgets('a new seed name proposes a signed pledge linked to a Variety',
(tester) async {
LocaleSettings.setLocaleSync(AppLocale.en);
await useTallSurface(tester);
final db = newTestDatabase();
addTearDown(db.close);
final repo = newTestRepository(db);
final tx = _CapturingTransport();
final service = serviceFor(db, tx);
await tester.pumpWidget(host(service, repo));
await openSheet(tester);
expect(find.byKey(const Key('propose.seed')), findsOneWidget);
await tester.enterText(
find.byKey(const Key('propose.seed')), 'Tomate rosa');
await tester.pump();
await tapSend(tester);
expect(tx.proposed, hasLength(1));
expect(tx.proposed.single.label, 'Tomate rosa');
// Default direction is owedToMe → I'm the creditor and signed my stub.
expect(tx.proposed.single.creditorSignature, isNotNull);
expect(tx.proposed.single.debtorSignature, isNull);
// The local row is recorded as proposed AND tied to a real Variety — a new
// name created one on send.
final row = await repo.plantareByPledgeId(tx.proposed.single.pledgeId);
expect(row!.remoteState, PlantareRemoteState.proposed);
expect(row.varietyId, isNotNull);
final linked = await repo.varietyLabels();
expect(linked.map((v) => v.id), contains(row.varietyId));
await service.stop();
});
testWidgets('typing an existing seed links the pledge to that variety',
(tester) async {
LocaleSettings.setLocaleSync(AppLocale.en);
await useTallSurface(tester);
final db = newTestDatabase();
addTearDown(db.close);
final repo = newTestRepository(db);
final existingId = await repo.addQuickVariety(label: 'Maíz rojo');
final tx = _CapturingTransport();
final service = serviceFor(db, tx);
await tester.pumpWidget(host(service, repo));
await openSheet(tester);
// A matching name resolves to the existing variety — no duplicate created.
await tester.enterText(find.byKey(const Key('propose.seed')), 'Maíz rojo');
await tester.pump();
await tapSend(tester);
expect(tx.proposed, hasLength(1));
final row = await repo.plantareByPledgeId(tx.proposed.single.pledgeId);
expect(row!.varietyId, existingId); // linked, not duplicated
expect((await repo.varietyLabels()).length, 1);
await service.stop();
});
}