import 'dart:async'; import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:reelbox/media/download_service.dart'; import 'package:reelbox/media/downloads_page.dart'; import 'package:reelbox/state/app_state.dart'; import 'catalog_widget_test.dart' show CatalogApi, mountCatalog; Future mountRow(WidgetTester tester, Widget row) async { final state = await mountCatalog(tester, CatalogApi(), const Scaffold()); await tester.pumpWidget( AppScope( state: state, child: MaterialApp(home: Scaffold(body: row)), ), ); await tester.pump(); return state; } void main() { testWidgets('secondary external downloaders live behind the quiet overflow', ( tester, ) async { final selected = []; final state = await mountRow( tester, ExternalDownloadMenu(onSelected: selected.add), ); addTearDown(state.dispose); expect(find.byIcon(Icons.more_vert), findsOneWidget); expect(find.byIcon(Icons.download_for_offline_outlined), findsNothing); expect(find.text('ADM'), findsNothing); expect(selected, isEmpty); await tester.tap(find.byIcon(Icons.more_vert)); await tester.pumpAndSettle(); expect(find.text('External downloader: ADM'), findsOneWidget); expect(find.text('External downloader: IDM'), findsOneWidget); await tester.tap(find.text('External downloader: ADM')); await tester.pumpAndSettle(); expect(selected, ['ADM']); }); for (final status in [TaskStatus.paused, TaskStatus.failed]) { testWidgets( '${status.name} exposes its labeled recovery action and readable failures', (tester) async { var attempts = 0; final entry = DownloadEntry.fromTask( DownloadTask(url: 'https://media.example/movie.mp4'), taskStatus: status, ); Future recover() async { attempts++; throw StateError( 'Cannot resume: this server does not support ranges.', ); } final state = await mountRow( tester, DownloadControls( entry: entry, onPause: () async {}, onResume: recover, onRetry: recover, onCancel: () async {}, ), ); addTearDown(state.dispose); final label = status == TaskStatus.paused ? 'Resume' : 'Retry'; expect(find.widgetWithText(FilledButton, label), findsOneWidget); await tester.tap(find.text(label)); await tester.pumpAndSettle(); expect(attempts, 1); expect( find.text('Cannot resume: this server does not support ranges.'), findsOneWidget, ); expect(tester.takeException(), isNull); }, ); } testWidgets( 'failed unknown-length transfer stops animating and shows its actual cause', (tester) async { final entry = DownloadEntry.fromTask( DownloadTask(url: 'https://media.example/movie.mp4'), taskStatus: TaskStatus.failed, error: 'Connection refused by media server', ); final state = await mountRow( tester, DownloadTransferDetails(entry: entry), ); addTearDown(state.dispose); expect( tester .widget( find.byType(LinearProgressIndicator), ) .value, 0, ); expect(find.textContaining('%'), findsNothing); expect(find.text('Connection refused by media server'), findsOneWidget); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); }, ); testWidgets( 'download controls are labeled and dispatch once while an operation is pending', (tester) async { final pending = Completer(); var pauses = 0; var cancels = 0; final entry = DownloadEntry.fromTask( DownloadTask(url: 'https://media.example/movie.mp4'), taskStatus: TaskStatus.running, ); final state = await mountRow( tester, DownloadControls( entry: entry, onPause: () { pauses++; return pending.future; }, onResume: () async {}, onRetry: () async {}, onCancel: () async { cancels++; }, ), ); addTearDown(state.dispose); expect(find.widgetWithText(FilledButton, 'Pause'), findsOneWidget); expect(find.widgetWithText(OutlinedButton, 'Cancel'), findsOneWidget); await tester.tap(find.text('Pause')); await tester.tap(find.text('Pause')); await tester.pump(); expect(pauses, 1); expect( tester .widget( find.widgetWithText(OutlinedButton, 'Cancel'), ) .onPressed, isNull, ); pending.complete(); await tester.pumpAndSettle(); await tester.tap(find.text('Cancel')); await tester.pumpAndSettle(); expect(cancels, 1); expect(tester.takeException(), isNull); }, ); for (final status in { TaskStatus.enqueued: 'Queued', TaskStatus.running: 'Downloading', TaskStatus.waitingToRetry: 'Waiting to retry', TaskStatus.paused: 'Paused', TaskStatus.complete: 'Downloaded', TaskStatus.failed: 'Failed', TaskStatus.canceled: 'Canceled', TaskStatus.notFound: 'Source not found', }.entries) { testWidgets( '${status.key.name} has a readable status and preserves measured progress', (tester) async { final entry = DownloadEntry.fromTask( DownloadTask(url: 'https://media.example/movie.mp4'), taskStatus: status.key, progress: .25, totalBytes: 4096, speed: 2048, error: status.key == TaskStatus.failed ? 'HTTP 403: expired URL' : '', ); final state = await mountRow( tester, DownloadTransferDetails(entry: entry), ); addTearDown(state.dispose); expect(find.text(status.value), findsOneWidget); expect( tester .widget( find.byType(LinearProgressIndicator), ) .value, status.key == TaskStatus.complete ? 1 : .25, ); expect( find.textContaining('/s'), status.key == TaskStatus.running ? findsOneWidget : findsNothing, ); if (status.key == TaskStatus.failed) { expect(find.text('HTTP 403: expired URL'), findsOneWidget); } expect(tester.takeException(), isNull); }, ); } testWidgets( 'queued Wi-Fi download shows its constraint without invented progress', (tester) async { final entry = DownloadEntry.fromTask( DownloadTask( url: 'https://media.example/movie.mp4', requiresWiFi: true, ), ); final state = await mountRow( tester, DownloadTransferDetails(entry: entry), ); addTearDown(state.dispose); expect(find.text('Queued ยท Wi-Fi required'), findsOneWidget); expect(find.textContaining('0%'), findsNothing); expect(find.textContaining('/s'), findsNothing); expect( tester .widget( find.byType(LinearProgressIndicator), ) .value, isNull, ); expect(tester.takeException(), isNull); }, ); }