import 'package:pipe/pipe.dart'; import 'package:test/test.dart'; const String fixturePath = 'test/fixtures/stuck_loading'; void main() { late ProjectAnalysis analysis; setUpAll(() async { final project = await loadProject(fixturePath); analysis = await analyzeProject(project, concurrency: 1); }); tearDownAll(() async { await analysis.dispose(); }); test('discovers Riverpod providers and their value types', () { final names = analysis.flow.riverpod.providers.map((p) => p.name).toSet(); expect( names, containsAll(['unhandledProvider', 'handledProvider']), ); final unhandled = analysis.flow.riverpod.providers.firstWhere( (p) => p.name == 'unhandledProvider', ); expect(unhandled.kind, 'FutureProvider'); expect(unhandled.isAsync, isTrue); expect(unhandled.valueType, 'UnhandledDoc'); }); test('records watch sites with their AsyncValue handling', () { final sites = analysis.flow.riverpod.sitesFor('unhandledProvider'); expect(sites, hasLength(1)); expect(sites.single.watches, isTrue); expect(sites.single.readsValue, isTrue); expect(sites.single.handlesError, isFalse); expect(sites.single.handlesLoading, isFalse); }); test('reports the unhandled provider and not the handled one', () { final hits = analysis.report.findings .where((f) => f.rule == ruleRiverpodAsyncGap) .toList(); // The two deliberate gaps: a direct read and a read through a variable. expect(hits.map((f) => f.data['provider']).toSet(), { 'unhandledProvider', 'storedUnhandledProvider', }); expect(hits, hasLength(2)); final unhandled = hits.singleWhere( (f) => f.data['provider'] == 'unhandledProvider', ); expect( unhandled.data['missing'], containsAll(['error', 'loading']), ); // A non-bool value type keeps this at high confidence and warning severity. expect(unhandled.confidence, 'high'); expect(unhandled.severity, PipeSeverity.warning); expect(unhandled.evidence, isNotEmpty); expect(unhandled.remediation, isNotNull); }); test('the handled provider produces no gap', () { final providers = analysis.report.findings .where((f) => f.rule == ruleRiverpodAsyncGap) .map((f) => f.data['provider']) .toSet(); expect(providers, isNot(contains('handledProvider'))); expect( analysis.flow.riverpod.gaps.map((g) => g.provider.name), isNot(contains('handledProvider')), ); }); test('a provider consumed only by ref.listen is not a render gap', () { // A listener mirrors the value into a side effect and draws nothing, so // it cannot leave a blank or stale screen. Seen when dogfooding on // word_alive: an analytics user-property listener was reported as a UI // read with unhandled loading and error. final sites = analysis.flow.riverpod.sitesFor('listenOnlyProvider'); expect(sites, hasLength(1)); expect(sites.single.watches, isFalse); expect(sites.single.readsValue, isTrue); expect( analysis.flow.riverpod.gaps.map((g) => g.provider.name), isNot(contains('listenOnlyProvider')), ); }); }