Files
plezy/test/mixins/refreshable_test.dart
T
edde746andl3gitpanda b46107cb58 fix(tv): dismiss on-screen keyboard on companion-remote search submit
close #1514

A query submitted from the companion remote now behaves like a submit
instead of focus-to-type: TvKeyboardController closes an already-open
OSK and lands focus on the input without reopening it, then the shared
handleSearchSubmit path runs the search and focuses the first result.
Also guard DebouncedMediaSearch against selection-only controller
notifications re-arming the debounce into a duplicate fetch.

Co-authored-by: l3gitpanda <12003346+l3gitpanda@users.noreply.github.com>
2026-07-10 08:48:38 +02:00

210 lines
6.0 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/mixins/refreshable.dart';
class _RefreshProbe extends StatefulWidget {
const _RefreshProbe({this.onState});
final void Function(_RefreshProbeState)? onState;
@override
State<_RefreshProbe> createState() => _RefreshProbeState();
}
class _RefreshProbeState extends State<_RefreshProbe>
with Refreshable, FullRefreshable, FocusableTab, SearchInputFocusable, LibraryLoadable {
int refreshCalls = 0;
int fullRefreshCalls = 0;
int focusActiveTabCalls = 0;
int focusSearchInputCalls = 0;
String? lastSubmittedQuery;
String? lastLibraryKey;
@override
void initState() {
super.initState();
widget.onState?.call(this);
}
@override
void refresh() => refreshCalls++;
@override
void fullRefresh() => fullRefreshCalls++;
@override
void focusActiveTabIfReady() => focusActiveTabCalls++;
@override
void focusSearchInput() => focusSearchInputCalls++;
@override
void submitSearchQuery(String query) => lastSubmittedQuery = query;
@override
void loadLibraryByKey(String libraryGlobalKey) => lastLibraryKey = libraryGlobalKey;
@override
Widget build(BuildContext context) => const SizedBox.shrink();
}
class _RefreshableOnly extends StatefulWidget {
const _RefreshableOnly({this.onState});
final void Function(_RefreshableOnlyState)? onState;
@override
State<_RefreshableOnly> createState() => _RefreshableOnlyState();
}
class _RefreshableOnlyState extends State<_RefreshableOnly> with Refreshable {
int refreshCalls = 0;
@override
void initState() {
super.initState();
widget.onState?.call(this);
}
@override
void refresh() => refreshCalls++;
@override
Widget build(BuildContext context) => const SizedBox.shrink();
}
class _PlainProbe extends StatefulWidget {
const _PlainProbe({this.onState});
final void Function(_PlainProbeState)? onState;
@override
State<_PlainProbe> createState() => _PlainProbeState();
}
class _PlainProbeState extends State<_PlainProbe> {
@override
void initState() {
super.initState();
widget.onState?.call(this);
}
@override
Widget build(BuildContext context) => const SizedBox.shrink();
}
void main() {
group('Refreshable', () {
testWidgets('refresh() invokes the implementation', (tester) async {
late _RefreshProbeState state;
await tester.pumpWidget(_RefreshProbe(onState: (s) => state = s));
state.refresh();
state.refresh();
expect(state.refreshCalls, 2);
});
testWidgets('a State mixing in Refreshable matches an `is Refreshable` check', (tester) async {
late _RefreshableOnlyState state;
await tester.pumpWidget(_RefreshableOnly(onState: (s) => state = s));
// This is the production usage: `if (currentState case final Refreshable r) r.refresh()`.
expect(state, isA<Refreshable>());
// Drive the refresh via the interface to mirror real callers.
if (state case final Refreshable r) {
r.refresh();
}
expect(state.refreshCalls, 1);
});
testWidgets('a plain State without the mixin does not match Refreshable', (tester) async {
late _PlainProbeState state;
await tester.pumpWidget(_PlainProbe(onState: (s) => state = s));
expect(state, isNot(isA<Refreshable>()));
expect(state, isNot(isA<FullRefreshable>()));
expect(state, isNot(isA<FocusableTab>()));
expect(state, isNot(isA<SearchInputFocusable>()));
expect(state, isNot(isA<LibraryLoadable>()));
});
});
group('FullRefreshable', () {
testWidgets('fullRefresh() invokes the implementation', (tester) async {
late _RefreshProbeState state;
await tester.pumpWidget(_RefreshProbe(onState: (s) => state = s));
if (state case final FullRefreshable f) {
f.fullRefresh();
}
expect(state.fullRefreshCalls, 1);
});
testWidgets('refresh() and fullRefresh() are independent counters', (tester) async {
late _RefreshProbeState state;
await tester.pumpWidget(_RefreshProbe(onState: (s) => state = s));
state.refresh();
state.refresh();
state.fullRefresh();
expect(state.refreshCalls, 2);
expect(state.fullRefreshCalls, 1);
});
});
group('FocusableTab', () {
testWidgets('focusActiveTabIfReady() invokes the implementation', (tester) async {
late _RefreshProbeState state;
await tester.pumpWidget(_RefreshProbe(onState: (s) => state = s));
expect(state, isA<FocusableTab>());
state.focusActiveTabIfReady();
expect(state.focusActiveTabCalls, 1);
});
});
group('SearchInputFocusable', () {
testWidgets('focusSearchInput() invokes the implementation', (tester) async {
late _RefreshProbeState state;
await tester.pumpWidget(_RefreshProbe(onState: (s) => state = s));
state.focusSearchInput();
expect(state.focusSearchInputCalls, 1);
});
testWidgets('submitSearchQuery() forwards the query argument', (tester) async {
late _RefreshProbeState state;
await tester.pumpWidget(_RefreshProbe(onState: (s) => state = s));
if (state case final SearchInputFocusable s) {
s.submitSearchQuery('movie');
}
expect(state.lastSubmittedQuery, 'movie');
});
});
group('LibraryLoadable', () {
testWidgets('loadLibraryByKey() forwards the key argument', (tester) async {
late _RefreshProbeState state;
await tester.pumpWidget(_RefreshProbe(onState: (s) => state = s));
state.loadLibraryByKey('server1:42');
expect(state.lastLibraryKey, 'server1:42');
});
});
group('combined mixins', () {
testWidgets('a State can mix in all five interface mixins simultaneously', (tester) async {
late _RefreshProbeState state;
await tester.pumpWidget(_RefreshProbe(onState: (s) => state = s));
expect(state, isA<Refreshable>());
expect(state, isA<FullRefreshable>());
expect(state, isA<FocusableTab>());
expect(state, isA<SearchInputFocusable>());
expect(state, isA<LibraryLoadable>());
});
});
}