From 4cc50abdccf99c8948f9a9cb39ca85a5ceb8a378 Mon Sep 17 00:00:00 2001 From: Phan An Date: Sun, 12 Jul 2026 21:17:11 +0200 Subject: [PATCH 1/2] Give the loading screen an escape hatch on weak connections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The data-loading screen showed an indefinite spinner while GET data ran with no timeout, so a weak connection could hang it forever with no feedback or way out. Now: a 30s timeout falls through to the existing retry/log-out box; after 6s of loading a "This is taking longer than usual…" message appears; and when the user has downloaded songs, a "View Downloads" button drops them into the offline library instead of waiting. --- lib/ui/screens/data_loading.dart | 93 +++++++- test/ui/screens/data_loading_test.dart | 100 +++++++++ test/ui/screens/data_loading_test.mocks.dart | 217 +++++++++++++++++++ 3 files changed, 399 insertions(+), 11 deletions(-) create mode 100644 test/ui/screens/data_loading_test.dart create mode 100644 test/ui/screens/data_loading_test.mocks.dart diff --git a/lib/ui/screens/data_loading.dart b/lib/ui/screens/data_loading.dart index 3466596b..63f3eae1 100644 --- a/lib/ui/screens/data_loading.dart +++ b/lib/ui/screens/data_loading.dart @@ -1,3 +1,7 @@ +import 'dart:async'; + +import 'package:app/app_state.dart'; +import 'package:app/enums.dart'; import 'package:app/providers/providers.dart'; import 'package:app/ui/screens/screens.dart'; import 'package:app/ui/widgets/widgets.dart'; @@ -14,7 +18,13 @@ class DataLoadingScreen extends StatefulWidget { } class _DataLoadingScreen extends State { + static const _stillLoadingAfter = Duration(seconds: 6); + static const _loadTimeout = Duration(seconds: 30); + var _hasError = false; + var _stillLoading = false; + Timer? _stillLoadingTimer; + Timer? _timeoutTimer; @override void initState() { @@ -22,29 +32,90 @@ class _DataLoadingScreen extends State { _loadData(); } + @override + void dispose() { + _cancelTimers(); + super.dispose(); + } + + void _cancelTimers() { + _stillLoadingTimer?.cancel(); + _timeoutTimer?.cancel(); + } + Future _loadData() async { + _stillLoadingTimer = Timer(_stillLoadingAfter, () { + if (mounted) setState(() => _stillLoading = true); + }); + _timeoutTimer = Timer(_loadTimeout, () { + if (mounted) setState(() => _hasError = true); + }); + try { await context.read().init(); - await Navigator.of(context).pushReplacementNamed(MainScreen.routeName); + if (!mounted || _hasError) return; + _cancelTimers(); + Navigator.of(context).pushReplacementNamed(MainScreen.routeName); } catch (e) { - print(e); - setState(() => _hasError = true); + _cancelTimers(); + if (mounted) setState(() => _hasError = true); } } + void _retry() { + _cancelTimers(); + setState(() { + _hasError = false; + _stillLoading = false; + }); + _loadData(); + } + + bool get _hasDownloads => + context.read().playables.isNotEmpty; + + void _viewDownloads() { + _cancelTimers(); + AppState.set('mode', AppMode.offline); + Navigator.of(context).pushReplacementNamed(MainScreen.routeName); + } + @override Widget build(BuildContext context) { return Scaffold( body: GradientDecoratedContainer( child: _hasError - ? OopsBox( - showLogOutButton: true, - onRetry: () { - setState(() => _hasError = false); - _loadData(); - }, - ) - : const ContainerWithSpinner(), + ? OopsBox(showLogOutButton: true, onRetry: _retry) + : _buildLoading(), + ), + ); + } + + Widget _buildLoading() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Spinner(), + if (_stillLoading) ...[ + const SizedBox(height: 28), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 40), + child: Text( + 'This is taking longer than usual…', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white70, fontSize: 15), + ), + ), + if (_hasDownloads) ...[ + const SizedBox(height: 20), + ElevatedButton( + onPressed: _viewDownloads, + child: const Text('View Downloads'), + ), + ], + ], + ], ), ); } diff --git a/test/ui/screens/data_loading_test.dart b/test/ui/screens/data_loading_test.dart new file mode 100644 index 00000000..c85dafd4 --- /dev/null +++ b/test/ui/screens/data_loading_test.dart @@ -0,0 +1,100 @@ +import 'dart:async'; + +import 'package:app/app_state.dart'; +import 'package:app/enums.dart'; +import 'package:app/models/models.dart'; +import 'package:app/providers/data_provider.dart'; +import 'package:app/providers/download_provider.dart'; +import 'package:app/ui/screens/data_loading.dart'; +import 'package:app/ui/screens/main.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:provider/provider.dart'; + +import '../../extensions/widget_tester_extension.dart'; +import 'data_loading_test.mocks.dart'; + +@GenerateMocks([DataProvider, DownloadProvider]) +void main() { + late MockDataProvider dataProvider; + late MockDownloadProvider downloadProvider; + late Completer initCompleter; + + setUp(() { + AppState.clear(); + dataProvider = MockDataProvider(); + downloadProvider = MockDownloadProvider(); + initCompleter = Completer(); + when(dataProvider.init()).thenAnswer((_) => initCompleter.future); + }); + + Future mount( + WidgetTester tester, { + required List downloads, + }) async { + when(downloadProvider.playables).thenReturn(downloads); + + await tester.pumpAppWidget( + MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: dataProvider), + Provider.value(value: downloadProvider), + ], + child: const DataLoadingScreen(), + ), + routes: {MainScreen.routeName: (_) => const Text('MAIN')}, + ); + } + + // Completes the still-hanging load so no timers outlive the test. + Future settle(WidgetTester tester) async { + if (!initCompleter.isCompleted) initCompleter.complete(); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + } + + testWidgets('does not nag while the load is still fresh', (tester) async { + await mount(tester, downloads: []); + await tester.pump(); + + expect(find.text('This is taking longer than usual…'), findsNothing); + + await settle(tester); + }); + + testWidgets( + 'shows the still-loading message after a delay, without a downloads button', + (tester) async { + await mount(tester, downloads: []); + await tester.pump(); + await tester.pump(const Duration(seconds: 7)); + + expect(find.text('This is taking longer than usual…'), findsOneWidget); + expect(find.text('View Downloads'), findsNothing); + + await settle(tester); + }, + ); + + testWidgets( + 'offers View Downloads after a delay and enters offline mode', + (tester) async { + await mount(tester, downloads: [Song.fake()]); + await tester.pump(); + await tester.pump(const Duration(seconds: 7)); + + expect(find.text('View Downloads'), findsOneWidget); + + await tester.tap(find.text('View Downloads')); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + expect(find.text('MAIN'), findsOneWidget); + expect(AppState.get('mode'), AppMode.offline); + + await settle(tester); + }, + ); +} diff --git a/test/ui/screens/data_loading_test.mocks.dart b/test/ui/screens/data_loading_test.mocks.dart new file mode 100644 index 00000000..69d4e98f --- /dev/null +++ b/test/ui/screens/data_loading_test.mocks.dart @@ -0,0 +1,217 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in app/test/ui/screens/data_loading_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; +import 'dart:ui' as _i4; + +import 'package:app/models/models.dart' as _i5; +import 'package:app/providers/providers.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i6; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [DataProvider]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDataProvider extends _i1.Mock implements _i2.DataProvider { + MockDataProvider() { + _i1.throwOnMissingStub(this); + } + + @override + bool get hasListeners => (super.noSuchMethod( + Invocation.getter(#hasListeners), + returnValue: false, + ) as bool); + + @override + _i3.Future init() => (super.noSuchMethod( + Invocation.method( + #init, + [], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + void addListener(_i4.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #addListener, + [listener], + ), + returnValueForMissingStub: null, + ); + + @override + void removeListener(_i4.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #removeListener, + [listener], + ), + returnValueForMissingStub: null, + ); + + @override + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void notifyListeners() => super.noSuchMethod( + Invocation.method( + #notifyListeners, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [DownloadProvider]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDownloadProvider extends _i1.Mock implements _i2.DownloadProvider { + MockDownloadProvider() { + _i1.throwOnMissingStub(this); + } + + @override + List<_i5.Playable> get playables => (super.noSuchMethod( + Invocation.getter(#playables), + returnValue: <_i5.Playable>[], + ) as List<_i5.Playable>); + + @override + _i3.Stream get downloadsClearedStream => (super.noSuchMethod( + Invocation.getter(#downloadsClearedStream), + returnValue: _i3.Stream.empty(), + ) as _i3.Stream); + + @override + _i3.Stream<_i5.Playable> get downloadRemovedStream => + (super.noSuchMethod( + Invocation.getter(#downloadRemovedStream), + returnValue: _i3.Stream<_i5.Playable>.empty(), + ) as _i3.Stream<_i5.Playable>); + + @override + _i3.Stream<_i2.Download> get playableDownloadedStream => (super.noSuchMethod( + Invocation.getter(#playableDownloadedStream), + returnValue: _i3.Stream<_i2.Download>.empty(), + ) as _i3.Stream<_i2.Download>); + + @override + _i3.Future get downloadsDir => (super.noSuchMethod( + Invocation.getter(#downloadsDir), + returnValue: _i3.Future.value(_i6.dummyValue( + this, + Invocation.getter(#downloadsDir), + )), + ) as _i3.Future); + + @override + _i3.Future download({required _i5.Playable? playable}) => + (super.noSuchMethod( + Invocation.method( + #download, + [], + {#playable: playable}, + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i2.Download? getForPlayable(_i5.Playable? playable) => + (super.noSuchMethod(Invocation.method( + #getForPlayable, + [playable], + )) as _i2.Download?); + + @override + bool has({required _i5.Playable? playable}) => (super.noSuchMethod( + Invocation.method( + #has, + [], + {#playable: playable}, + ), + returnValue: false, + ) as bool); + + @override + void persistMetadata() => super.noSuchMethod( + Invocation.method( + #persistMetadata, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void persistMetadataIfNeeded(_i5.Playable? playable) => + super.noSuchMethod( + Invocation.method( + #persistMetadataIfNeeded, + [playable], + ), + returnValueForMissingStub: null, + ); + + @override + _i3.Future removeForPlayable(_i5.Playable? playable) => + (super.noSuchMethod( + Invocation.method( + #removeForPlayable, + [playable], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future clear() => (super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + void unsubscribeAll() => super.noSuchMethod( + Invocation.method( + #unsubscribeAll, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void subscribe(_i3.StreamSubscription? sub) => super.noSuchMethod( + Invocation.method( + #subscribe, + [sub], + ), + returnValueForMissingStub: null, + ); +} From 6628a19c613eb6b78c240f6dd266b3b2272cf5ab Mon Sep 17 00:00:00 2001 From: Phan An Date: Sun, 12 Jul 2026 21:27:56 +0200 Subject: [PATCH 2/2] Ignore superseded loads; cover the fallback paths Address review: a load attempt now carries a generation so a stale init() future (e.g. one that resolves after the timeout fired and the user hit Retry) can't navigate or re-error over a newer attempt. Also use catch (_), and add tests for the timeout, failure, and retry paths (including the stale-future case). --- lib/ui/screens/data_loading.dart | 10 +- test/ui/screens/data_loading_test.dart | 60 ++++- test/ui/screens/data_loading_test.mocks.dart | 217 +++++++++++++++---- 3 files changed, 234 insertions(+), 53 deletions(-) diff --git a/lib/ui/screens/data_loading.dart b/lib/ui/screens/data_loading.dart index 63f3eae1..fd776f50 100644 --- a/lib/ui/screens/data_loading.dart +++ b/lib/ui/screens/data_loading.dart @@ -23,6 +23,7 @@ class _DataLoadingScreen extends State { var _hasError = false; var _stillLoading = false; + var _loadGeneration = 0; Timer? _stillLoadingTimer; Timer? _timeoutTimer; @@ -44,6 +45,8 @@ class _DataLoadingScreen extends State { } Future _loadData() async { + final generation = ++_loadGeneration; + _stillLoadingTimer = Timer(_stillLoadingAfter, () { if (mounted) setState(() => _stillLoading = true); }); @@ -53,10 +56,13 @@ class _DataLoadingScreen extends State { try { await context.read().init(); - if (!mounted || _hasError) return; + // Ignore a superseded attempt (e.g. a stale future resolving after the + // timeout fired and the user hit Retry). + if (!mounted || _hasError || generation != _loadGeneration) return; _cancelTimers(); Navigator.of(context).pushReplacementNamed(MainScreen.routeName); - } catch (e) { + } catch (_) { + if (generation != _loadGeneration) return; _cancelTimers(); if (mounted) setState(() => _hasError = true); } diff --git a/test/ui/screens/data_loading_test.dart b/test/ui/screens/data_loading_test.dart index c85dafd4..5530196e 100644 --- a/test/ui/screens/data_loading_test.dart +++ b/test/ui/screens/data_loading_test.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:app/app_state.dart'; import 'package:app/enums.dart'; import 'package:app/models/models.dart'; +import 'package:app/providers/auth_provider.dart'; import 'package:app/providers/data_provider.dart'; import 'package:app/providers/download_provider.dart'; import 'package:app/ui/screens/data_loading.dart'; @@ -16,23 +17,25 @@ import 'package:provider/provider.dart'; import '../../extensions/widget_tester_extension.dart'; import 'data_loading_test.mocks.dart'; -@GenerateMocks([DataProvider, DownloadProvider]) +@GenerateMocks([DataProvider, DownloadProvider, AuthProvider]) void main() { late MockDataProvider dataProvider; late MockDownloadProvider downloadProvider; + late MockAuthProvider authProvider; late Completer initCompleter; setUp(() { AppState.clear(); dataProvider = MockDataProvider(); downloadProvider = MockDownloadProvider(); + authProvider = MockAuthProvider(); initCompleter = Completer(); when(dataProvider.init()).thenAnswer((_) => initCompleter.future); }); Future mount( WidgetTester tester, { - required List downloads, + List downloads = const [], }) async { when(downloadProvider.playables).thenReturn(downloads); @@ -41,11 +44,13 @@ void main() { providers: [ ChangeNotifierProvider.value(value: dataProvider), Provider.value(value: downloadProvider), + Provider.value(value: authProvider), ], child: const DataLoadingScreen(), ), routes: {MainScreen.routeName: (_) => const Text('MAIN')}, ); + await tester.pump(); } // Completes the still-hanging load so no timers outlive the test. @@ -56,8 +61,7 @@ void main() { } testWidgets('does not nag while the load is still fresh', (tester) async { - await mount(tester, downloads: []); - await tester.pump(); + await mount(tester); expect(find.text('This is taking longer than usual…'), findsNothing); @@ -67,8 +71,7 @@ void main() { testWidgets( 'shows the still-loading message after a delay, without a downloads button', (tester) async { - await mount(tester, downloads: []); - await tester.pump(); + await mount(tester); await tester.pump(const Duration(seconds: 7)); expect(find.text('This is taking longer than usual…'), findsOneWidget); @@ -82,7 +85,6 @@ void main() { 'offers View Downloads after a delay and enters offline mode', (tester) async { await mount(tester, downloads: [Song.fake()]); - await tester.pump(); await tester.pump(const Duration(seconds: 7)); expect(find.text('View Downloads'), findsOneWidget); @@ -97,4 +99,48 @@ void main() { await settle(tester); }, ); + + testWidgets('falls back to the error box after the load times out', + (tester) async { + await mount(tester); + await tester.pump(const Duration(seconds: 30)); + + expect(find.text('Oops!'), findsOneWidget); + expect(find.text('Retry'), findsOneWidget); + expect(find.text('Log Out'), findsOneWidget); + + await settle(tester); + }); + + testWidgets('shows the error box when the load fails', (tester) async { + when(dataProvider.init()).thenAnswer((_) async => throw Exception('boom')); + + await mount(tester); + + expect(find.text('Oops!'), findsOneWidget); + }); + + testWidgets('a stale load cannot clobber a retry', (tester) async { + await mount(tester); + await tester.pump(const Duration(seconds: 30)); + expect(find.text('Oops!'), findsOneWidget); + + // Retry with a fresh, still-pending load. + final retryCompleter = Completer(); + when(dataProvider.init()).thenAnswer((_) => retryCompleter.future); + await tester.tap(find.text('Retry')); + await tester.pump(); + expect(find.text('Oops!'), findsNothing); + + // The stale first attempt resolves late — it must be ignored. + initCompleter.complete(); + await tester.pump(); + expect(find.text('MAIN'), findsNothing); + + // The retry attempt resolves — it navigates. + retryCompleter.complete(); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + expect(find.text('MAIN'), findsOneWidget); + }); } diff --git a/test/ui/screens/data_loading_test.mocks.dart b/test/ui/screens/data_loading_test.mocks.dart index 69d4e98f..855248ce 100644 --- a/test/ui/screens/data_loading_test.mocks.dart +++ b/test/ui/screens/data_loading_test.mocks.dart @@ -3,11 +3,11 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i3; -import 'dart:ui' as _i4; +import 'dart:async' as _i4; +import 'dart:ui' as _i5; -import 'package:app/models/models.dart' as _i5; -import 'package:app/providers/providers.dart' as _i2; +import 'package:app/models/models.dart' as _i2; +import 'package:app/providers/providers.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i6; @@ -25,10 +25,20 @@ import 'package:mockito/src/dummies.dart' as _i6; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +class _FakeUser_0 extends _i1.SmartFake implements _i2.User { + _FakeUser_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [DataProvider]. /// /// See the documentation for Mockito's code generation for more information. -class MockDataProvider extends _i1.Mock implements _i2.DataProvider { +class MockDataProvider extends _i1.Mock implements _i3.DataProvider { MockDataProvider() { _i1.throwOnMissingStub(this); } @@ -40,17 +50,17 @@ class MockDataProvider extends _i1.Mock implements _i2.DataProvider { ) as bool); @override - _i3.Future init() => (super.noSuchMethod( + _i4.Future init() => (super.noSuchMethod( Invocation.method( #init, [], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - void addListener(_i4.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i5.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -59,7 +69,7 @@ class MockDataProvider extends _i1.Mock implements _i2.DataProvider { ); @override - void removeListener(_i4.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i5.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -89,66 +99,66 @@ class MockDataProvider extends _i1.Mock implements _i2.DataProvider { /// A class which mocks [DownloadProvider]. /// /// See the documentation for Mockito's code generation for more information. -class MockDownloadProvider extends _i1.Mock implements _i2.DownloadProvider { +class MockDownloadProvider extends _i1.Mock implements _i3.DownloadProvider { MockDownloadProvider() { _i1.throwOnMissingStub(this); } @override - List<_i5.Playable> get playables => (super.noSuchMethod( + List<_i2.Playable> get playables => (super.noSuchMethod( Invocation.getter(#playables), - returnValue: <_i5.Playable>[], - ) as List<_i5.Playable>); + returnValue: <_i2.Playable>[], + ) as List<_i2.Playable>); @override - _i3.Stream get downloadsClearedStream => (super.noSuchMethod( + _i4.Stream get downloadsClearedStream => (super.noSuchMethod( Invocation.getter(#downloadsClearedStream), - returnValue: _i3.Stream.empty(), - ) as _i3.Stream); + returnValue: _i4.Stream.empty(), + ) as _i4.Stream); @override - _i3.Stream<_i5.Playable> get downloadRemovedStream => + _i4.Stream<_i2.Playable> get downloadRemovedStream => (super.noSuchMethod( Invocation.getter(#downloadRemovedStream), - returnValue: _i3.Stream<_i5.Playable>.empty(), - ) as _i3.Stream<_i5.Playable>); + returnValue: _i4.Stream<_i2.Playable>.empty(), + ) as _i4.Stream<_i2.Playable>); @override - _i3.Stream<_i2.Download> get playableDownloadedStream => (super.noSuchMethod( + _i4.Stream<_i3.Download> get playableDownloadedStream => (super.noSuchMethod( Invocation.getter(#playableDownloadedStream), - returnValue: _i3.Stream<_i2.Download>.empty(), - ) as _i3.Stream<_i2.Download>); + returnValue: _i4.Stream<_i3.Download>.empty(), + ) as _i4.Stream<_i3.Download>); @override - _i3.Future get downloadsDir => (super.noSuchMethod( + _i4.Future get downloadsDir => (super.noSuchMethod( Invocation.getter(#downloadsDir), - returnValue: _i3.Future.value(_i6.dummyValue( + returnValue: _i4.Future.value(_i6.dummyValue( this, Invocation.getter(#downloadsDir), )), - ) as _i3.Future); + ) as _i4.Future); @override - _i3.Future download({required _i5.Playable? playable}) => + _i4.Future download({required _i2.Playable? playable}) => (super.noSuchMethod( Invocation.method( #download, [], {#playable: playable}, ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i2.Download? getForPlayable(_i5.Playable? playable) => + _i3.Download? getForPlayable(_i2.Playable? playable) => (super.noSuchMethod(Invocation.method( #getForPlayable, [playable], - )) as _i2.Download?); + )) as _i3.Download?); @override - bool has({required _i5.Playable? playable}) => (super.noSuchMethod( + bool has({required _i2.Playable? playable}) => (super.noSuchMethod( Invocation.method( #has, [], @@ -167,7 +177,7 @@ class MockDownloadProvider extends _i1.Mock implements _i2.DownloadProvider { ); @override - void persistMetadataIfNeeded(_i5.Playable? playable) => + void persistMetadataIfNeeded(_i2.Playable? playable) => super.noSuchMethod( Invocation.method( #persistMetadataIfNeeded, @@ -177,25 +187,144 @@ class MockDownloadProvider extends _i1.Mock implements _i2.DownloadProvider { ); @override - _i3.Future removeForPlayable(_i5.Playable? playable) => + _i4.Future removeForPlayable(_i2.Playable? playable) => (super.noSuchMethod( Invocation.method( #removeForPlayable, [playable], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future clear() => (super.noSuchMethod( + _i4.Future clear() => (super.noSuchMethod( Invocation.method( #clear, [], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + + @override + void unsubscribeAll() => super.noSuchMethod( + Invocation.method( + #unsubscribeAll, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void subscribe(_i4.StreamSubscription? sub) => super.noSuchMethod( + Invocation.method( + #subscribe, + [sub], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [AuthProvider]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAuthProvider extends _i1.Mock implements _i3.AuthProvider { + MockAuthProvider() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.User get authUser => (super.noSuchMethod( + Invocation.getter(#authUser), + returnValue: _FakeUser_0( + this, + Invocation.getter(#authUser), + ), + ) as _i2.User); + + @override + _i4.Future<_i3.TwoFactorChallenge?> login({ + required String? host, + required String? email, + required String? password, + }) => + (super.noSuchMethod( + Invocation.method( + #login, + [], + { + #host: host, + #email: email, + #password: password, + }, + ), + returnValue: _i4.Future<_i3.TwoFactorChallenge?>.value(), + ) as _i4.Future<_i3.TwoFactorChallenge?>); + + @override + _i4.Future completeTwoFactorChallenge({ + required String? loginToken, + required String? code, + }) => + (super.noSuchMethod( + Invocation.method( + #completeTwoFactorChallenge, + [], + { + #loginToken: loginToken, + #code: code, + }, + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + + @override + _i4.Future loginWithOneTimeToken({ + required String? host, + required String? token, + }) => + (super.noSuchMethod( + Invocation.method( + #loginWithOneTimeToken, + [], + { + #host: host, + #token: token, + }, + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + + @override + void setAuthUser(_i2.User? user) => super.noSuchMethod( + Invocation.method( + #setAuthUser, + [user], + ), + returnValueForMissingStub: null, + ); + + @override + _i4.Future<_i2.User?> tryGetAuthUser() => (super.noSuchMethod( + Invocation.method( + #tryGetAuthUser, + [], + ), + returnValue: _i4.Future<_i2.User?>.value(), + ) as _i4.Future<_i2.User?>); + + @override + _i4.Future logout() => (super.noSuchMethod( + Invocation.method( + #logout, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override void unsubscribeAll() => super.noSuchMethod( @@ -207,7 +336,7 @@ class MockDownloadProvider extends _i1.Mock implements _i2.DownloadProvider { ); @override - void subscribe(_i3.StreamSubscription? sub) => super.noSuchMethod( + void subscribe(_i4.StreamSubscription? sub) => super.noSuchMethod( Invocation.method( #subscribe, [sub],