From 48e7a7a4193e872c4a66a5dca6651f8c254b0f13 Mon Sep 17 00:00:00 2001 From: Tkko Date: Mon, 16 May 2022 21:52:08 +0400 Subject: [PATCH] Fixed onCompleted mac called, Added tests --- .flutter-plugins-dependencies | 2 +- CHANGELOG.md | 5 + example/lib/pages/gallery_page.dart | 2 + .../pinput_templates/rounded_with_cursor.dart | 85 +++--- lib/src/pinput_state.dart | 21 +- pubspec.yaml | 2 +- test/helpers/helpers.dart | 1 + test/helpers/pump_app.dart | 10 + test/pinput_test.dart | 280 ++++++++++-------- 9 files changed, 236 insertions(+), 172 deletions(-) create mode 100644 test/helpers/helpers.dart create mode 100644 test/helpers/pump_app.dart diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies index 59b872a..86ba716 100644 --- a/.flutter-plugins-dependencies +++ b/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"smart_auth","path":"/Users/dev/Dev/TKKO/Tools/flutter/.pub-cache/hosted/pub.dartlang.org/smart_auth-1.0.5/","native_build":true,"dependencies":[]}],"android":[{"name":"smart_auth","path":"/Users/dev/Dev/TKKO/Tools/flutter/.pub-cache/hosted/pub.dartlang.org/smart_auth-1.0.5/","native_build":true,"dependencies":[]}],"macos":[{"name":"smart_auth","path":"/Users/dev/Dev/TKKO/Tools/flutter/.pub-cache/hosted/pub.dartlang.org/smart_auth-1.0.5/","native_build":true,"dependencies":[]}],"linux":[{"name":"smart_auth","path":"/Users/dev/Dev/TKKO/Tools/flutter/.pub-cache/hosted/pub.dartlang.org/smart_auth-1.0.5/","native_build":true,"dependencies":[]}],"windows":[{"name":"smart_auth","path":"/Users/dev/Dev/TKKO/Tools/flutter/.pub-cache/hosted/pub.dartlang.org/smart_auth-1.0.5/","native_build":true,"dependencies":[]}],"web":[{"name":"smart_auth","path":"/Users/dev/Dev/TKKO/Tools/flutter/.pub-cache/hosted/pub.dartlang.org/smart_auth-1.0.5/","dependencies":[]}]},"dependencyGraph":[{"name":"smart_auth","dependencies":[]}],"date_created":"2022-05-13 09:30:40.337494","version":"3.0.0"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"smart_auth","path":"/Users/dev/Dev/TKKO/Tools/flutter/.pub-cache/hosted/pub.dartlang.org/smart_auth-1.0.5/","native_build":true,"dependencies":[]}],"android":[{"name":"smart_auth","path":"/Users/dev/Dev/TKKO/Tools/flutter/.pub-cache/hosted/pub.dartlang.org/smart_auth-1.0.5/","native_build":true,"dependencies":[]}],"macos":[{"name":"smart_auth","path":"/Users/dev/Dev/TKKO/Tools/flutter/.pub-cache/hosted/pub.dartlang.org/smart_auth-1.0.5/","native_build":true,"dependencies":[]}],"linux":[{"name":"smart_auth","path":"/Users/dev/Dev/TKKO/Tools/flutter/.pub-cache/hosted/pub.dartlang.org/smart_auth-1.0.5/","native_build":true,"dependencies":[]}],"windows":[{"name":"smart_auth","path":"/Users/dev/Dev/TKKO/Tools/flutter/.pub-cache/hosted/pub.dartlang.org/smart_auth-1.0.5/","native_build":true,"dependencies":[]}],"web":[{"name":"smart_auth","path":"/Users/dev/Dev/TKKO/Tools/flutter/.pub-cache/hosted/pub.dartlang.org/smart_auth-1.0.5/","dependencies":[]}]},"dependencyGraph":[{"name":"smart_auth","dependencies":[]}],"date_created":"2022-05-16 21:50:29.998915","version":"3.0.0"} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 799b89a..82aa3cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.2.9 -16/05/2022 +- onCompleted mot called +- Added tests + + ## 2.2.8 -13/05/2022 - Fixed dart 2.17 hints diff --git a/example/lib/pages/gallery_page.dart b/example/lib/pages/gallery_page.dart index 43d149e..ff2f976 100644 --- a/example/lib/pages/gallery_page.dart +++ b/example/lib/pages/gallery_page.dart @@ -53,6 +53,8 @@ class GalleryPageState extends State }); } + final controller = TextEditingController(); + @override Widget build(BuildContext context) { return ScrollConfiguration( diff --git a/example/lib/pinput_templates/rounded_with_cursor.dart b/example/lib/pinput_templates/rounded_with_cursor.dart index de1ac92..507fa67 100644 --- a/example/lib/pinput_templates/rounded_with_cursor.dart +++ b/example/lib/pinput_templates/rounded_with_cursor.dart @@ -41,47 +41,56 @@ class _RoundedWithCustomCursorState extends State { ), ); - return Pinput( - controller: pinController, - focusNode: focusNode, - androidSmsAutofillMethod: AndroidSmsAutofillMethod.smsRetrieverApi, - listenForMultipleSmsOnAndroid: true, - defaultPinTheme: defaultPinTheme, - validator: (value) { - return value == '2222' ? null : 'Pin is incorrect'; - }, - onClipboardFound: (value) { - debugPrint('onClipboardFound: $value'); - pinController.setText(value); - }, - hapticFeedbackType: HapticFeedbackType.lightImpact, - onCompleted: debugPrint, - cursor: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Container( - margin: EdgeInsets.only(bottom: 9), - width: 22, - height: 1, - color: focusedBorderColor, + return Directionality( + // Specify direction if desired + textDirection: TextDirection.ltr, + child: Pinput( + controller: pinController, + focusNode: focusNode, + androidSmsAutofillMethod: AndroidSmsAutofillMethod.smsUserConsentApi, + listenForMultipleSmsOnAndroid: true, + defaultPinTheme: defaultPinTheme, + validator: (value) { + return value == '2222' ? null : 'Pin is incorrect'; + }, + onClipboardFound: (value) { + debugPrint('onClipboardFound: $value'); + pinController.setText(value); + }, + hapticFeedbackType: HapticFeedbackType.lightImpact, + onCompleted: (pin) { + debugPrint('onCompleted: $pin'); + }, + onChanged: (value) { + debugPrint('onChanged: $value'); + }, + cursor: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + margin: EdgeInsets.only(bottom: 9), + width: 22, + height: 1, + color: focusedBorderColor, + ), + ], + ), + focusedPinTheme: defaultPinTheme.copyWith( + decoration: defaultPinTheme.decoration!.copyWith( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: focusedBorderColor), ), - ], - ), - focusedPinTheme: defaultPinTheme.copyWith( - decoration: defaultPinTheme.decoration!.copyWith( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: focusedBorderColor), ), - ), - submittedPinTheme: defaultPinTheme.copyWith( - decoration: defaultPinTheme.decoration!.copyWith( - color: fillColor, - borderRadius: BorderRadius.circular(19), - border: Border.all(color: focusedBorderColor), + submittedPinTheme: defaultPinTheme.copyWith( + decoration: defaultPinTheme.decoration!.copyWith( + color: fillColor, + borderRadius: BorderRadius.circular(19), + border: Border.all(color: focusedBorderColor), + ), + ), + errorPinTheme: defaultPinTheme.copyBorderWith( + border: Border.all(color: Colors.redAccent), ), - ), - errorPinTheme: defaultPinTheme.copyBorderWith( - border: Border.all(color: Colors.redAccent), ), ); } diff --git a/lib/src/pinput_state.dart b/lib/src/pinput_state.dart index 5671d9a..8a44d10 100644 --- a/lib/src/pinput_state.dart +++ b/lib/src/pinput_state.dart @@ -1,5 +1,13 @@ part of 'pinput.dart'; +/// This allows a value of type T or T? +/// to be treated as a value of type T?. +/// +/// We use this so that APIs that have become +/// non-nullable can still be used with `!` and `?` +/// to support older versions of the API as well. +T? _ambiguate(T? value) => value; + class _PinputState extends State with RestorationMixin, WidgetsBindingObserver, PinputUtilsMixin implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient { @@ -79,8 +87,7 @@ class _PinputState extends State _maybeInitSmartAuth(); _maybeCheckClipboard(); // https://github.com/Tkko/Flutter_Pinput/issues/89 - // ignore: unnecessary_cast - (WidgetsBinding.instance as WidgetsBinding).addObserver(this); + _ambiguate(WidgetsBinding.instance)!.addObserver(this); } /// Android Autofill @@ -161,6 +168,7 @@ class _PinputState extends State _createLocalController(oldWidget.controller!.value); } else if (widget.controller != null && oldWidget.controller == null) { unregisterFromRestoration(_controller!); + _controller!.removeListener(_handleTextEditingControllerChanges); _controller!.dispose(); _controller = null; } @@ -190,6 +198,7 @@ class _PinputState extends State _controller = value == null ? RestorableTextEditingController() : RestorableTextEditingController.fromValue(value); + _controller!.addListener(_handleTextEditingControllerChanges); if (!restorePending) { _registerController(); } @@ -202,8 +211,7 @@ class _PinputState extends State _controller?.dispose(); _smartAuth?.removeSmsListener(); // https://github.com/Tkko/Flutter_Pinput/issues/89 - // ignore: unnecessary_cast - (WidgetsBinding.instance as WidgetsBinding).removeObserver(this); + _ambiguate(WidgetsBinding.instance)!.removeObserver(this); super.dispose(); } @@ -372,10 +380,7 @@ class _PinputState extends State child: EditableText( maxLines: 1, style: _hiddenTextStyle, - onChanged: (String text) { - if (widget.controller == null) { - _onChanged(text); - } + onChanged: (_) { _maybeUseHaptic(widget.hapticFeedbackType); }, expands: false, diff --git a/pubspec.yaml b/pubspec.yaml index 55686a1..8ae656c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: pinput description: Pin code input (OTP) text field, iOS SMS autofill, Android SMS autofill One Time Code, Password, Passcode, Captcha, Security, Coupon, Wowcher, 2FA, Two step verification homepage: https://github.com/Tkko/Flutter_PinPut repository: https://github.com/Tkko/Flutter_PinPut -version: 2.2.8 +version: 2.2.9 environment: sdk: '>=2.12.0 <3.0.0' diff --git a/test/helpers/helpers.dart b/test/helpers/helpers.dart new file mode 100644 index 0000000..b15fe65 --- /dev/null +++ b/test/helpers/helpers.dart @@ -0,0 +1 @@ +export 'pump_app.dart'; diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart new file mode 100644 index 0000000..9713769 --- /dev/null +++ b/test/helpers/pump_app.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +extension PumpApp on WidgetTester { + Future pumpApp(Widget widget) { + return pumpWidget( + MaterialApp(home: Material(child: widget)), + ); + } +} diff --git a/test/pinput_test.dart b/test/pinput_test.dart index 51bd936..fa0c3da 100644 --- a/test/pinput_test.dart +++ b/test/pinput_test.dart @@ -2,16 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinput/pinput.dart'; +import 'helpers/helpers.dart'; + void main() { testWidgets('Pins are displayed', (WidgetTester tester) async { final length = 4; - await tester.pumpWidget( - MaterialApp( - home: Material( - child: Pinput(length: length), - ), - ), - ); + await tester.pumpApp(Pinput(length: length)); expect(find.byType(Flexible), findsNWidgets(length)); expect(find.byType(AnimatedContainer), findsNWidgets(length)); @@ -33,20 +29,16 @@ void main() { final errorTheme = defaultTheme.copyDecorationWith( color: Colors.greenAccent.withOpacity(.5)); - await tester.pumpWidget( - MaterialApp( - home: Material( - child: Pinput( - length: length, - focusNode: focusNode, - defaultPinTheme: defaultTheme, - focusedPinTheme: focusedTheme, - submittedPinTheme: submittedTheme, - followingPinTheme: followingTheme, - disabledPinTheme: disabledTheme, - errorPinTheme: errorTheme, - ), - ), + await tester.pumpApp( + Pinput( + length: length, + focusNode: focusNode, + defaultPinTheme: defaultTheme, + focusedPinTheme: focusedTheme, + submittedPinTheme: submittedTheme, + followingPinTheme: followingTheme, + disabledPinTheme: disabledTheme, + errorPinTheme: errorTheme, ), ); @@ -101,16 +93,12 @@ void main() { final focusNode = FocusNode(); final defaultTheme = PinTheme(decoration: BoxDecoration()); final focusedTheme = defaultTheme.copyDecorationWith(color: Colors.red); - await tester.pumpWidget( - MaterialApp( - home: Material( - child: Pinput( - focusNode: focusNode, - autofocus: true, - defaultPinTheme: defaultTheme, - focusedPinTheme: focusedTheme, - ), - ), + await tester.pumpApp( + Pinput( + focusNode: focusNode, + autofocus: true, + defaultPinTheme: defaultTheme, + focusedPinTheme: focusedTheme, ), ); @@ -128,14 +116,10 @@ void main() { }); testWidgets('Should display custom cursor', (WidgetTester tester) async { - await tester.pumpWidget( - MaterialApp( - home: Material( - child: Pinput( - autofocus: true, - cursor: const FlutterLogo(), - ), - ), + await tester.pumpApp( + Pinput( + autofocus: true, + cursor: const FlutterLogo(), ), ); @@ -143,88 +127,148 @@ void main() { expect(find.byType(FlutterLogo), findsOneWidget); }); - testWidgets('onChanged callback is called', (WidgetTester tester) async { - String? fieldValue; - - await tester.pumpWidget( - MaterialApp( - home: Material( - child: Pinput( - onChanged: (value) => fieldValue = value, - ), + group('onChanged should work properly', () { + testWidgets('onChanged should work with controller', + (WidgetTester tester) async { + String? fieldValue; + int called = 0; + + await tester.pumpApp( + Pinput( + onChanged: (value) { + fieldValue = value; + called++; + }, ), - ), - ); + ); - expect(fieldValue, isNull); + expect(fieldValue, isNull); + expect(called, 0); + + Future checkText(String testValue) async { + await tester.enterText(find.byType(EditableText), testValue); + expect(fieldValue, equals(testValue)); + } + + await checkText('123'); + expect(called, 1); + + await checkText('123'); + expect(called, 1); + + await checkText(''); + expect(called, 2); + }); + + testWidgets('onChanged should work with controller', + (WidgetTester tester) async { + String? fieldValue; + int called = 0; + final TextEditingController controller = TextEditingController(); + + await tester.pumpApp( + Pinput( + controller: controller, + onChanged: (value) { + fieldValue = value; + called++; + }, + ), + ); - Future checkText(String testValue) async { - await tester.enterText(find.byType(EditableText), testValue); - expect(fieldValue, equals(testValue)); - } + expect(fieldValue, isNull); + expect(called, 0); - await checkText('123'); - await checkText(''); - }); + await tester.enterText(find.byType(EditableText), '11'); + expect(fieldValue, equals('11')); + expect(called, 1); - testWidgets('onCompleted callback is called', (WidgetTester tester) async { - String? fieldValue; + controller.setText('12'); + expect(fieldValue, equals('12')); + expect(called, 2); - await tester.pumpWidget( - MaterialApp( - home: Material( - child: Pinput( - length: 4, - onCompleted: (value) => fieldValue = value, - ), - ), - ), - ); + controller.setText('12'); + expect(fieldValue, equals('12')); + expect(called, 2); - expect(fieldValue, isNull); - - await tester.enterText(find.byType(EditableText), '1234'); - expect(fieldValue, equals('1234')); - - fieldValue = null; - await tester.enterText(find.byType(EditableText), '123'); - expect(fieldValue, isNull); + controller.clear(); + expect(fieldValue, equals('')); + expect(called, 3); + }); }); - testWidgets('onCompleted callback is called', (WidgetTester tester) async { - String? fieldValue; + group('onCompleted should work properly', () { + testWidgets('onCompleted works without controller', + (WidgetTester tester) async { + String? fieldValue; + int called = 0; + + await tester.pumpApp( + Pinput( + length: 4, + onCompleted: (value) { + fieldValue = value; + called++; + }, + ), + ); - await tester.pumpWidget( - MaterialApp( - home: Material( - child: Pinput( - length: 4, - onCompleted: (value) => fieldValue = value, - ), + expect(fieldValue, isNull); + expect(called, 0); + + await tester.enterText(find.byType(EditableText), '1234'); + expect(fieldValue, equals('1234')); + expect(called, 1); + + fieldValue = null; + await tester.enterText(find.byType(EditableText), '123'); + expect(fieldValue, isNull); + expect(called, 1); + }); + + testWidgets('onCompleted callback is called', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(); + String? fieldValue; + int called = 0; + + await tester.pumpApp( + Pinput( + controller: controller, + onCompleted: (value) { + fieldValue = value; + called++; + }, ), - ), - ); + ); - expect(fieldValue, isNull); + expect(fieldValue, isNull); + expect(called, 0); - await tester.enterText(find.byType(EditableText), '1234'); - expect(fieldValue, equals('1234')); + controller.setText('1234'); + expect(fieldValue, equals('1234')); + expect(called, 1); - fieldValue = null; - await tester.enterText(find.byType(EditableText), '123'); - expect(fieldValue, isNull); + controller.clear(); + expect(fieldValue, equals('1234')); + expect(called, 1); + + fieldValue = null; + await tester.enterText(find.byType(EditableText), '123'); + expect(fieldValue, isNull); + expect(called, 1); + + controller.setText('12345'); + expect(fieldValue, isNull); + expect(called, 1); + }); }); testWidgets('onTap is called upon tap', (WidgetTester tester) async { int tapCount = 0; - await tester.pumpWidget( - MaterialApp( - home: Material( - child: Pinput( - onTap: () => ++tapCount, - ), - ), + await tester.pumpApp( + Pinput( + onTap: () => ++tapCount, ), ); @@ -243,14 +287,10 @@ void main() { (WidgetTester tester) async { int tapCount = 0; - await tester.pumpWidget( - MaterialApp( - home: Material( - child: Pinput( - enabled: false, - onTap: () => ++tapCount, - ), - ), + await tester.pumpApp( + Pinput( + enabled: false, + onTap: () => ++tapCount, ), ); @@ -263,13 +303,9 @@ void main() { testWidgets('onLongPress is called', (WidgetTester tester) async { int tapCount = 0; - await tester.pumpWidget( - MaterialApp( - home: Material( - child: Pinput( - onLongPress: () => ++tapCount, - ), - ), + await tester.pumpApp( + Pinput( + onLongPress: () => ++tapCount, ), ); @@ -287,13 +323,9 @@ void main() { testWidgets('onSubmitted callback is called', (WidgetTester tester) async { String? fieldValue; - await tester.pumpWidget( - MaterialApp( - home: Material( - child: Pinput( - onSubmitted: (value) => fieldValue = value, - ), - ), + await tester.pumpApp( + Pinput( + onSubmitted: (value) => fieldValue = value, ), );