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); });
方案二:手动触发快照(简化测试)
如果觉得模拟按键繁琐,可以修改组件代码,监听文本变化手动添加快照:
- 在组件的
initState中添加监听:
@override void initState() { super.initState(); // 监听文本变化,手动向UndoHistoryController添加快照 _controller.addListener(() { _undoController.addSnapshot( TextEditingValue.fromJSON(_controller.value.toJSON()), ); }); }
- 测试代码可保留
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
相关产品推荐
相关产品推荐

