fix: eliminate cross-app consistency drift

This commit is contained in:
edde746
2026-07-13 23:13:53 +02:00
parent ec249d2ed4
commit d5f3e581df
142 changed files with 4751 additions and 1075 deletions
@@ -1,8 +1,18 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/focus/dpad_navigator.dart';
import 'package:plezy/focus/key_event_utils.dart';
import 'package:plezy/media/library_first_character.dart';
import 'package:plezy/screens/libraries/alpha_jump_bar.dart';
import 'package:plezy/utils/platform_detector.dart';
const _backKeys = [
_BackKeyCase('Escape', LogicalKeyboardKey.escape, PhysicalKeyboardKey.escape),
_BackKeyCase('GoBack', LogicalKeyboardKey.goBack, PhysicalKeyboardKey.escape),
_BackKeyCase('BrowserBack', LogicalKeyboardKey.browserBack, PhysicalKeyboardKey.browserBack),
_BackKeyCase('GameButtonB', LogicalKeyboardKey.gameButtonB, PhysicalKeyboardKey.enter),
];
void main() {
const alphabetCharacters = [
@@ -35,6 +45,91 @@ void main() {
LibraryFirstCharacter(key: 'Z', title: 'Z', size: 1),
];
tearDown(() {
TvDetectionService.debugSetAppleTVOverride(null);
BackKeyUpSuppressor.clearSuppression();
BackKeyCoordinator.clear();
});
for (final backKey in _backKeys) {
testWidgets('${backKey.label} runs back once on key up and consumes the normal press', (tester) async {
TvDetectionService.debugSetAppleTVOverride(false);
final harness = await _pumpBackHarness(tester);
final downHandled = _sendKeyDown(tester, backKey);
expect(downHandled, isTrue);
expect(harness.jumpBarBacks, 0);
expect(harness.ancestorEvents, 0);
final repeatHandled = _sendKeyRepeat(tester, backKey);
expect(repeatHandled, isTrue);
expect(harness.jumpBarBacks, 0);
expect(harness.ancestorEvents, 0);
final upHandled = _sendKeyUp(tester, backKey);
expect(upHandled, isTrue);
expect(harness.jumpBarBacks, 1);
expect(harness.ancestorEvents, 0);
expect(harness.ancestorBacks, 0);
await tester.pump();
harness.moveFocusOnBack = true;
final movingDownHandled = _sendKeyDown(tester, backKey);
final movingRepeatHandled = _sendKeyRepeat(tester, backKey);
final movingUpHandled = _sendKeyUp(tester, backKey);
expect(movingDownHandled, isTrue);
expect(movingRepeatHandled, isTrue);
expect(movingUpHandled, isTrue);
expect(harness.jumpBarBacks, 2);
expect(harness.ancestorEvents, 0);
expect(harness.ancestorBacks, 0);
await tester.pump();
expect(harness.nextFocusNode.hasFocus, isTrue);
});
testWidgets('${backKey.label} runs back once on Apple TV key down and consumes key up', (tester) async {
TvDetectionService.debugSetAppleTVOverride(true);
final harness = await _pumpBackHarness(tester);
final downHandled = _sendKeyDown(tester, backKey);
expect(downHandled, isTrue);
expect(harness.jumpBarBacks, 1);
expect(harness.ancestorEvents, 0);
final repeatHandled = _sendKeyRepeat(tester, backKey);
expect(repeatHandled, isTrue);
expect(harness.jumpBarBacks, 1);
expect(harness.ancestorEvents, 0);
final upHandled = _sendKeyUp(tester, backKey);
expect(upHandled, isTrue);
expect(harness.jumpBarBacks, 1);
expect(harness.ancestorEvents, 0);
expect(harness.ancestorBacks, 0);
await tester.pump();
harness.moveFocusOnBack = true;
final movingDownHandled = _sendKeyDown(tester, backKey);
expect(movingDownHandled, isTrue);
expect(harness.jumpBarBacks, 2);
expect(harness.ancestorEvents, 0);
await tester.pump();
expect(harness.nextFocusNode.hasFocus, isTrue);
final movingRepeatHandled = _sendKeyRepeat(tester, backKey);
final movingUpHandled = _sendKeyUp(tester, backKey);
expect(movingRepeatHandled, isTrue);
expect(movingUpHandled, isTrue);
expect(harness.jumpBarBacks, 2);
expect(harness.ancestorEvents, 2);
expect(harness.ancestorBacks, 0);
});
}
testWidgets('keeps the full alphabet visible in a short TV-height bar', (tester) async {
await tester.pumpWidget(
MaterialApp(
@@ -121,3 +216,114 @@ void main() {
expect(jumpedTo, 2);
});
}
class _BackKeyCase {
final String label;
final LogicalKeyboardKey logicalKey;
final PhysicalKeyboardKey physicalKey;
const _BackKeyCase(this.label, this.logicalKey, this.physicalKey);
}
class _BackHarness {
final jumpBarFocusNode = FocusNode(debugLabel: 'test_alpha_jump_bar_back');
final nextFocusNode = FocusNode(debugLabel: 'test_alpha_jump_bar_back_destination');
final ancestorFocusNode = FocusNode(debugLabel: 'test_alpha_jump_bar_back_ancestor');
var jumpBarBacks = 0;
var ancestorEvents = 0;
var ancestorBacks = 0;
var moveFocusOnBack = false;
void dispose() {
jumpBarFocusNode.dispose();
nextFocusNode.dispose();
ancestorFocusNode.dispose();
}
}
Future<_BackHarness> _pumpBackHarness(WidgetTester tester) async {
final harness = _BackHarness();
addTearDown(harness.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Focus(
focusNode: harness.ancestorFocusNode,
onKeyEvent: (_, event) {
harness.ancestorEvents++;
return handleBackKeyAction(event, () => harness.ancestorBacks++);
},
child: Column(
children: [
SizedBox(
height: 300,
child: AlphaJumpBar(
firstCharacters: const [
LibraryFirstCharacter(key: 'A', title: 'A', size: 3),
LibraryFirstCharacter(key: 'B', title: 'B', size: 4),
LibraryFirstCharacter(key: 'C', title: 'C', size: 2),
],
currentLetter: 'B',
focusNode: harness.jumpBarFocusNode,
onJump: (_) {},
onBack: () {
harness.jumpBarBacks++;
if (harness.moveFocusOnBack) {
harness.nextFocusNode.requestFocus();
}
},
),
),
Focus(focusNode: harness.nextFocusNode, child: const SizedBox(width: 1, height: 1)),
],
),
),
),
),
);
harness.jumpBarFocusNode.requestFocus();
await tester.pump();
expect(harness.jumpBarFocusNode.hasFocus, isTrue);
return harness;
}
bool _sendKeyDown(WidgetTester _, _BackKeyCase backKey) {
return _dispatchKeyEvent(
KeyDownEvent(physicalKey: backKey.physicalKey, logicalKey: backKey.logicalKey, timeStamp: Duration.zero),
);
}
bool _sendKeyRepeat(WidgetTester _, _BackKeyCase backKey) {
return _dispatchKeyEvent(
KeyRepeatEvent(
physicalKey: backKey.physicalKey,
logicalKey: backKey.logicalKey,
timeStamp: const Duration(milliseconds: 1),
),
);
}
bool _sendKeyUp(WidgetTester _, _BackKeyCase backKey) {
return _dispatchKeyEvent(
KeyUpEvent(
physicalKey: backKey.physicalKey,
logicalKey: backKey.logicalKey,
timeStamp: const Duration(milliseconds: 2),
),
);
}
bool _dispatchKeyEvent(KeyEvent event) {
final handled = HardwareKeyboard.instance.handleKeyEvent(event);
FocusNode? node = FocusManager.instance.primaryFocus;
while (node != null) {
final result = node.onKeyEvent?.call(node, event) ?? KeyEventResult.ignored;
if (result == KeyEventResult.handled) return true;
if (result == KeyEventResult.skipRemainingHandlers) return handled;
node = node.parent;
}
return handled;
}
@@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/focus/focusable_button.dart';
import 'package:plezy/focus/input_mode_tracker.dart';
import 'package:plezy/i18n/strings.g.dart';
import 'package:plezy/screens/libraries/state_messages.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() => LocaleSettings.setLocaleSync(AppLocale.en));
tearDown(() => LocaleSettings.setLocaleSync(AppLocale.en));
testWidgets('StateMessageWidget forwards action focus behavior to FocusableButton', (tester) async {
await _pump(
tester,
StateMessageWidget(
message: 'Nothing here',
actionLabel: 'Reload',
onAction: () {},
actionAutofocus: true,
actionUseBackgroundFocus: true,
),
);
final button = tester.widget<FocusableButton>(find.byType(FocusableButton));
expect(button.autofocus, isTrue);
expect(button.useBackgroundFocus, isTrue);
final focusedNode = FocusManager.instance.primaryFocus;
expect(focusedNode, isNotNull);
final focusedWidget = find.byWidgetPredicate((widget) => widget is Focus && widget.focusNode == focusedNode);
expect(focusedWidget, findsOneWidget);
expect(find.ancestor(of: focusedWidget, matching: find.byType(FocusableButton)), findsOneWidget);
});
testWidgets('ErrorStateWidget retry supports D-pad and pointer activation', (tester) async {
var retries = 0;
await _pump(
tester,
ErrorStateWidget(
message: 'Could not load',
retryLabel: 'Try again',
onRetry: () => retries++,
actionAutofocus: true,
actionUseBackgroundFocus: true,
),
);
final button = tester.widget<FocusableButton>(find.byType(FocusableButton));
expect(button.autofocus, isTrue);
expect(button.useBackgroundFocus, isTrue);
await tester.tap(find.widgetWithText(FilledButton, 'Try again'));
await tester.pump();
expect(retries, 1);
await tester.sendKeyEvent(LogicalKeyboardKey.select);
await tester.pump();
expect(retries, 2);
});
group('ErrorStateWidget localized defaults', () {
setUp(() async {
await LocaleSettings.setLocale(AppLocale.es);
});
testWidgets('uses the localized default retry label', (tester) async {
await _pump(tester, ErrorStateWidget(message: 'No se pudo cargar', onRetry: () {}));
expect(find.text('Reintentar'), findsOneWidget);
expect(find.text('Retry'), findsNothing);
});
});
}
Future<void> _pump(WidgetTester tester, Widget child) async {
await tester.pumpWidget(
TranslationProvider(
child: InputModeTracker(
child: MaterialApp(home: Scaffold(body: child)),
),
),
);
await tester.pumpAndSettle();
}