[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<KeyEvent> events = <KeyEvent>[];
    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.
This commit is contained in:
Bruno Leroux 2024-03-07 08:19:26 +01:00 committed by GitHub
parent f677027655
commit 8ade81fb20
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 101 additions and 39 deletions

View file

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

View file

@ -694,7 +694,7 @@ void main() {
await tester.tap(find.byType(DropdownMenu<TestMenu>));
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<TestMenu>));
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<TestMenu>));
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<TestMenu>));
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<TestMenu>));
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(

View file

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

View file

@ -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 <String>['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 <String>['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 <String>['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>{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>{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>{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>{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>{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<void> simulateGTKKeyEvent(bool keyDown, int scancode, int keycode, int modifiers) async {
final Map<String, dynamic> data = <String, dynamic>{
@ -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);

View file

@ -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<KeyDataTransitMode> {
KeySimulatorTransitModeVariant.keyDataThenRawKeyData()
: this(<KeyDataTransitMode>{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>{KeyDataTransitMode.rawKeyData});
@override
final Set<KeyDataTransitMode> values;

View file

@ -37,12 +37,25 @@ Future<void> _shouldThrow<T extends Error>(AsyncValueGetter<void> 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<RawKeyEvent> events = <RawKeyEvent>[];
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<KeyEvent> events = <KeyEvent>[];
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<Object> events = <Object>[];
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
Focus(
focusNode: focusNode,
@ -308,8 +320,9 @@ void main() {
debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.keyDataThenRawKeyData;
final List<Object> events = <Object>[];
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
Focus(
focusNode: focusNode,