Android TV returns to the platform keyboard for single-line fields; the Flutter overlay stays for multiline and explicit call sites. The bugs that forced the overlay (#1051, #1079) were an engine show/bind ordering race, now repaired at the app level: - MainActivity retries a soft-input show the engine dropped while the FlutterView was not yet served (flutter/flutter#177360), rebinds the IME key session once at first show, and consumes leaked D-pad keys while the keyboard is visible (bounded restartInput budget) so focus cannot wander behind a stuck keyboard. - The platform text-input hint is activation-based, so gamepad pause and the pre-IME D-pad intercept track a live session instead of mere field focus. - While a session is live with the keyboard away, Back closes it and is consumed once, Select re-raises the keyboard, and arrows keep caret-aware edge-escape navigation instead of dead-ending.
131 lines
4.9 KiB
Dart
131 lines
4.9 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:plezy/focus/input_mode_tracker.dart';
|
|
import 'package:plezy/i18n/strings.g.dart';
|
|
import 'package:plezy/screens/profile/add_local_profile_screen.dart';
|
|
import 'package:plezy/utils/platform_detector.dart';
|
|
|
|
import '../../test_helpers/prefs.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
setUp(() {
|
|
resetSharedPreferencesForTest();
|
|
TvDetectionService.debugSetAppleTVOverride(true);
|
|
// Simulated TV device: the test host's desktop OS must not leak into
|
|
// InputModeTracker's desktop force-TV exemption.
|
|
PlatformDetector.debugSetIsDesktopOSOverride(false);
|
|
LocaleSettings.setLocaleSync(AppLocale.en);
|
|
});
|
|
|
|
tearDown(() {
|
|
TvDetectionService.debugSetAppleTVOverride(null);
|
|
TvDetectionService.setForceTVSync(false);
|
|
PlatformDetector.debugSetIsDesktopOSOverride(null);
|
|
});
|
|
|
|
testWidgets('D-pad leaves profile name input and reaches actions', (tester) async {
|
|
await tester.pumpWidget(
|
|
TranslationProvider(
|
|
child: const InputModeTracker(child: MaterialApp(home: AddLocalProfileScreen())),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
final fieldFinder = find.byType(TextField);
|
|
|
|
expect(FocusManager.instance.primaryFocus?.debugLabel, 'AddLocalProfile:Name');
|
|
expect(find.byKey(const Key('tv_virtual_keyboard_panel')), findsNothing);
|
|
expect(tester.widget<TextField>(fieldFinder).readOnly, isFalse);
|
|
|
|
await tester.enterText(fieldFinder, 'A');
|
|
await tester.pump();
|
|
expect(tester.widget<TextField>(fieldFinder).controller!.text, 'A');
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.gameButtonB);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(FocusManager.instance.primaryFocus?.debugLabel, 'AddLocalProfile:Name');
|
|
expect(tester.widget<TextField>(fieldFinder).readOnly, isTrue);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
|
await tester.pump();
|
|
expect(FocusManager.instance.primaryFocus?.debugLabel, 'AddLocalProfile:SetPin');
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
|
await tester.pump();
|
|
expect(FocusManager.instance.primaryFocus?.debugLabel, 'AddLocalProfile:Continue');
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
|
await tester.pump();
|
|
expect(FocusManager.instance.primaryFocus?.debugLabel, 'AddLocalProfile:Cancel');
|
|
});
|
|
|
|
testWidgets('Android TV native keyboard done leaves profile name input', (tester) async {
|
|
TvDetectionService.debugSetAppleTVOverride(null);
|
|
await TvDetectionService.getInstance(forceTv: true);
|
|
TvDetectionService.setForceTVSync(true);
|
|
|
|
await tester.pumpWidget(
|
|
TranslationProvider(
|
|
child: const InputModeTracker(child: MaterialApp(home: AddLocalProfileScreen())),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
// The native IME opens in place: focus stays on the field, no overlay.
|
|
expect(FocusManager.instance.primaryFocus?.debugLabel, 'AddLocalProfile:Name');
|
|
expect(find.byKey(const Key('tv_virtual_keyboard_panel')), findsNothing);
|
|
|
|
await tester.showKeyboard(find.byType(TextField));
|
|
await tester.testTextInput.receiveAction(TextInputAction.done);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(FocusManager.instance.primaryFocus?.debugLabel, 'AddLocalProfile:SetPin');
|
|
});
|
|
|
|
testWidgets('remote back pops the new profile page', (tester) async {
|
|
await tester.pumpWidget(
|
|
TranslationProvider(
|
|
child: InputModeTracker(
|
|
child: MaterialApp(
|
|
home: Builder(
|
|
builder: (context) => Scaffold(
|
|
body: Center(
|
|
child: ElevatedButton(
|
|
onPressed: () =>
|
|
Navigator.of(context).push(MaterialPageRoute(builder: (_) => const AddLocalProfileScreen())),
|
|
child: const Text('Open new profile'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('Open new profile'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text(t.profiles.newProfile), findsOneWidget);
|
|
final fieldFinder = find.byType(TextField);
|
|
expect(find.byKey(const Key('tv_virtual_keyboard_panel')), findsNothing);
|
|
expect(tester.widget<TextField>(fieldFinder).readOnly, isFalse);
|
|
await tester.showKeyboard(fieldFinder);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.gameButtonB);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text(t.profiles.newProfile), findsOneWidget);
|
|
expect(find.byKey(const Key('tv_virtual_keyboard_panel')), findsNothing);
|
|
expect(tester.widget<TextField>(fieldFinder).readOnly, isTrue);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.gameButtonB);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Open new profile'), findsOneWidget);
|
|
expect(find.text(t.profiles.newProfile), findsNothing);
|
|
});
|
|
}
|