Skip to content

TestTextInput does not clear _keyHandler on reset #171491

@angelosilvestre

Description

@angelosilvestre

Steps to reproduce

  1. Run the tests from the sample code

Expected results

Both tests should pass.

Actual results

The first test passes, but the second fails.

Code sample

Code sample
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('first test', (tester) async {
    final myClient = _ExampleInputClient();

    await _runTest(tester, myClient);

    expect(myClient.performSelectorCalled, isTrue);
  });

  testWidgets('second test', (tester) async {
    final myClient = _ExampleInputClient();

    await _runTest(tester, myClient);

    expect(myClient.performSelectorCalled, isTrue);
  });
}

Future<void> _runTest(WidgetTester tester, TextInputClient client) async {
  debugDefaultTargetPlatformOverride = TargetPlatform.macOS;
  try {
    final focusNode = FocusNode();

    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Scaffold(
          body: Focus(
            focusNode: focusNode,
            autofocus: true,
            child: Container(),
          ),
        ),
      ),
    );

    /// Attach to the IME.
    final connection = TextInput.attach(client, const TextInputConfiguration());
    addTearDown(() {
      connection.close();
    });
    connection.show();

    // Press the left arrow, which should trigger a "moveLeft:" selector call.
    await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft, platform: 'macos');
    await tester.pumpAndSettle();

    // Intercept the messages after the text input connection is established.
    // This will prevent the test text input from receiving the "clearClient" message,
    // to clear the keyboard handler.
    tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
      SystemChannels.textInput,
      (MethodCall methodCall) async {
        return null;
      },
    );
  } finally {
    debugDefaultTargetPlatformOverride = null;
  }
}

/// A [TextInputClient] that reports whether the `performSelector` method was called.
class _ExampleInputClient with TextInputClient {
  bool get performSelectorCalled => _performSelectorCalled;
  bool _performSelectorCalled = false;

  @override
  AutofillScope? get currentAutofillScope => null;

  @override
  TextEditingValue? get currentTextEditingValue => _currentTextEditingValue;
  TextEditingValue _currentTextEditingValue = const TextEditingValue();

  @override
  void performSelector(String selectorName) {
    super.performSelector(selectorName);
    _performSelectorCalled = true;
  }

  @override
  void connectionClosed() {}

  @override
  void updateEditingValue(TextEditingValue value) {
    _currentTextEditingValue = value;
  }

  @override
  void performAction(TextInputAction action) {}

  @override
  void performPrivateCommand(String action, Map<String, dynamic> data) {}

  @override
  void showAutocorrectionPromptRect(int start, int end) {}

  @override
  void updateFloatingCursor(RawFloatingCursorPoint point) {}
}

Context

Developers might want to override the mock message handlers to perform some checks during tests. Overriding the handler for the text input channel can cause the text input to not work correctly in the subsequent tests.

The issue seems to be in TestTextInput.reset:

void reset() {
log.clear();
_client = null;
setClientArgs = null;
editingState = null;
_isVisible = false;
}

This method does not clear the _keyHandler. When the text input is reattached, it does not override the key handler. As the result, we keep a handler with the outdated client id, which causes the text input client to drop messages:

https://github.com/flutter/flutter/blob/0b2810862c4b2481098327b6ad6927a541e6858f/packages/flutter_test/lib/src/test_text_input.dart#L147C23-L151

final int client = args[0] as int;
if (client != _currentConnection!._id) {
// If the client IDs don't match, the incoming message was for a different
// client.
bool debugAllowAnyway = false;
assert(() {
// In debug builds we allow "-1" as a magical client ID that ignores
// this verification step so that tests can always get through, even
// when they are not mocking the engine side of text input.
if (client == -1) {
debugAllowAnyway = true;
}
return true;
}());
if (!debugAllowAnyway) {
return;
}
}

Logs

Flutter Doctor output

Doctor output
$ flutter doctor -v
[✓] Flutter (Channel stable, 3.32.4, on macOS 15.5 24F74 darwin-arm64, locale en-BR) [467ms]
    • Flutter version 3.32.4 on channel stable at /Users/angelosilvestre/dev/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 6fba2447e9 (3 weeks ago), 2025-06-12 19:03:56 -0700
    • Engine revision 8cd19e509d
    • Dart version 3.8.1
    • DevTools version 2.45.1

[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0) [4.0s]
    • Android SDK at /Users/angelosilvestre/Library/Android/sdk
    • Platform android-35, build-tools 34.0.0
    • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
      This is the JDK bundled with the latest Android Studio installation on this machine.
      To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
    • Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b829.9-10027231)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 16.4) [961ms]
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 16F6
    • CocoaPods version 1.16.2

[✓] Chrome - develop for the web [10ms]
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2022.3) [10ms]
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b829.9-10027231)

[✓] VS Code (version 1.100.2) [8ms]
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.112.0

[✓] Connected device (2 available) [6.2s]
    • macOS (desktop) • macos  • darwin-arm64   • macOS 15.5 24F74 darwin-arm64
    • Chrome (web)    • chrome • web-javascript • Google Chrome 137.0.7151.120

[✓] Network resources [984ms]
    • All expected network resources are available.

• No issues found!

Metadata

Metadata

Assignees

Labels

a: tests"flutter test", flutter_test, or one of our testsa: text inputEntering text in a text field or keyboard related problemsframeworkflutter/packages/flutter repository. See also f: labels.r: fixedIssue is closed as already fixed in a newer versionteam-text-inputOwned by Text Input team

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions