test(fix): stop handover widget test hanging on awaited stream .first
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.
This commit is contained in:
parent
b22db7d0cb
commit
d42ed0df1a
2 changed files with 64 additions and 2 deletions
|
|
@ -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 = <String>[];
|
||||||
|
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')}',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -84,13 +84,21 @@ void main() {
|
||||||
await tester.tap(find.byKey(const Key('handover.save')));
|
await tester.tap(find.byKey(const Key('handover.save')));
|
||||||
await tester.pumpAndSettle();
|
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.direction, SaleDirection.iSold);
|
||||||
expect(sales.single.amount, 2.5);
|
expect(sales.single.amount, 2.5);
|
||||||
expect(sales.single.currency, 'Ğ1');
|
expect(sales.single.currency, 'Ğ1');
|
||||||
expect(sales.single.counterparty, 'María');
|
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.direction, PlantareDirection.owedToMe);
|
||||||
expect(plantares.single.owedDescription, 'un puñado');
|
expect(plantares.single.owedDescription, 'un puñado');
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue