You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter测试中如何触发TextField的UndoHistoryController?

解决UndoHistoryController测试中无法触发撤销/重做的问题

问题场景

我有一个集成了TextField和UndoHistoryController的Flutter组件:

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();
                      },
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

编写测试用例后,点击“Undo”按钮的断言步骤失败,此时_undoController.value.canUndo为false。尝试替换pump()为pumpAndSettle()或多次调用pump(),均无法触发UndoHistoryController的响应:

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

问题根源

UndoHistoryController的历史记录基于文本编辑的操作快照生成,但tester.enterText()是直接赋值文本值,不会触发TextField内部的输入/删除等操作事件,因此UndoHistoryController无法捕获到可撤销的历史动作。

修复方案

方案一:模拟真实按键输入

通过sendKeyEvent()模拟真实的按键输入,触发UndoHistoryController需要的编辑事件:

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

  // 先聚焦TextField
  await tester.tap(find.byType(TextField));
  await tester.pump();

  // 逐个输入字符模拟真实操作
  for (var char in '1st change'.split('')) {
    await tester.sendKeyEvent(LogicalKeyboardKey.character(char));
    await tester.pump();
  }
  expect(find.text('1st change'), findsOne);

  // 全选并删除原有内容,再输入新文本
  await tester.sendKeyEvent(LogicalKeyboardKey.selectAll);
  await tester.pump();
  await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
  await tester.pump();
  for (var char in '2nd change'.split('')) {
    await tester.sendKeyEvent(LogicalKeyboardKey.character(char));
    await tester.pump();
  }
  expect(find.text('2nd change'), findsOne);

  // 执行撤销
  await tester.tap(find.text('Undo'));
  await tester.pump();
  expect(find.text('1st change'), findsOne);

  // 执行重做
  await tester.tap(find.text('Redo'));
  await tester.pump();
  expect(find.text('2nd change'), findsOne);
});

方案二:手动触发快照(简化测试)

如果觉得模拟按键繁琐,可以修改组件代码,监听文本变化手动添加快照:

  1. 在组件的initState中添加监听:
@override
void initState() {
  super.initState();
  // 监听文本变化,手动向UndoHistoryController添加快照
  _controller.addListener(() {
    _undoController.addSnapshot(
      TextEditingValue.fromJSON(_controller.value.toJSON()),
    );
  });
}
  1. 测试代码可保留enterText,确保触发监听:
await tester.enterText(find.byType(TextField), '1st change');
await tester.pump();
// 手动触发控制器监听(确保快照被添加)
tester.state<_MyHomePageState>(find.byType(MyHomePage))._controller.notifyListeners();
await tester.pump();

额外注意事项

  • 确保TextField的undoController已正确绑定到_undoController;
  • 点击Undo/Redo按钮后,pump()通常足够触发状态更新,若存在动画可使用pumpAndSettle()。

内容的提问来源于stack exchange,提问作者Valentin Vignal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 07:44:54