Files
plezy/test/screens/profile/add_local_profile_screen_test.dart
T
edde746 ce9556db22 fix(tv): restore the native Android IME for single-line text input
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.
2026-08-09 09:56:53 +02:00

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);
});
}