diff --git a/packages/app/pubspec.lock b/packages/app/pubspec.lock index 92c1a2f..ad1490d 100644 --- a/packages/app/pubspec.lock +++ b/packages/app/pubspec.lock @@ -222,6 +222,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + checks: + dependency: "direct main" + description: + name: checks + sha256: aad431b45a8ae2fa26db8c22e385b9cdec73f72986a1d9d9f2017f4c39ecf5c9 + url: "https://pub.dev" + source: hosted + version: "0.3.0" ci: dependency: transitive description: diff --git a/packages/app/pubspec.yaml b/packages/app/pubspec.yaml index b4a1032..575e0e7 100644 --- a/packages/app/pubspec.yaml +++ b/packages/app/pubspec.yaml @@ -41,6 +41,7 @@ dependencies: build_version: ^2.1.1 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. + checks: ^0.3.0 cupertino_icons: ^1.0.8 # Only required if you use Cupertino (iOS style) icons flutter: sdk: flutter diff --git a/packages/app/test/helpers/accessibility.dart b/packages/app/test/helpers/accessibility.dart index c2b8ae9..b21e5fe 100644 --- a/packages/app/test/helpers/accessibility.dart +++ b/packages/app/test/helpers/accessibility.dart @@ -1,7 +1,9 @@ +import 'package:checks/checks.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'checks.dart'; import 'pump_app.dart'; void testAccessibilityGuidelines( @@ -13,28 +15,28 @@ void testAccessibilityGuidelines( await tester.pumpApp(widget, overrides: overrides); final handle = tester.ensureSemantics(); - await expectLater(tester, meetsGuideline(androidTapTargetGuideline)); + await check(tester).meetsGuideline(androidTapTargetGuideline); handle.dispose(); }); testWidgets('on iOS.', (tester) async { await tester.pumpApp(widget, overrides: overrides); final handle = tester.ensureSemantics(); - await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); + await check(tester).meetsGuideline(iOSTapTargetGuideline); handle.dispose(); }); testWidgets('according to the WCAG.', (tester) async { await tester.pumpApp(widget, overrides: overrides); final handle = tester.ensureSemantics(); - await expectLater(tester, meetsGuideline(textContrastGuideline)); + await check(tester).meetsGuideline(textContrastGuideline); handle.dispose(); }); testWidgets('with regard to labeling buttons.', (tester) async { await tester.pumpApp(widget, overrides: overrides); final handle = tester.ensureSemantics(); - await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + await check(tester).meetsGuideline(labeledTapTargetGuideline); handle.dispose(); }); }); diff --git a/packages/app/test/helpers/checks.dart b/packages/app/test/helpers/checks.dart new file mode 100644 index 0000000..4f80893 --- /dev/null +++ b/packages/app/test/helpers/checks.dart @@ -0,0 +1,52 @@ +import 'package:checks/context.dart'; +import 'package:flutter_test/flutter_test.dart'; + +extension AccessibilityExpect on Subject { + Future meetsGuideline(AccessibilityGuideline guideline) async { + await context.expectAsync(() => [guideline.description], (tester) async { + final Evaluation result = await guideline.evaluate(tester); + if (result.passed) { + return null; + } + return Rejection(which: [result.reason!]); + }); + } +} + +extension WidgetFinderExpect on Subject { + Future findsOne() async { + await findsExactly(1); + } + + Future findsExactly(int count) async { + _findsWidgets(0, count); + } + + Future _findsWidgets(int? min, int? max) async { + assert(min != null || max != null); + assert(min == null || max == null || min <= max); + + await context.expectAsync(() => ['finds between $min and $max widgets'], + (finder) async { + int count = 0; + final Iterator iterator = finder.evaluate().iterator; + if (min != null) { + while (count < min && iterator.moveNext()) { + count += 1; + } + if (count < min) { + return Rejection(which: ['found less than $min widgets']); + } + } + if (max != null) { + while (count <= max && iterator.moveNext()) { + count += 1; + } + if (count > max) { + return Rejection(which: ['found more than $max widgets']); + } + } + return null; + }); + } +} diff --git a/packages/app/test/src/app/app_test.dart b/packages/app/test/src/app/app_test.dart index e842f1b..da75409 100644 --- a/packages/app/test/src/app/app_test.dart +++ b/packages/app/test/src/app/app_test.dart @@ -1,3 +1,4 @@ +import 'package:checks/checks.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -6,6 +7,7 @@ import 'package:our_democracy/src/features/settings/application/settings_service import 'package:our_democracy/src/features/settings/data/preferences_repository.dart'; import 'package:our_democracy/src/l10n/l10n.dart'; +import '../../helpers/checks.dart'; import '../../helpers/mocks.dart'; extension _WidgetTesterX on WidgetTester { @@ -27,25 +29,26 @@ extension _WidgetTesterX on WidgetTester { void main() { testWidgets('MyApp should build MaterialApp.router', (tester) async { await tester.pumpWidgetPage(); - expect(find.byType(MaterialApp), findsOneWidget); + check(find.byType(MaterialApp)).findsOne(); }); testWidgets('MyApp should have correct restorationScopeId', (tester) async { await tester.pumpWidgetPage(); final app = tester.widget(find.byType(MaterialApp)); - expect(app.restorationScopeId, 'app'); + check(app.restorationScopeId).equals('app'); }); testWidgets('MyApp should have correct localizationsDelegates', (tester) async { await tester.pumpWidgetPage(); final app = tester.widget(find.byType(MaterialApp)); - expect(app.localizationsDelegates, AppLocalizations.localizationsDelegates); + check(app.localizationsDelegates) + .equals(AppLocalizations.localizationsDelegates); }); testWidgets('MyApp should have correct supportedLocales', (tester) async { await tester.pumpWidgetPage(); final app = tester.widget(find.byType(MaterialApp)); - expect(app.supportedLocales, AppLocalizations.supportedLocales); + check(app.supportedLocales).equals(AppLocalizations.supportedLocales); }); } diff --git a/packages/app/test/src/app/bootstrap_test.dart b/packages/app/test/src/app/bootstrap_test.dart index 5881ee2..2e4ef98 100644 --- a/packages/app/test/src/app/bootstrap_test.dart +++ b/packages/app/test/src/app/bootstrap_test.dart @@ -1,3 +1,4 @@ +import 'package:checks/checks.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -20,15 +21,11 @@ void main() { test('main does not throw', () async { const app = MyApp(); - - await expectLater( - app.bootstrap( - ( - runApp: (_) {}, - getSharedPreferences: getSharedPreferences, - ), - ), - completes, + final env = ( + runApp: (_) {}, + getSharedPreferences: getSharedPreferences, ); + + check(app.bootstrap(env)).completes(); }); } diff --git a/packages/app/test/src/app/router_test.dart b/packages/app/test/src/app/router_test.dart index 179951d..02d73fb 100644 --- a/packages/app/test/src/app/router_test.dart +++ b/packages/app/test/src/app/router_test.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:checks/checks.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:our_democracy/src/app/router.dart'; @@ -10,37 +11,37 @@ void main() { test( 'defaultRouteType is a RouteType.material.', () { - expect(tested.defaultRouteType, isInstanceOf()); + check(tested.defaultRouteType).isA(); }, ); test('should contain the correct number of routes.', () { - expect(tested.routes.length, equals(2)); + check(tested.routes.length).equals(2); }); }); group('path', () { test('should be correct for WrapperRoute.', () { final wrapperRoute = tested.routes[0]; - expect(wrapperRoute.path, equals('/')); + check(wrapperRoute.path).equals('/'); }); test('should be correct for SampleItemListRoute.', () { final sampleItemListRoute = tested.routes[0].children?.routes.toList()[0]; - expect(sampleItemListRoute?.path, equals('')); + check(sampleItemListRoute?.path).equals(''); }); test('should be correct for SampleItemDetailsRoute.', () { final sampleItemDetailsRoute = tested.routes[0].children?.routes.toList()[1]; - expect(sampleItemDetailsRoute?.path, equals('sample-item')); + check(sampleItemDetailsRoute?.path).equals('sample-item'); }); test('should be correct for SettingsRoute.', () { final settingsRoute = tested.routes[0].children?.routes.toList()[2]; - expect(settingsRoute?.path, equals('settings')); + check(settingsRoute?.path).equals('settings'); }); test('should redirect on 404', () { final redirectRoute = tested.routes[1]; - expect(redirectRoute.path, equals('/*')); + check(redirectRoute.path).equals('/*'); }); }); }); diff --git a/packages/app/test/src/app/wrapper_page_test.dart b/packages/app/test/src/app/wrapper_page_test.dart index 35a42b1..1baada2 100644 --- a/packages/app/test/src/app/wrapper_page_test.dart +++ b/packages/app/test/src/app/wrapper_page_test.dart @@ -1,3 +1,4 @@ +import 'package:checks/checks.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -7,6 +8,7 @@ import 'package:our_democracy/src/l10n/l10n.dart'; import 'package:our_democracy/src/utils/design.dart'; import 'package:our_democracy/src/utils/router.dart'; +import '../../helpers/checks.dart'; import '../../helpers/riverpod.dart'; extension _WidgetTesterX on WidgetTester { @@ -35,8 +37,8 @@ extension _WidgetTesterX on WidgetTester { const WrapperRoute(), ]); await pumpAndSettle(); - expect(router.urlState.url, equals('/')); - expect(find.byType(WrapperPage), findsOneWidget); + check(router.urlState.url).equals('/'); + check(find.byType(WrapperPage)).findsOne(); } } @@ -47,28 +49,28 @@ void main() { await tester.pumpWidgetPage(); final handle = tester.ensureSemantics(); - await expectLater(tester, meetsGuideline(androidTapTargetGuideline)); + await check(tester).meetsGuideline(androidTapTargetGuideline); handle.dispose(); }); testWidgets('on iOS.', (tester) async { await tester.pumpWidgetPage(); final handle = tester.ensureSemantics(); - await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); + await check(tester).meetsGuideline(iOSTapTargetGuideline); handle.dispose(); }); testWidgets('according to the WCAG.', (tester) async { await tester.pumpWidgetPage(); final handle = tester.ensureSemantics(); - await expectLater(tester, meetsGuideline(textContrastGuideline)); + await check(tester).meetsGuideline(textContrastGuideline); handle.dispose(); }); testWidgets('with regard to labeling buttons.', (tester) async { await tester.pumpWidgetPage(); final handle = tester.ensureSemantics(); - await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + await check(tester).meetsGuideline(labeledTapTargetGuideline); handle.dispose(); }); }); diff --git a/packages/app/test/src/features/sample/application/sample_items_service_test.dart b/packages/app/test/src/features/sample/application/sample_items_service_test.dart index b3c840a..081f292 100644 --- a/packages/app/test/src/features/sample/application/sample_items_service_test.dart +++ b/packages/app/test/src/features/sample/application/sample_items_service_test.dart @@ -1,3 +1,4 @@ +import 'package:checks/checks.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:our_democracy/src/features/sample/application/sample_items_service.dart'; @@ -8,7 +9,7 @@ void main() { final container = ProviderContainer(); final model = await container.read(sampleItemsServiceProvider.future); - expect(model.items.length, 3); + check(model.items.length).equals(3); }); }); } diff --git a/packages/app/test/src/features/sample/domain/sample_item_entity_test.dart b/packages/app/test/src/features/sample/domain/sample_item_entity_test.dart index 0608a39..14f4294 100644 --- a/packages/app/test/src/features/sample/domain/sample_item_entity_test.dart +++ b/packages/app/test/src/features/sample/domain/sample_item_entity_test.dart @@ -1,10 +1,11 @@ +import 'package:checks/checks.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:our_democracy/src/features/sample/domain/sample_item_entity.dart'; void main() { test('SampleItemEntity should correctly wrap an int', () { const entity = SampleItemEntity(1); - expect(entity, isA()); - expect(entity, equals(1)); + check(entity).isA(); + check(entity.id).equals(1); }); } diff --git a/packages/app/test/src/features/sample/domain/sample_items_model_test.dart b/packages/app/test/src/features/sample/domain/sample_items_model_test.dart index b5de5f3..aabc957 100644 --- a/packages/app/test/src/features/sample/domain/sample_items_model_test.dart +++ b/packages/app/test/src/features/sample/domain/sample_items_model_test.dart @@ -1,3 +1,4 @@ +import 'package:checks/checks.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:our_democracy/src/features/sample/domain/sample_items_model.dart'; @@ -11,7 +12,7 @@ void main() { final newModel = model.copyWith(items: []); // Assert - expect(newModel, equals(model)); + check(newModel).equals(model); }); }); } diff --git a/packages/app/test/src/features/sample/presentation/items/sample_item_details_page_test.dart b/packages/app/test/src/features/sample/presentation/items/sample_item_details_page_test.dart index 032dec3..7210fc3 100644 --- a/packages/app/test/src/features/sample/presentation/items/sample_item_details_page_test.dart +++ b/packages/app/test/src/features/sample/presentation/items/sample_item_details_page_test.dart @@ -1,7 +1,9 @@ +import 'package:checks/checks.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:our_democracy/src/features/sample/presentation/items/sample_item_details_page.dart'; import '../../../../../helpers/accessibility.dart'; +import '../../../../../helpers/checks.dart'; import '../../../../../helpers/pump_app.dart'; void main() { @@ -15,7 +17,7 @@ void main() { await tester.pumpApp(widget); // Verify that the widget displays the expected information - expect(find.text('More Information Here'), findsOneWidget); + check(find.text('More Information Here')).findsOne(); }); }); diff --git a/packages/app/test/src/features/sample/presentation/items/sample_item_list_page_test.dart b/packages/app/test/src/features/sample/presentation/items/sample_item_list_page_test.dart index 01b737f..6049dbc 100644 --- a/packages/app/test/src/features/sample/presentation/items/sample_item_list_page_test.dart +++ b/packages/app/test/src/features/sample/presentation/items/sample_item_list_page_test.dart @@ -1,8 +1,10 @@ +import 'package:checks/checks.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:our_democracy/src/features/sample/presentation/items/sample_items_list_page.dart'; import '../../../../../helpers/accessibility.dart'; +import '../../../../../helpers/checks.dart'; import '../../../../../helpers/pump_app.dart'; void main() { @@ -16,7 +18,7 @@ void main() { await tester.pumpApp(const Material(child: widget)); // Verify that the widget displays the expected information - expect(find.text('SampleItem 1'), findsOneWidget); + check(find.text('SampleItem 1')).findsOne(); }); testAccessibilityGuidelines(const Material(child: SampleItemsListPage())); diff --git a/packages/app/test/src/features/settings/application/settings_service_test.dart b/packages/app/test/src/features/settings/application/settings_service_test.dart index 40efe16..1b9d285 100644 --- a/packages/app/test/src/features/settings/application/settings_service_test.dart +++ b/packages/app/test/src/features/settings/application/settings_service_test.dart @@ -1,3 +1,4 @@ +import 'package:checks/checks.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -24,12 +25,12 @@ void main() { final model = container.read(settingsServiceProvider); final notifier = container.read(settingsServiceProvider.notifier); - expect(model.themeMode, ThemeMode.system); + check(model.themeMode).equals(ThemeMode.system); await notifier.updateThemeMode(ThemeMode.light); final newModel = container.read(settingsServiceProvider); - expect(newModel.themeMode, ThemeMode.light); + check(newModel.themeMode).equals(ThemeMode.light); }); }); @@ -38,12 +39,11 @@ void main() { // Arrange final container = ProviderContainer(); + // Act + final call = () => container.read(initialSettingsProvider); + // Assert - expect( - // Act - () => container.read(initialSettingsProvider), - throwsA(isA()), - ); + check(call).throws(); }); }); } diff --git a/packages/app/test/src/features/settings/data/preferences_repository_test.dart b/packages/app/test/src/features/settings/data/preferences_repository_test.dart index b599fa4..443fb71 100644 --- a/packages/app/test/src/features/settings/data/preferences_repository_test.dart +++ b/packages/app/test/src/features/settings/data/preferences_repository_test.dart @@ -1,3 +1,4 @@ +import 'package:checks/checks.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -25,7 +26,7 @@ void main() { final settings = await repo.load(); // Assert - expect(settings.themeMode, ThemeMode.system); + check(settings.themeMode).equals(ThemeMode.system); }); test('should decode the theme mode', () async { @@ -47,7 +48,7 @@ void main() { final settings = await repo.load(); // Assert - expect(settings.themeMode, ThemeMode.dark); + check(settings.themeMode).equals(ThemeMode.dark); }); }); @@ -61,7 +62,7 @@ void main() { container.read(preferencesRepositoryProvider); // Assert - expect(call, throwsA(isA())); + check(call).throws(); }); }); } diff --git a/packages/app/test/src/features/settings/domain/settings_model_test.dart b/packages/app/test/src/features/settings/domain/settings_model_test.dart index af72f2e..d25f73f 100644 --- a/packages/app/test/src/features/settings/domain/settings_model_test.dart +++ b/packages/app/test/src/features/settings/domain/settings_model_test.dart @@ -1,3 +1,4 @@ +import 'package:checks/checks.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:our_democracy/src/features/settings/domain/settings_model.dart'; @@ -12,7 +13,7 @@ void main() { final newModel = model.copyWith(themeMode: ThemeMode.system); // Assert - expect(newModel, equals(model)); + check(newModel).equals(model); }); test('should support serialization to and from JSON', () { @@ -24,7 +25,7 @@ void main() { final newModel = SettingsModel.fromJson(json); // Assert - expect(newModel, equals(model)); + check(newModel).equals(model); }); }); } diff --git a/packages/app/test/src/features/settings/presentation/preferences/settings_page_test.dart b/packages/app/test/src/features/settings/presentation/preferences/settings_page_test.dart index 5162874..e643016 100644 --- a/packages/app/test/src/features/settings/presentation/preferences/settings_page_test.dart +++ b/packages/app/test/src/features/settings/presentation/preferences/settings_page_test.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:checks/checks.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -9,6 +10,7 @@ import 'package:our_democracy/src/features/settings/domain/settings_model.dart'; import 'package:our_democracy/src/features/settings/presentation/preferences/settings_page.dart'; import '../../../../../helpers/accessibility.dart'; +import '../../../../../helpers/checks.dart'; import '../../../../../helpers/mocks.dart'; import '../../../../../helpers/pump_app.dart'; @@ -43,7 +45,7 @@ void main() { await tester.tap(find.byKey(const ValueKey(ThemeMode.dark))); await tester.pumpAndSettle(); - expect(find.text('Dark Theme'), findsOneWidget); + check(find.text('Dark Theme')).findsOne(); verify(() => mockSharedPreferences.setString('prefs', darkMode)) .called(1);