From 2a8ec1d3a01244e5d1a1e9d6163265fa6310a7bb Mon Sep 17 00:00:00 2001 From: pdenert Date: Fri, 20 Dec 2024 01:48:55 +0100 Subject: [PATCH 1/6] Add tests for different enter text scenarios Co-authored-by: Mateusz Wojtczak --- dev/e2e_app/integration_test/common.dart | 2 +- .../integration_test/text_fields_test.dart | 32 ++++++++++++ dev/e2e_app/lib/text_fields_screen.dart | 49 +++++++++++++++++++ .../test/patrol_finder_test.dart | 44 +++++++++++++++++ .../test/utils/text_fields_screen.dart | 49 +++++++++++++++++++ 5 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 dev/e2e_app/integration_test/text_fields_test.dart create mode 100644 dev/e2e_app/lib/text_fields_screen.dart create mode 100644 packages/patrol_finders/test/utils/text_fields_screen.dart diff --git a/dev/e2e_app/integration_test/common.dart b/dev/e2e_app/integration_test/common.dart index 15d43c7a2..3654c23f9 100644 --- a/dev/e2e_app/integration_test/common.dart +++ b/dev/e2e_app/integration_test/common.dart @@ -5,7 +5,7 @@ import 'package:patrol/patrol.dart'; export 'package:flutter_test/flutter_test.dart'; export 'package:patrol/patrol.dart'; -final _patrolTesterConfig = PatrolTesterConfig(); +final _patrolTesterConfig = PatrolTesterConfig(printLogs: true); final _nativeAutomatorConfig = NativeAutomatorConfig( findTimeout: Duration(seconds: 20), // 10 seconds is too short for some CIs ); diff --git a/dev/e2e_app/integration_test/text_fields_test.dart b/dev/e2e_app/integration_test/text_fields_test.dart new file mode 100644 index 000000000..2a4b240fc --- /dev/null +++ b/dev/e2e_app/integration_test/text_fields_test.dart @@ -0,0 +1,32 @@ +import 'package:e2e_app/text_fields_screen.dart'; +import 'package:flutter/material.dart'; + +import 'common.dart'; + +void main() { + patrol('Can enter text into same field twice', ($) async { + await $.pumpWidgetAndSettle(const TextFieldsScreen()); + await $.pumpAndSettle(); + + // Enter text into the first text field. + await $(const Key('textField1')).enterText('User'); + await $(const Key('buttonFocus')).tap(); + expect($('User'), findsOneWidget); + + // Enter text into the first text field again. After focusing on the button. + await $(const Key('textField1')).enterText('User2'); + await $(const Key('buttonUnfocus')).tap(); + expect($('User'), findsNothing); + expect($('User2'), findsOneWidget); + + // Enter text into the first text field again. After unfocusing the button. + // Then enter text into the second text field and into the first field again. + await $(const Key('textField1')).enterText('User3'); + await $(const Key('textField2')).enterText('User4'); + await $(const Key('textField1')).enterText('User5'); + expect($('User2'), findsNothing); + expect($('User3'), findsNothing); + expect($('User4'), findsOneWidget); + expect($('User5'), findsOneWidget); + }); +} diff --git a/dev/e2e_app/lib/text_fields_screen.dart b/dev/e2e_app/lib/text_fields_screen.dart new file mode 100644 index 000000000..30013ebf1 --- /dev/null +++ b/dev/e2e_app/lib/text_fields_screen.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +class TextFieldsScreen extends StatefulWidget { + const TextFieldsScreen({super.key}); + + @override + State createState() => _TextFieldsScreenState(); +} + +class _TextFieldsScreenState extends State { + FocusNode focusNode = FocusNode(); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Text Fields'), + ), + body: Center( + child: SingleChildScrollView( + child: Column( + children: [ + const TextField( + key: Key('textField1'), + ), + ElevatedButton.icon( + key: const Key('buttonFocus'), + onPressed: () => + FocusScope.of(context).requestFocus(focusNode), + label: const Icon(Icons.search), + focusNode: focusNode, + ), + const TextField( + key: Key('textField2'), + ), + ElevatedButton.icon( + key: const Key('buttonUnfocus'), + onPressed: () => FocusScope.of(context).unfocus(), + label: const Icon(Icons.search), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/packages/patrol_finders/test/patrol_finder_test.dart b/packages/patrol_finders/test/patrol_finder_test.dart index 9e8ef5d56..0485f7a21 100644 --- a/packages/patrol_finders/test/patrol_finder_test.dart +++ b/packages/patrol_finders/test/patrol_finder_test.dart @@ -4,6 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:patrol_finders/src/custom_finders/custom_finders.dart'; +import 'utils/text_fields_screen.dart'; + // See how finders are tested in `package:flutter_test`: // https://github.com/flutter/flutter/blob/master/packages/flutter_test/test/finders_test.dart @@ -1105,5 +1107,47 @@ void main() { }, ); }); + + patrolWidgetTest( + 'can enter text into the same field, after focusing on button', + ($) async { + await $.pumpWidgetAndSettle(const TextFieldsScreen()); + + await $(const Key('textField1')).enterText('User'); + await $(const Key('buttonFocus')).tap(); + expect($('User'), findsOneWidget); + + await $(const Key('textField1')).enterText('User2'); + expect($('User'), findsNothing); + expect($('User2'), findsOneWidget); + }); + + patrolWidgetTest('can enter text into same field, after unfocusing', + ($) async { + await $.pumpWidgetAndSettle(const TextFieldsScreen()); + + await $(const Key('textField1')).enterText('User2'); + await $(const Key('buttonUnfocus')).tap(); + expect($('User2'), findsOneWidget); + + await $(const Key('textField1')).enterText('User3'); + expect($('User2'), findsNothing); + expect($('User3'), findsOneWidget); + }); + + patrolWidgetTest( + 'can enter text into same field, after entering text in another field', + ($) async { + await $.pumpWidgetAndSettle(const TextFieldsScreen()); + + await $(const Key('textField1')).enterText('User3'); + expect($('User3'), findsOneWidget); + + await $(const Key('textField2')).enterText('User4'); + await $(const Key('textField1')).enterText('User5'); + expect($('User3'), findsNothing); + expect($('User4'), findsOneWidget); + expect($('User5'), findsOneWidget); + }); }); } diff --git a/packages/patrol_finders/test/utils/text_fields_screen.dart b/packages/patrol_finders/test/utils/text_fields_screen.dart new file mode 100644 index 000000000..30013ebf1 --- /dev/null +++ b/packages/patrol_finders/test/utils/text_fields_screen.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +class TextFieldsScreen extends StatefulWidget { + const TextFieldsScreen({super.key}); + + @override + State createState() => _TextFieldsScreenState(); +} + +class _TextFieldsScreenState extends State { + FocusNode focusNode = FocusNode(); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Text Fields'), + ), + body: Center( + child: SingleChildScrollView( + child: Column( + children: [ + const TextField( + key: Key('textField1'), + ), + ElevatedButton.icon( + key: const Key('buttonFocus'), + onPressed: () => + FocusScope.of(context).requestFocus(focusNode), + label: const Icon(Icons.search), + focusNode: focusNode, + ), + const TextField( + key: Key('textField2'), + ), + ElevatedButton.icon( + key: const Key('buttonUnfocus'), + onPressed: () => FocusScope.of(context).unfocus(), + label: const Icon(Icons.search), + ), + ], + ), + ), + ), + ), + ); + } +} From ae66c1744880ede59bd240398b5258f4e1d84577 Mon Sep 17 00:00:00 2001 From: pdenert Date: Fri, 20 Dec 2024 01:49:36 +0100 Subject: [PATCH 2/6] Add patch for enter text show keyboard --- .../src/custom_finders/custom_finders.dart | 1 + .../lib/src/custom_finders/patrol_tester.dart | 8 +- .../lib/src/custom_finders/widget_tester.dart | 784 ++++++++++++++++++ 3 files changed, 790 insertions(+), 3 deletions(-) create mode 100644 packages/patrol_finders/lib/src/custom_finders/widget_tester.dart diff --git a/packages/patrol_finders/lib/src/custom_finders/custom_finders.dart b/packages/patrol_finders/lib/src/custom_finders/custom_finders.dart index 1f0ce1be2..2cf658b6a 100644 --- a/packages/patrol_finders/lib/src/custom_finders/custom_finders.dart +++ b/packages/patrol_finders/lib/src/custom_finders/custom_finders.dart @@ -2,3 +2,4 @@ export '../common.dart'; export 'exceptions.dart'; export 'patrol_finder.dart'; export 'patrol_tester.dart'; +export 'widget_tester.dart'; diff --git a/packages/patrol_finders/lib/src/custom_finders/patrol_tester.dart b/packages/patrol_finders/lib/src/custom_finders/patrol_tester.dart index 4c99e9690..60b2d71af 100644 --- a/packages/patrol_finders/lib/src/custom_finders/patrol_tester.dart +++ b/packages/patrol_finders/lib/src/custom_finders/patrol_tester.dart @@ -127,9 +127,10 @@ const defaultScrollMaxIteration = 15; class PatrolTester { /// Creates a new [PatrolTester] which wraps [tester]. PatrolTester({ - required this.tester, + required WidgetTester tester, required this.config, - }) : patrolLog = PatrolLogWriter(); + }) : patrolLog = PatrolLogWriter(), + tester = PatrolWidgetTester(tester); /// Global configuration of this tester. final PatrolTesterConfig config; @@ -138,7 +139,7 @@ class PatrolTester { final PatrolLogWriter patrolLog; /// Flutter's widget tester that this [PatrolTester] wraps. - final WidgetTester tester; + final PatrolWidgetTester tester; /// Wraps a function with a log entry for the start and end of the function. Future wrapWithPatrolLog({ @@ -427,6 +428,7 @@ class PatrolTester { timeout: visibleTimeout, enablePatrolLog: false, ); + await tester.tap(resolvedFinder.first); await tester.enterText(resolvedFinder.first, text); if (!kIsWeb) { // When registering `testTextInput`, we have to unregister it diff --git a/packages/patrol_finders/lib/src/custom_finders/widget_tester.dart b/packages/patrol_finders/lib/src/custom_finders/widget_tester.dart new file mode 100644 index 000000000..66a1becfd --- /dev/null +++ b/packages/patrol_finders/lib/src/custom_finders/widget_tester.dart @@ -0,0 +1,784 @@ +/// This file overrides the internal implementation of [WidgetTester] +// ignore_for_file: implementation_imports +library; + +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/src/gestures/events.dart'; +import 'package:flutter/src/gestures/hit_test.dart'; +import 'package:flutter/src/rendering/layer.dart'; +import 'package:flutter/src/rendering/object.dart'; +import 'package:flutter/src/scheduler/ticker.dart'; +import 'package:flutter/src/semantics/semantics.dart'; +import 'package:flutter/src/services/keyboard_key.g.dart'; +import 'package:flutter_test/flutter_test.dart'; + +// This is a temporary class made for patch mixins to work +abstract class _PWT implements WidgetTester { + const _PWT(); +} + +/// Wraps Flutter [WidgetTester] class to +/// allow applying custom patches (e.g. see [_ShowKeyboardPatch]) +class PatrolWidgetTester extends _PWT with _ShowKeyboardPatch { + /// Wraps [tester] with custom patch logic + const PatrolWidgetTester(WidgetTester tester) : _tester = tester; + + final WidgetTester _tester; + + @override + Iterable get allElements => _tester.allElements; + + @override + Iterable get allRenderObjects => _tester.allRenderObjects; + + @override + Iterable> get allStates => _tester.allStates; + + @override + Iterable get allWidgets => _tester.allWidgets; + + @override + bool any(FinderBase finder) => _tester.any(finder); + + @override + TestWidgetsFlutterBinding get binding => _tester.binding; + + @override + Future createGesture({ + int? pointer, + PointerDeviceKind kind = PointerDeviceKind.touch, + int buttons = kPrimaryButton, + }) { + return _tester.createGesture( + pointer: pointer, + kind: kind, + buttons: buttons, + ); + } + + @override + Ticker createTicker(TickerCallback onTick) => _tester.createTicker(onTick); + + @override + void dispatchEvent(PointerEvent event, HitTestResult result) { + _tester.dispatchEvent(event, result); + } + + @override + Future drag( + FinderBase finder, + Offset offset, { + int? pointer, + int buttons = kPrimaryButton, + double touchSlopX = kDragSlopDefault, + double touchSlopY = kDragSlopDefault, + bool warnIfMissed = true, + PointerDeviceKind kind = PointerDeviceKind.touch, + }) { + return _tester.drag( + finder, + offset, + pointer: pointer, + buttons: buttons, + touchSlopX: touchSlopX, + touchSlopY: touchSlopY, + warnIfMissed: warnIfMissed, + kind: kind, + ); + } + + @override + Future dragFrom( + Offset startLocation, + Offset offset, { + int? pointer, + int buttons = kPrimaryButton, + double touchSlopX = kDragSlopDefault, + double touchSlopY = kDragSlopDefault, + PointerDeviceKind kind = PointerDeviceKind.touch, + }) { + return _tester.dragFrom( + startLocation, + offset, + pointer: pointer, + buttons: buttons, + touchSlopX: touchSlopX, + touchSlopY: touchSlopY, + kind: kind, + ); + } + + @override + Future dragUntilVisible( + FinderBase finder, + FinderBase view, + Offset moveStep, { + int maxIteration = 50, + Duration duration = const Duration(milliseconds: 50), + }) { + return _tester.dragUntilVisible( + finder, + view, + moveStep, + maxIteration: maxIteration, + duration: duration, + ); + } + + @override + T element(FinderBase finder) => + _tester.element(finder); + + @override + Iterable elementList(FinderBase finder) => + _tester.elementList(finder); + + @override + SemanticsHandle ensureSemantics() => _tester.ensureSemantics(); + + @override + Future ensureVisible(FinderBase finder) => + _tester.ensureVisible(finder); + + @override + Future enterText(FinderBase finder, String text) => + _tester.enterText(finder, text); + + @override + T firstElement(FinderBase finder) => + _tester.firstElement(finder); + + @override + T firstRenderObject(FinderBase finder) => + _tester.firstRenderObject(finder); + + @override + T firstState>(FinderBase finder) => + _tester.firstState(finder); + + @override + T firstWidget(FinderBase finder) => + _tester.firstWidget(finder); + + @override + Future fling( + FinderBase finder, + Offset offset, + double speed, { + int? pointer, + int buttons = kPrimaryButton, + Duration frameInterval = const Duration(milliseconds: 16), + Offset initialOffset = Offset.zero, + Duration initialOffsetDelay = const Duration(seconds: 1), + bool warnIfMissed = true, + PointerDeviceKind deviceKind = PointerDeviceKind.touch, + }) { + return _tester.fling( + finder, + offset, + speed, + pointer: pointer, + buttons: buttons, + frameInterval: frameInterval, + initialOffset: initialOffset, + initialOffsetDelay: initialOffsetDelay, + warnIfMissed: warnIfMissed, + deviceKind: deviceKind, + ); + } + + @override + Future flingFrom( + Offset startLocation, + Offset offset, + double speed, { + int? pointer, + int buttons = kPrimaryButton, + Duration frameInterval = const Duration(milliseconds: 16), + Offset initialOffset = Offset.zero, + Duration initialOffsetDelay = const Duration(seconds: 1), + PointerDeviceKind deviceKind = PointerDeviceKind.touch, + }) { + return _tester.flingFrom( + startLocation, + offset, + speed, + pointer: pointer, + buttons: buttons, + frameInterval: frameInterval, + initialOffset: initialOffset, + initialOffsetDelay: initialOffsetDelay, + deviceKind: deviceKind, + ); + } + + @override + Offset getBottomLeft( + FinderBase finder, { + bool warnIfMissed = false, + String callee = 'getBottomLeft', + }) { + return _tester.getBottomLeft( + finder, + warnIfMissed: warnIfMissed, + callee: callee, + ); + } + + @override + Offset getBottomRight( + FinderBase finder, { + bool warnIfMissed = false, + String callee = 'getBottomRight', + }) { + return _tester.getBottomRight( + finder, + warnIfMissed: warnIfMissed, + callee: callee, + ); + } + + @override + Offset getCenter( + FinderBase finder, { + bool warnIfMissed = false, + String callee = 'getCenter', + }) { + return _tester.getCenter( + finder, + warnIfMissed: warnIfMissed, + callee: callee, + ); + } + + @override + Rect getRect(FinderBase finder) => _tester.getRect(finder); + + @override + Future getRestorationData() => + _tester.getRestorationData(); + + @override + SemanticsNode getSemantics(FinderBase finder) => + _tester.getSemantics(finder); + + @override + Size getSize(FinderBase finder) => _tester.getSize(finder); + + @override + Offset getTopLeft( + FinderBase finder, { + bool warnIfMissed = false, + String callee = 'getTopLeft', + }) { + return _tester.getTopLeft( + finder, + warnIfMissed: warnIfMissed, + callee: callee, + ); + } + + @override + Offset getTopRight( + FinderBase finder, { + bool warnIfMissed = false, + String callee = 'getTopRight', + }) { + return _tester.getTopRight( + finder, + warnIfMissed: warnIfMissed, + callee: callee, + ); + } + + @override + Future> handlePointerEventRecord( + Iterable records, + ) { + return _tester.handlePointerEventRecord(records); + } + + @override + bool get hasRunningAnimations => _tester.hasRunningAnimations; + + @override + HitTestResult hitTestOnBinding(Offset location, {int? viewId}) { + return _tester.hitTestOnBinding(location, viewId: viewId); + } + + @override + Future idle() => _tester.idle(); + + @override + Iterable layerListOf(FinderBase finder) => + _tester.layerListOf(finder); + + @override + List get layers => _tester.layers; + + @override + Future longPress( + FinderBase finder, { + int? pointer, + int buttons = kPrimaryButton, + bool warnIfMissed = true, + PointerDeviceKind kind = PointerDeviceKind.touch, + }) { + return _tester.longPress( + finder, + pointer: pointer, + buttons: buttons, + warnIfMissed: warnIfMissed, + kind: kind, + ); + } + + @override + Future longPressAt( + Offset location, { + int? pointer, + int buttons = kPrimaryButton, + PointerDeviceKind kind = PointerDeviceKind.touch, + }) { + return _tester.longPressAt( + location, + pointer: pointer, + buttons: buttons, + kind: kind, + ); + } + + @override + int get nextPointer => _tester.nextPointer; + + @override + Future pageBack() => _tester.pageBack(); + + @override + TestPlatformDispatcher get platformDispatcher => _tester.platformDispatcher; + + @override + Future press( + FinderBase finder, { + int? pointer, + int buttons = kPrimaryButton, + bool warnIfMissed = true, + PointerDeviceKind kind = PointerDeviceKind.touch, + }) { + return _tester.press( + finder, + pointer: pointer, + buttons: buttons, + warnIfMissed: warnIfMissed, + kind: kind, + ); + } + + @override + void printToConsole(String message) { + _tester.printToConsole(message); + } + + @override + Future pump([ + Duration? duration, + EnginePhase phase = EnginePhase.sendSemanticsUpdate, + ]) { + return _tester.pump(duration, phase); + } + + @override + Future pumpAndSettle([ + Duration duration = const Duration(milliseconds: 100), + EnginePhase phase = EnginePhase.sendSemanticsUpdate, + Duration timeout = const Duration(minutes: 10), + ]) { + return _tester.pumpAndSettle(duration, phase, timeout); + } + + @override + Future pumpBenchmark(Duration duration) => + _tester.pumpBenchmark(duration); + + @override + Future pumpFrames( + Widget target, + Duration maxDuration, [ + Duration interval = const Duration(milliseconds: 16, microseconds: 683), + ]) { + return _tester.pumpFrames(target, maxDuration, interval); + } + + @override + Future pumpWidget( + Widget widget, { + Duration? duration, + EnginePhase phase = EnginePhase.sendSemanticsUpdate, + bool wrapWithView = true, + }) { + return _tester.pumpWidget( + widget, + duration: duration, + phase: phase, + wrapWithView: wrapWithView, + ); + } + + @override + T renderObject(FinderBase finder) => + _tester.renderObject(finder); + + @override + Iterable renderObjectList( + FinderBase finder, + ) => + _tester.renderObjectList(finder); + + @override + Future restartAndRestore() => _tester.restartAndRestore(); + + @override + Future restoreFrom(TestRestorationData data) => + _tester.restoreFrom(data); + + @override + Future runAsync( + Future Function() callback, { + Duration additionalTime = const Duration(milliseconds: 1000), + }) { + return _tester.runAsync(callback, additionalTime: additionalTime); + } + + @override + Future scrollUntilVisible( + FinderBase finder, + double delta, { + FinderBase? scrollable, + int maxScrolls = 50, + Duration duration = const Duration(milliseconds: 50), + }) { + return _tester.scrollUntilVisible( + finder, + delta, + scrollable: scrollable, + maxScrolls: maxScrolls, + duration: duration, + ); + } + + @override + SemanticsController get semantics => _tester.semantics; + + @override + Future sendEventToBinding(PointerEvent event) => + _tester.sendEventToBinding(event); + + @override + Future sendKeyDownEvent( + LogicalKeyboardKey key, { + String? platform, + String? character, + PhysicalKeyboardKey? physicalKey, + }) { + return _tester.sendKeyDownEvent( + key, + platform: platform, + character: character, + physicalKey: physicalKey, + ); + } + + @override + Future sendKeyEvent( + LogicalKeyboardKey key, { + String? platform, + String? character, + PhysicalKeyboardKey? physicalKey, + }) { + return _tester.sendKeyEvent( + key, + platform: platform, + character: character, + physicalKey: physicalKey, + ); + } + + @override + Future sendKeyRepeatEvent( + LogicalKeyboardKey key, { + String? platform, + String? character, + PhysicalKeyboardKey? physicalKey, + }) { + return _tester.sendKeyRepeatEvent( + key, + platform: platform, + character: character, + physicalKey: physicalKey, + ); + } + + @override + Future sendKeyUpEvent( + LogicalKeyboardKey key, { + String? platform, + PhysicalKeyboardKey? physicalKey, + }) { + return _tester.sendKeyUpEvent( + key, + platform: platform, + physicalKey: physicalKey, + ); + } + + @override + Future showKeyboard(FinderBase finder) => + _tester.showKeyboard(finder); + + @override + Future startGesture( + Offset downLocation, { + int? pointer, + PointerDeviceKind kind = PointerDeviceKind.touch, + int buttons = kPrimaryButton, + }) { + return _tester.startGesture( + downLocation, + pointer: pointer, + kind: kind, + buttons: buttons, + ); + } + + @override + T state>(FinderBase finder) => + _tester.state(finder); + + @override + Iterable stateList>( + FinderBase finder, + ) => + _tester.stateList(finder); + + @override + List takeAnnouncements() => + _tester.takeAnnouncements(); + + @override + dynamic takeException() => _tester.takeException(); + + @override + Future tap( + FinderBase finder, { + int? pointer, + int buttons = kPrimaryButton, + bool warnIfMissed = true, + PointerDeviceKind kind = PointerDeviceKind.touch, + }) { + return _tester.tap( + finder, + pointer: pointer, + buttons: buttons, + warnIfMissed: warnIfMissed, + kind: kind, + ); + } + + @override + Future tapAt( + Offset location, { + int? pointer, + int buttons = kPrimaryButton, + PointerDeviceKind kind = PointerDeviceKind.touch, + }) { + return _tester.tapAt( + location, + pointer: pointer, + buttons: buttons, + kind: kind, + ); + } + + @override + Future tapOnText( + FinderBase textRangeFinder, { + int? pointer, + int buttons = kPrimaryButton, + }) { + return _tester.tapOnText( + textRangeFinder, + pointer: pointer, + buttons: buttons, + ); + } + + @override + String get testDescription => _tester.testDescription; + + @override + TestTextInput get testTextInput => _tester.testTextInput; + + @override + Future timedDrag( + FinderBase finder, + Offset offset, + Duration duration, { + int? pointer, + int buttons = kPrimaryButton, + double frequency = 60.0, + bool warnIfMissed = true, + }) { + return _tester.timedDrag( + finder, + offset, + duration, + pointer: pointer, + buttons: buttons, + frequency: frequency, + warnIfMissed: warnIfMissed, + ); + } + + @override + Future timedDragFrom( + Offset startLocation, + Offset offset, + Duration duration, { + int? pointer, + int buttons = kPrimaryButton, + double frequency = 60.0, + }) { + return _tester.timedDragFrom( + startLocation, + offset, + duration, + pointer: pointer, + buttons: buttons, + frequency: frequency, + ); + } + + @override + Future trackpadFling( + FinderBase finder, + Offset offset, + double speed, { + int? pointer, + int buttons = kPrimaryButton, + Duration frameInterval = const Duration(milliseconds: 16), + Offset initialOffset = Offset.zero, + Duration initialOffsetDelay = const Duration(seconds: 1), + bool warnIfMissed = true, + }) { + return _tester.trackpadFling( + finder, + offset, + speed, + pointer: pointer, + buttons: buttons, + frameInterval: frameInterval, + initialOffset: initialOffset, + initialOffsetDelay: initialOffsetDelay, + warnIfMissed: warnIfMissed, + ); + } + + @override + Future trackpadFlingFrom( + Offset startLocation, + Offset offset, + double speed, { + int? pointer, + int buttons = kPrimaryButton, + Duration frameInterval = const Duration(milliseconds: 16), + Offset initialOffset = Offset.zero, + Duration initialOffsetDelay = const Duration(seconds: 1), + }) { + return _tester.trackpadFlingFrom( + startLocation, + offset, + speed, + pointer: pointer, + buttons: buttons, + frameInterval: frameInterval, + initialOffset: initialOffset, + initialOffsetDelay: initialOffsetDelay, + ); + } + + @override + void verifyTickersWereDisposed([String when = 'when none should have been']) { + _tester.verifyTickersWereDisposed(when); + } + + @override + TestFlutterView get view => _tester.view; + + @override + TestFlutterView viewOf(FinderBase finder) => _tester.viewOf(finder); + + @override + T widget(FinderBase finder) => + _tester.widget(finder); + + @override + Iterable widgetList(FinderBase finder) => + _tester.widgetList(finder); +} + +mixin _ShowKeyboardPatch on _PWT { + /// This is a patched version of [WidgetTester.showKeyboard] method + /// + /// TODO: Remove this patch when [this issue gets closed](https://github.com/flutter/flutter/issues/134604) + /// + /// Give the text input widget specified by [finder] the focus, as if the + /// onscreen keyboard had appeared. + /// + /// Implies a call to [pump]. + /// + /// The widget specified by [finder] must be an [EditableText] or have + /// an [EditableText] descendant. For example `find.byType(TextField)` + /// or `find.byType(TextFormField)`, or `find.byType(EditableText)`. + /// + /// Tests that just need to add text to widgets like [TextField] + /// or [TextFormField] only need to call [enterText]. + @override + Future showKeyboard(FinderBase finder) async { + var skipOffstage = true; + if (finder is Finder) { + skipOffstage = finder.skipOffstage; + } + return TestAsyncUtils.guard(() async { + final editable = state( + find.descendant( + of: finder, + matching: find.byType(EditableText, skipOffstage: skipOffstage), + matchRoot: true, + ), + ); + // Setting focusedEditable causes the binding to call requestKeyboard() + // on the EditableTextState, which itself eventually calls TextInput.attach + // to establish the connection. + binding.focusedEditable = editable; + + void removeFocusedEditable() { + final isPrimaryFocusEditableText = FocusManager + .instance.primaryFocus?.context + ?.findAncestorStateOfType() != + null; + + if (isPrimaryFocusEditableText) { + binding.focusedEditable = null; + } + + FocusManager.instance.removeListener(removeFocusedEditable); + } + + FocusManager.instance.addListener(removeFocusedEditable); + + await pump(); + }); + } +} From 9b8d1f2947e5d0f6797c75d583afa3acfc854e16 Mon Sep 17 00:00:00 2001 From: pdenert Date: Fri, 20 Dec 2024 01:56:12 +0100 Subject: [PATCH 3/6] Update changelog --- packages/patrol_finders/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/patrol_finders/CHANGELOG.md b/packages/patrol_finders/CHANGELOG.md index f6a1ea691..c67e7f208 100644 --- a/packages/patrol_finders/CHANGELOG.md +++ b/packages/patrol_finders/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased + +- Patch `enterText` into same field twice. (#2461) + ## 2.5.1 - Disable printing logs in nested `waitUntilVisible` and `waitUntilExists` calls. From 22f656a6399d7c2b143177ac5ce8b6cc46df05e5 Mon Sep 17 00:00:00 2001 From: pdenert Date: Fri, 20 Dec 2024 10:47:23 +0100 Subject: [PATCH 4/6] Change tester type to WidgetTester --- .../patrol_finders/lib/src/custom_finders/patrol_tester.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/patrol_finders/lib/src/custom_finders/patrol_tester.dart b/packages/patrol_finders/lib/src/custom_finders/patrol_tester.dart index 60b2d71af..3a62ae781 100644 --- a/packages/patrol_finders/lib/src/custom_finders/patrol_tester.dart +++ b/packages/patrol_finders/lib/src/custom_finders/patrol_tester.dart @@ -139,7 +139,7 @@ class PatrolTester { final PatrolLogWriter patrolLog; /// Flutter's widget tester that this [PatrolTester] wraps. - final PatrolWidgetTester tester; + final WidgetTester tester; /// Wraps a function with a log entry for the start and end of the function. Future wrapWithPatrolLog({ From 50d122927c808270599a430bb0d446bfe9dcc282 Mon Sep 17 00:00:00 2001 From: pdenert Date: Fri, 20 Dec 2024 12:14:09 +0100 Subject: [PATCH 5/6] Remove usage of deprecated member --- .../patrol_finders/lib/src/custom_finders/widget_tester.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/patrol_finders/lib/src/custom_finders/widget_tester.dart b/packages/patrol_finders/lib/src/custom_finders/widget_tester.dart index 66a1becfd..af88edc8c 100644 --- a/packages/patrol_finders/lib/src/custom_finders/widget_tester.dart +++ b/packages/patrol_finders/lib/src/custom_finders/widget_tester.dart @@ -448,7 +448,7 @@ class PatrolWidgetTester extends _PWT with _ShowKeyboardPatch { Future Function() callback, { Duration additionalTime = const Duration(milliseconds: 1000), }) { - return _tester.runAsync(callback, additionalTime: additionalTime); + return _tester.runAsync(callback); } @override From 62c0bb57e4d3eafbb19f34da42e6e39171ff482a Mon Sep 17 00:00:00 2001 From: pdenert Date: Fri, 20 Dec 2024 14:54:38 +0100 Subject: [PATCH 6/6] Bring back deprecated member with ignore and comment --- .../patrol_finders/lib/src/custom_finders/widget_tester.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/patrol_finders/lib/src/custom_finders/widget_tester.dart b/packages/patrol_finders/lib/src/custom_finders/widget_tester.dart index af88edc8c..e398c8cfa 100644 --- a/packages/patrol_finders/lib/src/custom_finders/widget_tester.dart +++ b/packages/patrol_finders/lib/src/custom_finders/widget_tester.dart @@ -448,7 +448,10 @@ class PatrolWidgetTester extends _PWT with _ShowKeyboardPatch { Future Function() callback, { Duration additionalTime = const Duration(milliseconds: 1000), }) { - return _tester.runAsync(callback); + // The deprecated member use is necessary for compatibility with older + // versions of the Flutter framework. + // ignore: deprecated_member_use + return _tester.runAsync(callback, additionalTime: additionalTime); } @override