Files
plezy/test/screens/libraries/base_library_tab_focus_test.dart
T

203 lines
5.9 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/focus/input_mode_tracker.dart';
import 'package:plezy/media/media_backend.dart';
import 'package:plezy/media/media_library.dart';
import 'package:plezy/screens/libraries/tabs/base_library_tab.dart';
import 'package:plezy/utils/platform_detector.dart';
const _library = MediaLibrary(id: '1', backend: MediaBackend.plex, title: 'Movies');
const _libraryB = MediaLibrary(id: '2', backend: MediaBackend.plex, title: 'Shows');
class _ProbeTab extends BaseLibraryTab<String> {
const _ProbeTab({super.key, required this.loadedItems, required super.onBack})
: super(library: _library, isActive: true);
final List<String> loadedItems;
@override
State<_ProbeTab> createState() => _ProbeTabState();
}
class _ProbeTabState extends BaseLibraryTabState<String, _ProbeTab> {
int focusFirstItemCalls = 0;
@override
Future<List<String>> loadData() async => widget.loadedItems;
@override
Widget buildContent(List<String> items) => const SizedBox.shrink();
@override
IconData get emptyIcon => Icons.inbox_rounded;
@override
String get emptyMessage => 'Empty';
@override
String get errorContext => 'probe';
@override
void focusFirstItem() {
focusFirstItemCalls++;
}
}
class _ControlledTab extends BaseLibraryTab<String> {
const _ControlledTab({super.key, required super.library, required this.load, super.onDataLoaded})
: super(suppressAutoFocus: true);
final Future<List<String>> Function(MediaLibrary library) load;
@override
State<_ControlledTab> createState() => _ControlledTabState();
}
class _ControlledTabState extends BaseLibraryTabState<String, _ControlledTab> {
@override
Future<List<String>> loadData() => widget.load(widget.library);
@override
Widget buildContent(List<String> items) => ListView(children: items.map(Text.new).toList());
@override
IconData get emptyIcon => Icons.inbox_rounded;
@override
String get emptyMessage => 'Empty';
@override
String get errorContext => 'controlled';
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
TvDetectionService.debugSetAppleTVOverride(true);
});
tearDown(() {
TvDetectionService.debugSetAppleTVOverride(null);
});
Future<_ProbeTabState> pumpProbe(
WidgetTester tester, {
required List<String> loadedItems,
required VoidCallback onBack,
}) async {
final key = GlobalKey<_ProbeTabState>();
await tester.pumpWidget(
InputModeTracker(
child: MaterialApp(
home: _ProbeTab(key: key, loadedItems: loadedItems, onBack: onBack),
),
),
);
await tester.pump();
await tester.pump();
return key.currentState!;
}
testWidgets('empty active tab focuses library chrome fallback', (tester) async {
var fallbackCalls = 0;
final state = await pumpProbe(tester, loadedItems: const [], onBack: () => fallbackCalls++);
expect(fallbackCalls, 1);
expect(state.focusFirstItemCalls, 0);
});
testWidgets('non-empty active tab focuses first item', (tester) async {
var fallbackCalls = 0;
final state = await pumpProbe(tester, loadedItems: const ['item'], onBack: () => fallbackCalls++);
expect(fallbackCalls, 0);
expect(state.focusFirstItemCalls, 1);
});
testWidgets('retained state rejects a completion from the previous library', (tester) async {
final key = GlobalKey<_ControlledTabState>();
final a = Completer<List<String>>();
final b = Completer<List<String>>();
var loadedCalls = 0;
Future<List<String>> load(MediaLibrary library) => library.id == _library.id ? a.future : b.future;
await tester.pumpWidget(
MaterialApp(
home: _ControlledTab(key: key, library: _library, load: load, onDataLoaded: () => loadedCalls++),
),
);
await tester.pumpWidget(
MaterialApp(
home: _ControlledTab(key: key, library: _libraryB, load: load, onDataLoaded: () => loadedCalls++),
),
);
b.complete(const ['current B']);
await tester.pump();
await tester.pump();
expect(find.text('current B'), findsOneWidget);
expect(loadedCalls, 1);
a.complete(const ['stale A']);
await tester.pump();
await tester.pump();
expect(find.text('current B'), findsOneWidget);
expect(find.text('stale A'), findsNothing);
expect(loadedCalls, 1);
});
testWidgets('retained state rejects a stale failure after current success', (tester) async {
final key = GlobalKey<_ControlledTabState>();
final a = Completer<List<String>>();
final b = Completer<List<String>>();
Future<List<String>> load(MediaLibrary library) => library.id == _library.id ? a.future : b.future;
await tester.pumpWidget(
MaterialApp(
home: _ControlledTab(key: key, library: _library, load: load),
),
);
await tester.pumpWidget(
MaterialApp(
home: _ControlledTab(key: key, library: _libraryB, load: load),
),
);
b.complete(const ['current B']);
await tester.pump();
a.completeError(StateError('stale failure'));
await tester.pump();
expect(find.text('current B'), findsOneWidget);
expect(find.textContaining('stale failure'), findsNothing);
});
testWidgets('newest same-library refresh owns the committed result', (tester) async {
final key = GlobalKey<_ControlledTabState>();
final loads = [Completer<List<String>>(), Completer<List<String>>()];
var request = 0;
await tester.pumpWidget(
MaterialApp(
home: _ControlledTab(key: key, library: _library, load: (_) => loads[request++].future),
),
);
key.currentState!.refresh();
loads[1].complete(const ['newer']);
await tester.pump();
expect(find.text('newer'), findsOneWidget);
loads[0].complete(const ['older']);
await tester.pump();
expect(find.text('newer'), findsOneWidget);
expect(find.text('older'), findsNothing);
});
}