import 'package:pipe/pipe.dart'; import 'package:test/test.dart'; const String fixturePath = 'test/fixtures/stuck_loading'; void main() { late Project project; late ResolutionRun run; late StateFlow flow; late FindingReport report; setUpAll(() async { project = await loadProject(fixturePath); run = await resolveProject(project, concurrency: 1); flow = buildStateFlow(project, run.outcomes); report = runChecks(flow: flow); }); tearDownAll(() async { await run.dispose(); }); test('fixture resolves with zero analyzer errors', () { final failures = run.outcomes.where((o) => !o.resolved).toList(); expect( failures, isEmpty, reason: failures.map((f) => '${f.path}: ${f.failure}').join('\n'), ); // Resolution succeeding is not the same as the fixture being valid: a unit // can resolve while containing compile errors, which would silently distort // the IR every rule test depends on. `dart analyze` cannot catch this because // test/fixtures/** is excluded from the repo's analysis_options, so it is // asserted here from the analyzer's own diagnostics. final errors = []; for (final outcome in run.outcomes) { for (final diagnostic in outcome.diagnostics) { if (diagnostic.severity.name != 'error') continue; errors.add( '${outcome.path}:${outcome.unit?.lineInfo.getLocation(diagnostic.offset).lineNumber} ' '${diagnostic.diagnosticCode.lowerCaseName}: ${diagnostic.message}', ); } } expect(errors, isEmpty, reason: errors.join('\n')); }); test('builds the state union and its variants', () { final union = flow.unions.firstWhere((u) => u.name == 'ItemState'); expect(union.owner, 'ItemBloc'); expect( union.variants.map((v) => v.name), containsAll(['ItemLoading', 'ItemLoaded', 'ItemError']), ); expect(union.style, UnionStyle.abstract); }); test('loading-not-reset fires on the swallowed loading path', () { final hits = report.findings .where((finding) => finding.rule == ruleLoadingNotReset) .toList(); expect(hits, isNotEmpty); // Every firing site is in the fixture's bad handlers/catches, and all of // them are high confidence warnings. expect( hits.every((f) => f.location.contains('item_bloc.dart')), isTrue, reason: 'unexpected firing site: ${hits.map((f) => f.location).toList()}', ); expect(hits.every((f) => f.confidence == 'high'), isTrue); expect(hits.every((f) => f.severity == PipeSeverity.warning), isTrue); }); test('does not fire where the catch resolves the loading state', () { // GuardedCubit emits a loading variant and awaits inside a try whose catch // emits a terminal state, so reporting it would be a false positive. This is // the precise form of the old "only one finding" assertion, which stopped // proving anything once the fixture gained a second bad site. final owners = report.findings .where((f) => f.rule == ruleLoadingNotReset) .map((f) => f.data['owner']) .toSet(); expect(owners, isNot(contains('GuardedCubit'))); expect(owners, contains('ItemBloc')); }); test('emitted-not-rendered reports variants no UI consumes', () { final hits = report.findings .where((finding) => finding.rule == ruleEmittedNotRendered) .toList(); expect(hits, isNotEmpty); expect(hits.map((f) => f.data['union']).toSet(), contains('ItemState')); // ItemState has no UI observing it, so those findings are high confidence. final itemState = hits.where( (finding) => finding.data['union'] == 'ItemState', ); expect(itemState, isNotEmpty); expect(itemState.every((f) => f.confidence == 'high'), isTrue); }); test('event-without-handler catches a dispatch with no registration', () { final hits = report.findings .where((finding) => finding.rule == ruleEventWithoutHandler) .toList(); // ItemBloc registers handlers for both of its events, and SyncBloc/UploadBloc // cover their other events through base and group handlers, so the only // reports must be the two deliberate gaps. expect( {for (final hit in hits) '${hit.data['owner']}.${hit.data['event']}'}, {'GapBloc.UnhandledTap', 'UploadBloc.UploadAbandoned'}, ); expect(hits, hasLength(2)); }); test('every finding carries evidence', () { for (final finding in report.findings) { expect(finding.evidence, isNotEmpty, reason: finding.message); expect(finding.remediation, isNotNull, reason: finding.message); expect(finding.file, isNotNull, reason: finding.message); } }); }