From 8ade81fb201ae556b31dd6ab6a0b8cdcbf72b99b Mon Sep 17 00:00:00 2001 From: Bruno Leroux Date: Thu, 7 Mar 2024 08:19:26 +0100 Subject: [PATCH] [flutter_test] Change KeyEventSimulator default transit mode (#143847) ## Description This PRs changes the default value transit mode for key event simulation. The default transit mode for key event simulation is currently `KeyDataTransitMode.rawKeyData` while on the framework side `KeyDataTransitMode.keyDataThenRawKeyData` is the preferred transit mode. `KeyDataTransitMode.keyDataThenRawKeyData` is more accurate and can help detect issues. For instance the following test will fail with `KeyDataTransitMode.rawKeyData` because raw keyboard logic for modifier keys is less accurate: ```dart testWidgets('Press control left once', (WidgetTester tester) async { debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.keyDataThenRawKeyData; final List events = []; final FocusNode focusNode = FocusNode(); addTearDown(focusNode.dispose); await tester.pumpWidget( Focus( focusNode: focusNode, autofocus: true, onKeyEvent: (_, KeyEvent event) { events.add(event); return KeyEventResult.handled; }, child: Container(), ), ); await simulateKeyDownEvent(LogicalKeyboardKey.controlLeft); // This will fail when transit mode is KeyDataTransitMode.rawKeyData // because a down event for controlRight is synthesized. expect(events.length, 1); debugKeyEventSimulatorTransitModeOverride = null; }); ``` And the following this test is ok with `KeyDataTransitMode.rawKeyData` but rightly fails with `KeyDataTransitMode.keyDataThenRawKeyData`: ```dart testWidgets('Simulates consecutive key down events', (WidgetTester tester) async { debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.rawKeyData; // Simulating several key down events without key up in between is tolerated // when transit mode is KeyDataTransitMode.rawKeyData, it will trigger an // assert on KeyDataTransitMode.keyDataThenRawKeyData. await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown); await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown); debugKeyEventSimulatorTransitModeOverride = null; }); ``` ## Related Issue Related to https://github.com/flutter/flutter/issues/143845 ## Tests Adds two tests. --- .../test/cupertino/text_field_test.dart | 2 - .../test/material/dropdown_menu_test.dart | 26 ++++---- .../test/material/text_field_test.dart | 2 - .../test/services/raw_keyboard_test.dart | 64 +++++++++++++++---- .../lib/src/event_simulation.dart | 21 +++++- .../test/event_simulation_test.dart | 25 ++++++-- 6 files changed, 101 insertions(+), 39 deletions(-) diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart index 6955117509d..d443e546aaf 100644 --- a/packages/flutter/test/cupertino/text_field_test.dart +++ b/packages/flutter/test/cupertino/text_field_test.dart @@ -8507,14 +8507,12 @@ void main() { expect(controller.selection.extentOffset, 20); await tester.pump(kDoubleTapTimeout); - await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); await tester.tapAt(textOffsetToPosition(tester, 23)); await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 13); expect(controller.selection.extentOffset, 23); await tester.pump(kDoubleTapTimeout); - await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); await tester.tapAt(textOffsetToPosition(tester, 4)); await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 13); diff --git a/packages/flutter/test/material/dropdown_menu_test.dart b/packages/flutter/test/material/dropdown_menu_test.dart index 243e59e4f8f..f3ba1aeff20 100644 --- a/packages/flutter/test/material/dropdown_menu_test.dart +++ b/packages/flutter/test/material/dropdown_menu_test.dart @@ -694,7 +694,7 @@ void main() { await tester.tap(find.byType(DropdownMenu)); await tester.pump(); - await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pumpAndSettle(); Finder button0Material = find.descendant( of: find.widgetWithText(MenuItemButton, 'Item 0').last, @@ -705,7 +705,7 @@ void main() { expect(item0material.color, themeData.colorScheme.onSurface.withOpacity(0.12)); // Press down key one more time, the highlight should move to the next item. - await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pumpAndSettle(); final Finder button1Material = find.descendant( of: find.widgetWithText(MenuItemButton, 'Menu 1').last, @@ -735,7 +735,7 @@ void main() { await tester.tap(find.byType(DropdownMenu)); await tester.pump(); - await simulateKeyDownEvent(LogicalKeyboardKey.arrowUp); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pumpAndSettle(); Finder button5Material = find.descendant( of: find.widgetWithText(MenuItemButton, 'Item 5').last, @@ -746,7 +746,7 @@ void main() { expect(item5material.color, themeData.colorScheme.onSurface.withOpacity(0.12)); // Press up key one more time, the highlight should move up to the item 4. - await simulateKeyDownEvent(LogicalKeyboardKey.arrowUp); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pumpAndSettle(); final Finder button4Material = find.descendant( of: find.widgetWithText(MenuItemButton, 'Item 4').last, @@ -779,17 +779,17 @@ void main() { await tester.tap(find.byType(DropdownMenu)); await tester.pump(); - await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(find.widgetWithText(TextField, 'Item 0'), findsOneWidget); // Press down key one more time to the next item. - await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(find.widgetWithText(TextField, 'Menu 1'), findsOneWidget); // Press down to the next item. - await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); expect(find.widgetWithText(TextField, 'Item 2'), findsOneWidget); }, variant: TargetPlatformVariant.desktop()); @@ -810,17 +810,17 @@ void main() { await tester.tap(find.byType(DropdownMenu)); await tester.pump(); - await simulateKeyDownEvent(LogicalKeyboardKey.arrowUp); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pump(); expect(find.widgetWithText(TextField, 'Item 5'), findsOneWidget); // Press up key one more time to the upper item. - await simulateKeyDownEvent(LogicalKeyboardKey.arrowUp); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pump(); expect(find.widgetWithText(TextField, 'Item 4'), findsOneWidget); // Press up to the upper item. - await simulateKeyDownEvent(LogicalKeyboardKey.arrowUp); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pump(); expect(find.widgetWithText(TextField, 'Item 3'), findsOneWidget); }, variant: TargetPlatformVariant.desktop()); @@ -849,7 +849,7 @@ void main() { await tester.tap(find.byType(DropdownMenu)); await tester.pumpAndSettle(); - await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pumpAndSettle(); final Finder button0Material = find.descendant( of: find.widgetWithText(MenuItemButton, 'Item 0').last, @@ -859,7 +859,7 @@ void main() { expect(item0Material.color, themeData.colorScheme.onSurface.withOpacity(0.12)); // first item can be highlighted as it's enabled. // Continue to press down key. Item 3 should be highlighted as Menu 1 and Item 2 are both disabled. - await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pumpAndSettle(); final Finder button3Material = find.descendant( of: find.widgetWithText(MenuItemButton, 'Item 3').last, @@ -941,6 +941,7 @@ void main() { // Press up to the upper item (Item 0). await simulateKeyDownEvent(LogicalKeyboardKey.arrowUp); + await simulateKeyUpEvent(LogicalKeyboardKey.arrowUp); await tester.pumpAndSettle(); expect(find.widgetWithText(TextField, 'Item 0'), findsOneWidget); final Finder button0Material = find.descendant( @@ -952,6 +953,7 @@ void main() { // Continue to move up to the last item (Item 5). await simulateKeyDownEvent(LogicalKeyboardKey.arrowUp); + await simulateKeyUpEvent(LogicalKeyboardKey.arrowUp); await tester.pumpAndSettle(); expect(find.widgetWithText(TextField, 'Item 5'), findsOneWidget); final Finder button5Material = find.descendant( diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 87c56e64954..19c187ece18 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -15933,14 +15933,12 @@ void main() { expect(controller.selection.extentOffset, 20); await tester.pump(kDoubleTapTimeout); - await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); await tester.tapAt(textOffsetToPosition(tester, 23)); await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 13); expect(controller.selection.extentOffset, 23); await tester.pump(kDoubleTapTimeout); - await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); await tester.tapAt(textOffsetToPosition(tester, 4)); await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 13); diff --git a/packages/flutter/test/services/raw_keyboard_test.dart b/packages/flutter/test/services/raw_keyboard_test.dart index 4a1305d3b2a..ae87556742a 100644 --- a/packages/flutter/test/services/raw_keyboard_test.dart +++ b/packages/flutter/test/services/raw_keyboard_test.dart @@ -30,7 +30,7 @@ void main() { await simulateKeyDownEvent(LogicalKeyboardKey.backquote, platform: platform); RawKeyboard.instance.removeListener(handleKey); } - }); + }, variant: KeySimulatorTransitModeVariant.rawKeyData()); testWidgets('No character is produced for non-printables', (WidgetTester tester) async { for (final String platform in ['linux', 'android', 'macos', 'fuchsia', 'windows', 'web', 'ios']) { @@ -41,7 +41,7 @@ void main() { await simulateKeyDownEvent(LogicalKeyboardKey.shiftLeft, platform: platform); RawKeyboard.instance.removeListener(handleKey); } - }); + }, variant: KeySimulatorTransitModeVariant.rawKeyData()); testWidgets('keysPressed is maintained', (WidgetTester tester) async { for (final String platform in ['linux', 'android', 'macos', 'fuchsia', 'windows', 'ios']) { @@ -147,7 +147,10 @@ void main() { expect(RawKeyboard.instance.keysPressed, isEmpty, reason: 'on $platform'); } } - }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 + }, + variant: KeySimulatorTransitModeVariant.rawKeyData(), + skip: isBrowser, // https://github.com/flutter/flutter/issues/61021 + ); testWidgets('keysPressed is correct when modifier is released before key', (WidgetTester tester) async { for (final String platform in ['linux', 'android', 'macos', 'fuchsia', 'windows', 'ios']) { @@ -198,7 +201,10 @@ void main() { await simulateKeyUpEvent(LogicalKeyboardKey.keyA, platform: platform, physicalKey: PhysicalKeyboardKey.keyA); expect(RawKeyboard.instance.keysPressed, isEmpty, reason: 'on $platform'); } - }, skip: isBrowser); // https://github.com/flutter/flutter/issues/76741 + }, + variant: KeySimulatorTransitModeVariant.rawKeyData(), + skip: isBrowser, // https://github.com/flutter/flutter/issues/76741 + ); testWidgets('keysPressed modifiers are synchronized with key events on macOS', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); @@ -222,7 +228,10 @@ void main() { {LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.keyA}, ), ); - }, skip: isBrowser); // [intended] This is a macOS-specific test. + }, + variant: KeySimulatorTransitModeVariant.rawKeyData(), + skip: isBrowser, // [intended] This is a macOS-specific test. + ); testWidgets('keysPressed modifiers are synchronized with key events on iOS', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); @@ -246,7 +255,10 @@ void main() { {LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.keyA}, ), ); - }, skip: isBrowser); // [intended] This is an iOS-specific test. + }, + variant: KeySimulatorTransitModeVariant.rawKeyData(), + skip: isBrowser, // [intended] This is a iOS-specific test. + ); testWidgets('keysPressed modifiers are synchronized with key events on Windows', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); @@ -270,7 +282,10 @@ void main() { {LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.keyA}, ), ); - }, skip: isBrowser); // [intended] This is a Windows-specific test. + }, + variant: KeySimulatorTransitModeVariant.rawKeyData(), + skip: isBrowser, // [intended] This is a Windows-specific test. + ); testWidgets('keysPressed modifiers are synchronized with key events on android', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); @@ -294,7 +309,10 @@ void main() { {LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.keyA}, ), ); - }, skip: isBrowser); // [intended] This is an Android-specific test. + }, + variant: KeySimulatorTransitModeVariant.rawKeyData(), + skip: isBrowser, // [intended] This is an Android-specific test. + ); testWidgets('keysPressed modifiers are synchronized with key events on fuchsia', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); @@ -318,7 +336,10 @@ void main() { {LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.keyA}, ), ); - }, skip: isBrowser); // [intended] This is a Fuchsia-specific test. + }, + variant: KeySimulatorTransitModeVariant.rawKeyData(), + skip: isBrowser, // [intended] This is a Fuchsia-specific test. + ); testWidgets('keysPressed modifiers are synchronized with key events on Linux GLFW', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); @@ -348,7 +369,10 @@ void main() { }, ), ); - }, skip: isBrowser); // [intended] This is a GLFW-specific test. + }, + variant: KeySimulatorTransitModeVariant.rawKeyData(), + skip: isBrowser, // [intended] This is a GLFW-specific test. + ); Future simulateGTKKeyEvent(bool keyDown, int scancode, int keycode, int modifiers) async { final Map data = { @@ -397,7 +421,10 @@ void main() { }, ), ); - }, skip: isBrowser); // [intended] This is a GTK-specific test. + }, + variant: KeySimulatorTransitModeVariant.rawKeyData(), + skip: isBrowser, // [intended] This is a GTK-specific test. + ); // Regression test for https://github.com/flutter/flutter/issues/114591 . // @@ -414,7 +441,10 @@ void main() { }, ), ); - }, skip: isBrowser); // [intended] This is a GTK-specific test. + }, + variant: KeySimulatorTransitModeVariant.rawKeyData(), + skip: isBrowser, // [intended] This is a GTK-specific test. + ); // Regression test for https://github.com/flutter/flutter/issues/114591 . // @@ -447,7 +477,10 @@ void main() { }, ), ); - }, skip: !isBrowser); // [intended] This is a Browser-specific test. + }, + variant: KeySimulatorTransitModeVariant.rawKeyData(), + skip: !isBrowser, // [intended] This is a Browser-specific test. + ); testWidgets('keysPressed modifiers are synchronized with key events on web', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); @@ -572,7 +605,10 @@ void main() { }, ), ); - }, skip: isBrowser); // [intended] This is an Android-specific test. + }, + variant: KeySimulatorTransitModeVariant.rawKeyData(), + skip: isBrowser, // [intended] This is a Android-specific test. + ); testWidgets('sided modifiers without a side set return all sides on macOS', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); diff --git a/packages/flutter_test/lib/src/event_simulation.dart b/packages/flutter_test/lib/src/event_simulation.dart index 98c9c99213d..329659509a0 100644 --- a/packages/flutter_test/lib/src/event_simulation.dart +++ b/packages/flutter_test/lib/src/event_simulation.dart @@ -684,7 +684,7 @@ abstract final class KeyEventSimulator { return result!; } - static const KeyDataTransitMode _defaultTransitMode = KeyDataTransitMode.rawKeyData; + static const KeyDataTransitMode _defaultTransitMode = KeyDataTransitMode.keyDataThenRawKeyData; // The simulation transit mode for [simulateKeyDownEvent], [simulateKeyUpEvent], // and [simulateKeyRepeatEvent]. @@ -693,8 +693,8 @@ abstract final class KeyEventSimulator { // and delivered. For detailed introduction, see [KeyDataTransitMode] and // its values. // - // The `_transitMode` defaults to [KeyDataTransitMode.rawKeyEvent], and can be - // overridden with [debugKeyEventSimulatorTransitModeOverride]. In widget tests, it + // The `_transitMode` defaults to [KeyDataTransitMode.keyDataThenRawKeyData], and can + // be overridden with [debugKeyEventSimulatorTransitModeOverride]. In widget tests, it // is often set with [KeySimulationModeVariant]. static KeyDataTransitMode get _transitMode { KeyDataTransitMode? result; @@ -705,6 +705,12 @@ abstract final class KeyEventSimulator { return result ?? _defaultTransitMode; } + /// Returns the transit mode that simulated key events are constructed + /// and delivered. For detailed introduction, see [KeyDataTransitMode] + /// and its values. + @visibleForTesting + static KeyDataTransitMode get transitMode => _transitMode; + static String get _defaultPlatform => kIsWeb ? 'web' : Platform.operatingSystem; /// Simulates sending a hardware key down event. @@ -965,6 +971,15 @@ class KeySimulatorTransitModeVariant extends TestVariant { KeySimulatorTransitModeVariant.keyDataThenRawKeyData() : this({KeyDataTransitMode.keyDataThenRawKeyData}); + /// Creates a [KeySimulatorTransitModeVariant] that only contains + /// [KeyDataTransitMode.rawKeyData]. + @Deprecated( + 'No longer supported. Transit mode is always key data only. ' + 'This feature was deprecated after v3.18.0-2.0.pre.', + ) + KeySimulatorTransitModeVariant.rawKeyData() + : this({KeyDataTransitMode.rawKeyData}); + @override final Set values; diff --git a/packages/flutter_test/test/event_simulation_test.dart b/packages/flutter_test/test/event_simulation_test.dart index 8844b950514..eb22da709bd 100644 --- a/packages/flutter_test/test/event_simulation_test.dart +++ b/packages/flutter_test/test/event_simulation_test.dart @@ -37,12 +37,25 @@ Future _shouldThrow(AsyncValueGetter func) async { } void main() { + testWidgets('default transit mode is keyDataThenRawKeyData', (WidgetTester tester) async { + expect(KeyEventSimulator.transitMode, KeyDataTransitMode.keyDataThenRawKeyData); + }); + + testWidgets('debugKeyEventSimulatorTransitModeOverride overrides default transit mode', (WidgetTester tester) async { + debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.rawKeyData; + expect(KeyEventSimulator.transitMode, KeyDataTransitMode.rawKeyData); + // Unsetting debugKeyEventSimulatorTransitModeOverride can't be called in a + // tear down callback because TestWidgetsFlutterBinding._verifyInvariants + // is called before tear down callbacks. + debugKeyEventSimulatorTransitModeOverride = null; + }); + testWidgets('simulates keyboard events (RawEvent)', (WidgetTester tester) async { debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.rawKeyData; final List events = []; - final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( RawKeyboardListener( @@ -80,7 +93,6 @@ void main() { } await tester.pumpWidget(Container()); - focusNode.dispose(); debugKeyEventSimulatorTransitModeOverride = null; }); @@ -89,8 +101,8 @@ void main() { debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.keyDataThenRawKeyData; final List events = []; - final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( KeyboardListener( @@ -243,7 +255,6 @@ void main() { await tester.idle(); await tester.pumpWidget(Container()); - focusNode.dispose(); debugKeyEventSimulatorTransitModeOverride = null; }); @@ -252,8 +263,9 @@ void main() { debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.rawKeyData; final List events = []; - final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( Focus( focusNode: focusNode, @@ -308,8 +320,9 @@ void main() { debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.keyDataThenRawKeyData; final List events = []; - final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( Focus( focusNode: focusNode,