diff --git a/.github/workflows/example.yaml b/.github/workflows/example.yaml new file mode 100644 index 0000000..f63577c --- /dev/null +++ b/.github/workflows/example.yaml @@ -0,0 +1,29 @@ +name: example + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + semantic-pull-request: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 + + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 + with: + flutter_channel: stable + + spell-check: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/spell_check.yml@v1 + with: + includes: | + **/*.md + modified_files_only: false diff --git a/example/lib/main.dart b/example/lib/main.dart index 009cc26..4c50bb6 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -13,8 +13,8 @@ class MyApp extends StatelessWidget { return MaterialApp( home: Scaffold( appBar: AppBar(title: const Text('Formz Example')), - body: const Padding( - padding: EdgeInsets.all(24), + body: Padding( + padding: const EdgeInsets.all(24), child: SingleChildScrollView(child: MyForm()), ), ), @@ -23,7 +23,9 @@ class MyApp extends StatelessWidget { } class MyForm extends StatefulWidget { - const MyForm({super.key}); + MyForm({super.key, Random? seed}) : seed = seed ?? Random(); + + final Random seed; @override State createState() => _MyFormState(); @@ -89,7 +91,7 @@ class _MyFormState extends State { Future _submitForm() async { await Future.delayed(const Duration(seconds: 1)); - if (Random().nextInt(2) == 0) throw Exception(); + if (widget.seed.nextInt(2) == 0) throw Exception(); } void _resetForm() { @@ -123,6 +125,7 @@ class _MyFormState extends State { child: Column( children: [ TextFormField( + key: const Key('myForm_emailInput'), controller: _emailController, decoration: const InputDecoration( icon: Icon(Icons.email), @@ -134,6 +137,7 @@ class _MyFormState extends State { textInputAction: TextInputAction.next, ), TextFormField( + key: const Key('myForm_passwordInput'), controller: _passwordController, decoration: const InputDecoration( icon: Icon(Icons.lock), @@ -153,6 +157,7 @@ class _MyFormState extends State { const CircularProgressIndicator() else ElevatedButton( + key: const Key('myForm_submit'), onPressed: _onSubmit, child: const Text('Submit'), ), diff --git a/example/pubspec.yaml b/example/pubspec.yaml index cb60db4..cd5e6c2 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -11,6 +11,7 @@ dependencies: sdk: flutter formz: path: ../ + mocktail: ^1.0.0 dev_dependencies: flutter_test: diff --git a/example/test/main_test.dart b/example/test/main_test.dart new file mode 100644 index 0000000..b839cd6 --- /dev/null +++ b/example/test/main_test.dart @@ -0,0 +1,152 @@ +import 'dart:math'; + +import 'package:example/main.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockRandom extends Mock implements Random {} + + +final _seed = MockRandom(); + +void main() { + + group('$MyApp', () { + testWidgets('render example', (tester) async { + await tester.pumpWidget(const MyApp()); + expect(find.text('Formz Example'), findsOneWidget); + }); + }); + + group('$MyForm', () { + + setUp(() { + when(()=> _seed.nextInt(any())).thenReturn(1); + }); + + testWidgets('submits valid values', (tester) async { + await tester.pumpMyForm(); + + await tester.enterText( + find.byKey(const Key('myForm_emailInput')), + 'email@example.com', + ); + await tester.pumpAndSettle(); + + await tester.enterText( + find.byKey(const Key('myForm_passwordInput')), + '123password', + ); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('myForm_submit'))); + await tester.pumpAndSettle(); + await tester.pumpAndSettle(); + expect(find.text('Submitted successfully! 🎉'), findsOneWidget); + }); + + group('invalid values', () { + testWidgets('invalid email', (tester) async { + await tester.pumpMyForm(); + + await tester.enterText( + find.byKey(const Key('myForm_emailInput')), + 'emailexample.com', + ); + await tester.pumpAndSettle(); + + await tester.enterText( + find.byKey(const Key('myForm_passwordInput')), + '123password', + ); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('myForm_submit'))); + await tester.pumpAndSettle(); + await tester.pumpAndSettle(); + + expect( + find.text('Please ensure the email entered is valid'), + findsOneWidget, + ); + }); + + testWidgets('empty email', (tester) async { + await tester.pumpMyForm(); + + await tester.enterText( + find.byKey(const Key('myForm_emailInput')), + '', + ); + await tester.pumpAndSettle(); + + await tester.enterText( + find.byKey(const Key('myForm_passwordInput')), + '123password', + ); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('myForm_submit'))); + await tester.pumpAndSettle(); + await tester.pumpAndSettle(); + + expect( + find.text('Please ensure the email entered is valid'), + findsOneWidget, + ); + }); + + testWidgets('invalid password', (tester) async { + await tester.pumpMyForm(); + + await tester.enterText( + find.byKey(const Key('myForm_emailInput')), + 'email@example.com', + ); + await tester.pumpAndSettle(); + + await tester.enterText( + find.byKey(const Key('myForm_passwordInput')), + '12345678', + ); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('myForm_submit'))); + await tester.pumpAndSettle(); + await tester.pumpAndSettle(); + + expect( + find.text('''Password must be at least 8 characters and contain at least one letter and number'''), + findsOneWidget, + ); + }); + + testWidgets('empty password', (tester) async { + await tester.pumpMyForm(); + + await tester.enterText( + find.byKey(const Key('myForm_emailInput')), + 'email@example.com', + ); + await tester.pumpAndSettle(); + + await tester.enterText( + find.byKey(const Key('myForm_passwordInput')), + '', + ); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('myForm_submit'))); + await tester.pumpAndSettle(); + await tester.pumpAndSettle(); + + expect( + find.text('''Password must be at least 8 characters and contain at least one letter and number'''), + findsOneWidget, + ); + }); + }); + }); +} + +extension on WidgetTester { + Future pumpMyForm() async { + await pumpWidget(MaterialApp(home: Scaffold(body: MyForm(seed: _seed)))); + } +}