From d51d3d70570e479b51bf1793f1169460a64e1253 Mon Sep 17 00:00:00 2001 From: vjrj Date: Sun, 12 Jul 2026 07:54:35 +0200 Subject: [PATCH] test(fix): stop handover widget test hanging on awaited stream .first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'giving all with payment and promise' widget test awaited repo.watchSales().first / watchPlantares().first in its body. A Drift stream's .first never completes under the widget-test fake-async clock (pumpAndSettle has already returned, so dart_test.yaml's per-test timeout can't bound it), so the test hung to the framework's 10-min hard cap. - Read the recorded sale/plantare with one-shot db.select(...).get() queries instead (same filter, resolves fine under fake-async) — the suite now passes in ~1s. - Add a pure-Dart guard test that scans test/ui/ and fails loudly if any awaited watchX().first reappears, so this stops being recurrent. --- .../no_stream_first_in_widget_tests_test.dart | 54 +++++++++++++++++++ .../test/ui/handover_sheet_test.dart | 12 ++++- 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 apps/app_seeds/test/no_stream_first_in_widget_tests_test.dart diff --git a/apps/app_seeds/test/no_stream_first_in_widget_tests_test.dart b/apps/app_seeds/test/no_stream_first_in_widget_tests_test.dart new file mode 100644 index 0000000..b499751 --- /dev/null +++ b/apps/app_seeds/test/no_stream_first_in_widget_tests_test.dart @@ -0,0 +1,54 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +/// Guard against the single most reliable way to make a widget test hang. +/// +/// Awaiting a Drift/Nostr stream's `.first` (e.g. `await repo.watchSales() +/// .first`) inside a `testWidgets` body never completes: the widget-test +/// fake-async clock does not advance the stream's backing timer, so the test +/// sits until the framework's 10-minute hard cap — the per-test `timeout:` in +/// dart_test.yaml can't bound it because `pumpAndSettle` has already returned. +/// It has bitten this repo more than once. Read one-shot state with a plain +/// `.get()` / `.getSingle()` query instead; those resolve fine under fake-async. +/// +/// This is a pure-Dart source scan (no widgets, milliseconds) so it runs in the +/// fast, hang-proof gate and fails loudly the moment the pattern reappears. +void main() { + test('no `watchX().first` awaited inside widget tests', () { + // `watchAnything(...)` immediately followed by `.first` — catches + // watchSales().first, watchPlantares().first, watchInventoryForVariety(id) + // .first, etc. Finder.first (find.byIcon(...).first) has no `watch(` before + // it, so it is never matched. + final streamFirst = RegExp(r'watch\w*\([^;]*\)\s*\.first\b'); + + final uiTests = Directory('test/ui'); + expect( + uiTests.existsSync(), + isTrue, + reason: 'expected widget tests under test/ui/', + ); + + final offenders = []; + for (final entity in uiTests.listSync(recursive: true)) { + if (entity is! File || !entity.path.endsWith('.dart')) continue; + final lines = entity.readAsLinesSync(); + for (var i = 0; i < lines.length; i++) { + final trimmed = lines[i].trimLeft(); + if (trimmed.startsWith('//') || trimmed.startsWith('*')) continue; + if (streamFirst.hasMatch(lines[i])) { + offenders.add('${entity.path}:${i + 1}: ${lines[i].trim()}'); + } + } + } + + expect( + offenders, + isEmpty, + reason: + 'Awaiting a stream .first inside a widget test hangs it to the 10-min ' + 'cap. Replace with a one-shot query (db.select(...).get()):\n' + '${offenders.join('\n')}', + ); + }); +} diff --git a/apps/app_seeds/test/ui/handover_sheet_test.dart b/apps/app_seeds/test/ui/handover_sheet_test.dart index d47f1cf..f00214a 100644 --- a/apps/app_seeds/test/ui/handover_sheet_test.dart +++ b/apps/app_seeds/test/ui/handover_sheet_test.dart @@ -84,13 +84,21 @@ void main() { await tester.tap(find.byKey(const Key('handover.save'))); await tester.pumpAndSettle(); - final sales = await repo.watchSales().first; + // One-shot .get() reads (NOT watch().first): a Drift stream's `.first` + // never completes under the widget-test fake-async clock, so awaiting it + // in a testWidgets body hangs the whole test to the 10-min hard cap. A + // plain SELECT resolves fine (same as the movements/lot reads below). + final sales = await (db.select( + db.sales, + )..where((s) => s.isDeleted.equals(false))).get(); expect(sales.single.direction, SaleDirection.iSold); expect(sales.single.amount, 2.5); expect(sales.single.currency, 'Ğ1'); expect(sales.single.counterparty, 'María'); - final plantares = await repo.watchPlantares().first; + final plantares = await (db.select( + db.plantares, + )..where((p) => p.isDeleted.equals(false))).get(); expect(plantares.single.direction, PlantareDirection.owedToMe); expect(plantares.single.owedDescription, 'un puñado');