flutterdartflutter-textformfieldflutter-textinputfield

How to trigger the undo controller of a text field in flutter test


I have this widget containing a TextField and a UndoHistoryController :

class _MyHomePageState extends State<MyHomePage> {
  final TextEditingController _controller = TextEditingController();
  final FocusNode _focusNode = FocusNode();
  final UndoHistoryController _undoController = UndoHistoryController();

  TextStyle? get enabledStyle => Theme.of(context).textTheme.bodyMedium;
  TextStyle? get disabledStyle => Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.grey);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TextField(
              maxLines: 4,
              controller: _controller,
              focusNode: _focusNode,
              undoController: _undoController,
            ),
            ValueListenableBuilder<UndoHistoryValue>(
              valueListenable: _undoController,
              builder: (BuildContext context, UndoHistoryValue value, Widget? child) {
                return Row(
                  children: <Widget>[
                    TextButton(
                      child: Text('Undo', style: value.canUndo ? enabledStyle : disabledStyle),
                      onPressed: () {
                        _undoController.undo();
                      },
                    ),
                    TextButton(
                      child: Text('Redo', style: value.canRedo ? enabledStyle : disabledStyle),
                      onPressed: () {
                        _undoController.redo();
                      },
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

I'm trying to write a test about it:

testWidgets('The undo history controller should undo and redo the history changes', (WidgetTester tester) async {
  await tester.pumpWidget(
    const example.UndoHistoryControllerExampleApp(),
  );

  expect(find.byType(TextField), findsOne);
  expect(find.widgetWithText(TextButton, 'Undo'), findsOne);
  expect(find.widgetWithText(TextButton, 'Redo'), findsOne);

  await tester.enterText(find.byType(TextField), '1st change');
  await tester.pump();
  expect(find.text('1st change'), findsOne);

  await tester.enterText(find.byType(TextField), '2nd change');
  await tester.pump();
  expect(find.text('2nd change'), findsOne);

  await tester.tap(find.text('Undo'));
  await tester.pump();
  expect(find.text('2nd change'), findsOne);

  await tester.tap(find.text('Redo'));
  await tester.pump();
  expect(find.text('2nd change'), findsOne);
});

which fails when at the expect(find.text('2nd change'), findsOne); after the tap on the "Undo" button. It looks like _undoController.value.canUndo is false in the onPressed of the "Undo" button.

I tried replacing the pump() with pumpAndSettle() or multiple pump()s after each enterText in the test, but it doesn't seem to trigger anything in the undo history controller.

How to make it work?


Solution

  • I had to wait for the _kThrottleDuration = Duration(milliseconds: 500). It looks like the undo history controller waits a bit before updating its state:

    In UndoHistoryState

      // This duration was chosen as a best fit for the behavior of Mac, Linux,
      // and Windows undo/redo state save durations, but it is not perfect for any
      // of them.
      static const Duration _kThrottleDuration = Duration(milliseconds: 500);
    

    The updated test is:

    testWidgets('The undo history controller should undo and redo the history changes', (WidgetTester tester) async {
      await tester.pumpWidget(
        const example.UndoHistoryControllerExampleApp(),
      );
    
      expect(find.byType(TextField), findsOne);
      expect(find.widgetWithText(TextButton, 'Undo'), findsOne);
      expect(find.widgetWithText(TextButton, 'Redo'), findsOne);
    
      await tester.enterText(find.byType(TextField), '1st change');
      await tester.pump(const Duration(milliseconds: 500));
      expect(find.text('1st change'), findsOne);
    
      await tester.enterText(find.byType(TextField), '2nd change');
      await tester.pump(const Duration(milliseconds: 500));
      expect(find.text('2nd change'), findsOne);
    
      await tester.tap(find.text('Undo'));
      await tester.pump();
      expect(find.text('2nd change'), findsOne);
    
      await tester.tap(find.text('Redo'));
      await tester.pump();
      expect(find.text('2nd change'), findsOne);
    });