// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; Future<void> sendKeyCombination( WidgetTester tester, SingleActivator activator, ) async { final List<LogicalKeyboardKey> modifiers = <LogicalKeyboardKey>[ if (activator.control) LogicalKeyboardKey.control, if (activator.shift) LogicalKeyboardKey.shift, if (activator.alt) LogicalKeyboardKey.alt, if (activator.meta) LogicalKeyboardKey.meta, ]; for (final LogicalKeyboardKey modifier in modifiers) { await tester.sendKeyDownEvent(modifier); } await tester.sendKeyDownEvent(activator.trigger); await tester.sendKeyUpEvent(activator.trigger); await tester.pump(); for (final LogicalKeyboardKey modifier in modifiers.reversed) { await tester.sendKeyUpEvent(modifier); } } void main() { Widget buildSpyAboveEditableText({ required FocusNode editableFocusNode, required FocusNode spyFocusNode, }) { return MaterialApp( home: Align( alignment: Alignment.topLeft, child: SizedBox( // Softwrap at exactly 20 characters. width: 201, height: 200, child: ActionSpy( focusNode: spyFocusNode, child: EditableText( controller: TextEditingController(text: 'dummy text'), showSelectionHandles: true, autofocus: true, focusNode: editableFocusNode, style: const TextStyle(fontSize: 10.0), textScaleFactor: 1, // Avoid the cursor from taking up width. cursorWidth: 0, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, selectionControls: materialTextSelectionControls, keyboardType: TextInputType.text, maxLines: null, textAlign: TextAlign.left, ), ), ), ), ); } group('macOS does not accept shortcuts if focus under EditableText', () { final TargetPlatformVariant macOSOnly = TargetPlatformVariant.only(TargetPlatform.macOS); testWidgets('word modifier + arrowLeft', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); final FocusNode spy = FocusNode(); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, spyFocusNode: spy, ), ); editable.requestFocus(); await tester.pump(); final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy)); await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true)); await tester.pump(); expect(state.lastIntent, isNull); }, variant: macOSOnly); testWidgets('word modifier + arrowRight', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); final FocusNode spy = FocusNode(); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, spyFocusNode: spy, ), ); editable.requestFocus(); await tester.pump(); final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy)); await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true)); await tester.pump(); expect(state.lastIntent, isNull); }, variant: macOSOnly); testWidgets('line modifier + arrowLeft', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); final FocusNode spy = FocusNode(); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, spyFocusNode: spy, ), ); editable.requestFocus(); await tester.pump(); final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy)); await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, meta: true)); await tester.pump(); expect(state.lastIntent, isNull); }, variant: macOSOnly); testWidgets('line modifier + arrowRight', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); final FocusNode spy = FocusNode(); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, spyFocusNode: spy, ), ); editable.requestFocus(); await tester.pump(); final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy)); await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, meta: true)); await tester.pump(); expect(state.lastIntent, isNull); }, variant: macOSOnly); testWidgets('word modifier + arrow key movement', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); final FocusNode spy = FocusNode(); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, spyFocusNode: spy, ), ); editable.requestFocus(); await tester.pump(); final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy)); await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true)); await tester.pump(); expect(state.lastIntent, isNull); await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true)); await tester.pump(); expect(state.lastIntent, isNull); await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true)); await tester.pump(); expect(state.lastIntent, isNull); await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true)); await tester.pump(); expect(state.lastIntent, isNull); }, variant: macOSOnly); testWidgets('line modifier + arrow key movement', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); final FocusNode spy = FocusNode(); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, spyFocusNode: spy, ), ); editable.requestFocus(); await tester.pump(); final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy)); await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, meta: true)); await tester.pump(); expect(state.lastIntent, isNull); await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, meta: true)); await tester.pump(); expect(state.lastIntent, isNull); await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, meta: true)); await tester.pump(); expect(state.lastIntent, isNull); await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, meta: true)); await tester.pump(); expect(state.lastIntent, isNull); }, variant: macOSOnly); }); group('macOS does accept shortcuts if focus above EditableText', () { final TargetPlatformVariant macOSOnly = TargetPlatformVariant.only(TargetPlatform.macOS); testWidgets('word modifier + arrowLeft', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); final FocusNode spy = FocusNode(); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, spyFocusNode: spy, ), ); spy.requestFocus(); await tester.pump(); final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy)); await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true)); await tester.pump(); expect(state.lastIntent, isA<ExtendSelectionToNextWordBoundaryIntent>()); }, variant: macOSOnly); testWidgets('word modifier + arrowRight', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); final FocusNode spy = FocusNode(); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, spyFocusNode: spy, ), ); spy.requestFocus(); await tester.pump(); final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy)); await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true)); await tester.pump(); expect(state.lastIntent, isA<ExtendSelectionToNextWordBoundaryIntent>()); }, variant: macOSOnly); testWidgets('line modifier + arrowLeft', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); final FocusNode spy = FocusNode(); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, spyFocusNode: spy, ), ); spy.requestFocus(); await tester.pump(); final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy)); await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, meta: true)); await tester.pump(); expect(state.lastIntent, isA<ExtendSelectionToLineBreakIntent>()); }, variant: macOSOnly); testWidgets('line modifier + arrowRight', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); final FocusNode spy = FocusNode(); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, spyFocusNode: spy, ), ); spy.requestFocus(); await tester.pump(); final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy)); await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, meta: true)); await tester.pump(); expect(state.lastIntent, isA<ExtendSelectionToLineBreakIntent>()); }, variant: macOSOnly); testWidgets('word modifier + arrow key movement', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); final FocusNode spy = FocusNode(); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, spyFocusNode: spy, ), ); spy.requestFocus(); await tester.pump(); final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy)); await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true)); await tester.pump(); expect(state.lastIntent, isA<ExtendSelectionToNextWordBoundaryIntent>()); state.lastIntent = null; await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true)); await tester.pump(); expect(state.lastIntent, isA<ExtendSelectionToNextWordBoundaryIntent>()); state.lastIntent = null; await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true)); await tester.pump(); expect(state.lastIntent, isA<ExtendSelectionToNextWordBoundaryIntent>()); state.lastIntent = null; await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true)); await tester.pump(); expect(state.lastIntent, isA<ExtendSelectionToNextWordBoundaryIntent>()); }, variant: macOSOnly); testWidgets('line modifier + arrow key movement', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); final FocusNode spy = FocusNode(); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, spyFocusNode: spy, ), ); spy.requestFocus(); await tester.pump(); final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy)); await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, meta: true)); await tester.pump(); expect(state.lastIntent, isA<ExtendSelectionToLineBreakIntent>()); state.lastIntent = null; await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, meta: true)); await tester.pump(); expect(state.lastIntent, isA<ExtendSelectionToLineBreakIntent>()); state.lastIntent = null; await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, meta: true)); await tester.pump(); expect(state.lastIntent, isA<ExtendSelectionToLineBreakIntent>()); state.lastIntent = null; await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, meta: true)); await tester.pump(); expect(state.lastIntent, isA<ExtendSelectionToLineBreakIntent>()); }, variant: macOSOnly); }, skip: kIsWeb); // [intended] specific tests target non-web. } class ActionSpy extends StatefulWidget { const ActionSpy({super.key, required this.focusNode, required this.child}); final FocusNode focusNode; final Widget child; @override State<ActionSpy> createState() => ActionSpyState(); } class ActionSpyState extends State<ActionSpy> { Intent? lastIntent; late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{ ExtendSelectionByCharacterIntent: CallbackAction<ExtendSelectionByCharacterIntent>(onInvoke: _captureIntent), ExtendSelectionToNextWordBoundaryIntent: CallbackAction<ExtendSelectionToNextWordBoundaryIntent>(onInvoke: _captureIntent), ExtendSelectionToLineBreakIntent: CallbackAction<ExtendSelectionToLineBreakIntent>(onInvoke: _captureIntent), ExpandSelectionToLineBreakIntent: CallbackAction<ExpandSelectionToLineBreakIntent>(onInvoke: _captureIntent), ExpandSelectionToDocumentBoundaryIntent: CallbackAction<ExpandSelectionToDocumentBoundaryIntent>(onInvoke: _captureIntent), ExtendSelectionVerticallyToAdjacentLineIntent: CallbackAction<ExtendSelectionVerticallyToAdjacentLineIntent>(onInvoke: _captureIntent), ExtendSelectionToDocumentBoundaryIntent: CallbackAction<ExtendSelectionToDocumentBoundaryIntent>(onInvoke: _captureIntent), ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: CallbackAction<ExtendSelectionToNextWordBoundaryOrCaretLocationIntent>(onInvoke: _captureIntent), }; // ignore: use_setters_to_change_properties void _captureIntent(Intent intent) { lastIntent = intent; } @override Widget build(BuildContext context) { return Actions( actions: _actions, child: Focus( focusNode: widget.focusNode, child: widget.child, ), ); } }