From 2b47853abc8532bae3d5229fbb7eb80b8023b3fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20M=C3=A4gel?= Date: Fri, 22 Dec 2023 07:48:48 +0100 Subject: [PATCH] test(Dialog): Add Dialog widget tests --- lib/common_widgets/app_bar.dart | 24 +++- lib/common_widgets/contexts_dialog.dart | 35 ++--- lib/common_widgets/priorities_dialog.dart | 79 +++++++++++ lib/common_widgets/projects_dialog.dart | 35 ++--- lib/config/theme/theme.dart | 2 - .../filter/states/filter_cubit.dart | 14 ++ test/common_widgets/contexts_dialog_test.dart | 127 ++++++++++++++++++ .../priorities_dialog_test.dart | 127 ++++++++++++++++++ test/common_widgets/projects_dialog_test.dart | 127 ++++++++++++++++++ 9 files changed, 521 insertions(+), 49 deletions(-) create mode 100644 lib/common_widgets/priorities_dialog.dart create mode 100644 test/common_widgets/contexts_dialog_test.dart create mode 100644 test/common_widgets/priorities_dialog_test.dart create mode 100644 test/common_widgets/projects_dialog_test.dart diff --git a/lib/common_widgets/app_bar.dart b/lib/common_widgets/app_bar.dart index 3e15a55..20dba2a 100644 --- a/lib/common_widgets/app_bar.dart +++ b/lib/common_widgets/app_bar.dart @@ -4,8 +4,9 @@ import 'package:ntodotxt/common_widgets/contexts_dialog.dart'; import 'package:ntodotxt/common_widgets/filter_dialog.dart'; import 'package:ntodotxt/common_widgets/group_by_dialog.dart'; import 'package:ntodotxt/common_widgets/order_dialog.dart'; +import 'package:ntodotxt/common_widgets/priorities_dialog.dart'; import 'package:ntodotxt/common_widgets/projects_dialog.dart'; -import 'package:ntodotxt/constants/app.dart' show maxScreenWidthCompact; +import 'package:ntodotxt/domain/todo/todo_model.dart' show Priority; import 'package:ntodotxt/misc.dart' show CustomScrollBehavior, PlatformInfo; import 'package:ntodotxt/presentation/filter/states/filter_cubit.dart'; import 'package:ntodotxt/presentation/filter/states/filter_state.dart'; @@ -25,9 +26,7 @@ class MainAppBar extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { - final screenWidth = MediaQuery.of(context).size.width; return AppBar( - titleSpacing: screenWidth < maxScreenWidthCompact ? 0.0 : null, title: Text(title), actions: toolbar == null ? null @@ -114,11 +113,26 @@ class AppBarFilterList extends StatelessWidget { }, ), const SizedBox(width: 4), + ActionChip( + padding: EdgeInsets.zero, + avatar: const Icon(Icons.flag_outlined), + labelPadding: const EdgeInsets.only(right: 8.0), + label: + Text('priorities (${state.filter.priorities.length})'), + onPressed: () async { + await PriorityListDialog.dialog( + context: context, + cubit: BlocProvider.of(context), + items: Priority.values.toSet(), + ); + }, + ), + const SizedBox(width: 4), ActionChip( padding: EdgeInsets.zero, avatar: const Icon(Icons.rocket_launch_outlined), labelPadding: const EdgeInsets.only(right: 8.0), - label: const Text('projects'), + label: Text('projects (${state.filter.projects.length})'), onPressed: () async { await ProjectListDialog.dialog( context: context, @@ -132,7 +146,7 @@ class AppBarFilterList extends StatelessWidget { padding: EdgeInsets.zero, avatar: const Icon(Icons.join_inner), labelPadding: const EdgeInsets.only(right: 8.0), - label: const Text('contexts'), + label: Text('contexts (${state.filter.contexts.length})'), onPressed: () async { await ContextListDialog.dialog( context: context, diff --git a/lib/common_widgets/contexts_dialog.dart b/lib/common_widgets/contexts_dialog.dart index 7b86ddf..6debc77 100644 --- a/lib/common_widgets/contexts_dialog.dart +++ b/lib/common_widgets/contexts_dialog.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:ntodotxt/common_widgets/chip.dart'; import 'package:ntodotxt/presentation/filter/states/filter_cubit.dart' show FilterCubit; @@ -40,32 +41,24 @@ class _ContextListDialogState extends State { @override Widget build(BuildContext context) { return AlertDialog( - title: const Center( - child: Text('Contexts'), - ), - content: SizedBox( - width: double.maxFinite, - child: ListView.builder( - shrinkWrap: true, - itemCount: widget.items.length, - itemBuilder: (BuildContext context, int index) { - String context = widget.items.elementAt(index); - return CheckboxListTile( - controlAffinity: ListTileControlAffinity.leading, - title: Text(context), - value: selectedItems.contains(context), - onChanged: (bool? value) { + title: const Text('Contexts'), + content: GenericChipGroup( + children: [ + for (String item in widget.items) + GenericChoiceChip( + label: Text(item), + selected: selectedItems.contains(item), + onSelected: (bool selected) { setState(() { - if (value == true) { - selectedItems.add(context); + if (selected == true) { + selectedItems.add(item); } else { - selectedItems.remove(context); + selectedItems.remove(item); } }); }, - ); - }, - ), + ), + ], ), actions: [ TextButton( diff --git a/lib/common_widgets/priorities_dialog.dart b/lib/common_widgets/priorities_dialog.dart new file mode 100644 index 0000000..bde6d46 --- /dev/null +++ b/lib/common_widgets/priorities_dialog.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:ntodotxt/common_widgets/chip.dart'; +import 'package:ntodotxt/domain/todo/todo_model.dart' show Priority; +import 'package:ntodotxt/presentation/filter/states/filter_cubit.dart' + show FilterCubit; + +class PriorityListDialog extends StatefulWidget { + final FilterCubit cubit; + final Set items; + + const PriorityListDialog({ + required this.cubit, + required this.items, + super.key, + }); + + static Future dialog({ + required BuildContext context, + required FilterCubit cubit, + required Set items, + }) async { + return await showDialog( + context: context, + builder: (BuildContext context) => + PriorityListDialog(cubit: cubit, items: items), + ); + } + + @override + State createState() => _PriorityListDialogState(); +} + +class _PriorityListDialogState extends State { + Set selectedItems = {}; + + @override + void initState() { + selectedItems = {...widget.cubit.state.filter.priorities}; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Priorities'), + content: GenericChipGroup( + children: [ + for (Priority item in widget.items) + GenericChoiceChip( + label: Text(item.name), + selected: selectedItems.contains(item), + onSelected: (bool selected) { + setState(() { + if (selected == true) { + selectedItems.add(item); + } else { + selectedItems.remove(item); + } + }); + }, + ), + ], + ), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () => Navigator.pop(context), + ), + TextButton( + child: const Text('Apply'), + onPressed: () { + widget.cubit.updatePriorities(selectedItems); + Navigator.pop(context); + }, + ), + ], + ); + } +} diff --git a/lib/common_widgets/projects_dialog.dart b/lib/common_widgets/projects_dialog.dart index b87421b..82d60f2 100644 --- a/lib/common_widgets/projects_dialog.dart +++ b/lib/common_widgets/projects_dialog.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:ntodotxt/common_widgets/chip.dart'; import 'package:ntodotxt/presentation/filter/states/filter_cubit.dart' show FilterCubit; @@ -40,32 +41,24 @@ class _ProjectListDialogState extends State { @override Widget build(BuildContext context) { return AlertDialog( - title: const Center( - child: Text('Projects'), - ), - content: SizedBox( - width: double.maxFinite, - child: ListView.builder( - shrinkWrap: true, - itemCount: widget.items.length, - itemBuilder: (BuildContext context, int index) { - String project = widget.items.elementAt(index); - return CheckboxListTile( - controlAffinity: ListTileControlAffinity.leading, - title: Text(project), - value: selectedItems.contains(project), - onChanged: (bool? value) { + title: const Text('Projects'), + content: GenericChipGroup( + children: [ + for (String item in widget.items) + GenericChoiceChip( + label: Text(item), + selected: selectedItems.contains(item), + onSelected: (bool selected) { setState(() { - if (value == true) { - selectedItems.add(project); + if (selected == true) { + selectedItems.add(item); } else { - selectedItems.remove(project); + selectedItems.remove(item); } }); }, - ); - }, - ), + ), + ], ), actions: [ TextButton( diff --git a/lib/config/theme/theme.dart b/lib/config/theme/theme.dart index 5655ce0..0335141 100644 --- a/lib/config/theme/theme.dart +++ b/lib/config/theme/theme.dart @@ -11,7 +11,6 @@ final ThemeData dark = CustomTheme.dark; /// Customize versions of the theme data. final ThemeData lightTheme = light.copyWith( appBarTheme: light.appBarTheme.copyWith( - centerTitle: true, backgroundColor: Colors.transparent, ), snackBarTheme: light.snackBarTheme.copyWith( @@ -71,7 +70,6 @@ final ThemeData lightTheme = light.copyWith( ); final ThemeData darkTheme = dark.copyWith( appBarTheme: dark.appBarTheme.copyWith( - centerTitle: true, backgroundColor: Colors.transparent, ), snackBarTheme: light.snackBarTheme.copyWith( diff --git a/lib/presentation/filter/states/filter_cubit.dart b/lib/presentation/filter/states/filter_cubit.dart index 1a1d399..41b71ef 100644 --- a/lib/presentation/filter/states/filter_cubit.dart +++ b/lib/presentation/filter/states/filter_cubit.dart @@ -118,6 +118,20 @@ class FilterCubit extends Cubit { } } + void updatePriorities(Set priorities) { + try { + emit( + state.success( + filter: state.filter.copyWith( + priorities: {...priorities}, + ), + ), + ); + } on Exception catch (e) { + emit(state.error(message: e.toString())); + } + } + void addProject(String project) { try { emit( diff --git a/test/common_widgets/contexts_dialog_test.dart b/test/common_widgets/contexts_dialog_test.dart new file mode 100644 index 0000000..f57e556 --- /dev/null +++ b/test/common_widgets/contexts_dialog_test.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:ntodotxt/common_widgets/contexts_dialog.dart'; +import 'package:ntodotxt/data/filter/filter_controller.dart'; +import 'package:ntodotxt/domain/filter/filter_model.dart' show Filter; +import 'package:ntodotxt/domain/filter/filter_repository.dart'; +import 'package:ntodotxt/presentation/filter/states/filter_cubit.dart'; +import 'package:ntodotxt/presentation/filter/states/filter_state.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; + +class MaterialAppContextListDialog extends StatelessWidget { + const MaterialAppContextListDialog({super.key}); + + @override + Widget build(BuildContext context) { + return RepositoryProvider( + create: (BuildContext context) => FilterRepository( + FilterController(inMemoryDatabasePath), + ), + child: BlocProvider( + create: (BuildContext context) => FilterCubit( + repository: context.read(), + filter: const Filter(), + ), + child: Builder( + builder: (BuildContext context) { + return MaterialApp( + home: Scaffold( + body: BlocBuilder( + builder: (BuildContext context, FilterState state) { + return Column( + children: [ + Text(state.filter.contexts.toString()), + Builder( + builder: (BuildContext context) { + return TextButton( + child: const Text('Open dialog'), + onPressed: () async { + await ContextListDialog.dialog( + context: context, + cubit: BlocProvider.of(context), + items: {'context1', 'context2', 'context3'}, + ); + }, + ); + }, + ), + ], + ); + }, + ), + ), + ); + }, + ), + ), + ); + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('ContextListDialog', () { + testWidgets('apply', (tester) async { + await tester.pumpWidget(const MaterialAppContextListDialog()); + await tester.pump(); + + await tester.tap(find.text('Open dialog')); + await tester.pump(); + + expect(find.byType(Dialog), findsOneWidget); + await tester.tap(find.text('context1')); + await tester.pump(); + + await tester.tap(find.text('Apply')); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (Widget widget) => + widget is Text && widget.data!.contains('context1'), + ), + findsOneWidget, + ); + + await tester.tap(find.text('Open dialog')); + await tester.pump(); + + expect(find.byType(Dialog), findsOneWidget); + await tester.tap(find.text('context1')); + await tester.pump(); + + await tester.tap(find.text('Apply')); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && widget.data == '{}', + ), + findsOneWidget, + ); + }); + testWidgets('cancel', (tester) async { + await tester.pumpWidget(const MaterialAppContextListDialog()); + await tester.pump(); + + await tester.tap(find.text('Open dialog')); + await tester.pump(); + + expect(find.byType(Dialog), findsOneWidget); + await tester.tap(find.text('context1')); + await tester.pump(); + + await tester.tap(find.text('Cancel')); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && widget.data == '{}', + ), + findsOneWidget, + ); + }); + }); +} diff --git a/test/common_widgets/priorities_dialog_test.dart b/test/common_widgets/priorities_dialog_test.dart new file mode 100644 index 0000000..1adb01c --- /dev/null +++ b/test/common_widgets/priorities_dialog_test.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:ntodotxt/common_widgets/priorities_dialog.dart'; +import 'package:ntodotxt/data/filter/filter_controller.dart'; +import 'package:ntodotxt/domain/filter/filter_model.dart' show Filter; +import 'package:ntodotxt/domain/filter/filter_repository.dart'; +import 'package:ntodotxt/domain/todo/todo_model.dart' show Priority; +import 'package:ntodotxt/presentation/filter/states/filter_cubit.dart'; +import 'package:ntodotxt/presentation/filter/states/filter_state.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; + +class MaterialAppPriorityListDialog extends StatelessWidget { + const MaterialAppPriorityListDialog({super.key}); + + @override + Widget build(BuildContext context) { + return RepositoryProvider( + create: (BuildContext context) => FilterRepository( + FilterController(inMemoryDatabasePath), + ), + child: BlocProvider( + create: (BuildContext context) => FilterCubit( + repository: context.read(), + filter: const Filter(), + ), + child: Builder( + builder: (BuildContext context) { + return MaterialApp( + home: Scaffold( + body: BlocBuilder( + builder: (BuildContext context, FilterState state) { + return Column( + children: [ + Text(state.filter.priorities.toString()), + Builder( + builder: (BuildContext context) { + return TextButton( + child: const Text('Open dialog'), + onPressed: () async { + await PriorityListDialog.dialog( + context: context, + cubit: BlocProvider.of(context), + items: {Priority.A, Priority.B, Priority.C}, + ); + }, + ); + }, + ), + ], + ); + }, + ), + ), + ); + }, + ), + ), + ); + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('PriorityListDialog', () { + testWidgets('apply', (tester) async { + await tester.pumpWidget(const MaterialAppPriorityListDialog()); + await tester.pump(); + + await tester.tap(find.text('Open dialog')); + await tester.pump(); + + expect(find.byType(Dialog), findsOneWidget); + await tester.tap(find.text('A')); + await tester.pump(); + + await tester.tap(find.text('Apply')); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && widget.data!.contains('A'), + ), + findsOneWidget, + ); + + await tester.tap(find.text('Open dialog')); + await tester.pump(); + + expect(find.byType(Dialog), findsOneWidget); + await tester.tap(find.text('A')); + await tester.pump(); + + await tester.tap(find.text('Apply')); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && widget.data == '{}', + ), + findsOneWidget, + ); + }); + testWidgets('cancel', (tester) async { + await tester.pumpWidget(const MaterialAppPriorityListDialog()); + await tester.pump(); + + await tester.tap(find.text('Open dialog')); + await tester.pump(); + + expect(find.byType(Dialog), findsOneWidget); + await tester.tap(find.text('A')); + await tester.pump(); + + await tester.tap(find.text('Cancel')); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && widget.data == '{}', + ), + findsOneWidget, + ); + }); + }); +} diff --git a/test/common_widgets/projects_dialog_test.dart b/test/common_widgets/projects_dialog_test.dart new file mode 100644 index 0000000..c91a8c6 --- /dev/null +++ b/test/common_widgets/projects_dialog_test.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:ntodotxt/common_widgets/projects_dialog.dart'; +import 'package:ntodotxt/data/filter/filter_controller.dart'; +import 'package:ntodotxt/domain/filter/filter_model.dart' show Filter; +import 'package:ntodotxt/domain/filter/filter_repository.dart'; +import 'package:ntodotxt/presentation/filter/states/filter_cubit.dart'; +import 'package:ntodotxt/presentation/filter/states/filter_state.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; + +class MaterialAppProjectListDialog extends StatelessWidget { + const MaterialAppProjectListDialog({super.key}); + + @override + Widget build(BuildContext context) { + return RepositoryProvider( + create: (BuildContext context) => FilterRepository( + FilterController(inMemoryDatabasePath), + ), + child: BlocProvider( + create: (BuildContext context) => FilterCubit( + repository: context.read(), + filter: const Filter(), + ), + child: Builder( + builder: (BuildContext context) { + return MaterialApp( + home: Scaffold( + body: BlocBuilder( + builder: (BuildContext context, FilterState state) { + return Column( + children: [ + Text(state.filter.projects.toString()), + Builder( + builder: (BuildContext context) { + return TextButton( + child: const Text('Open dialog'), + onPressed: () async { + await ProjectListDialog.dialog( + context: context, + cubit: BlocProvider.of(context), + items: {'project1', 'project2', 'project3'}, + ); + }, + ); + }, + ), + ], + ); + }, + ), + ), + ); + }, + ), + ), + ); + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('ProjectListDialog', () { + testWidgets('apply', (tester) async { + await tester.pumpWidget(const MaterialAppProjectListDialog()); + await tester.pump(); + + await tester.tap(find.text('Open dialog')); + await tester.pump(); + + expect(find.byType(Dialog), findsOneWidget); + await tester.tap(find.text('project1')); + await tester.pump(); + + await tester.tap(find.text('Apply')); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (Widget widget) => + widget is Text && widget.data!.contains('project1'), + ), + findsOneWidget, + ); + + await tester.tap(find.text('Open dialog')); + await tester.pump(); + + expect(find.byType(Dialog), findsOneWidget); + await tester.tap(find.text('project1')); + await tester.pump(); + + await tester.tap(find.text('Apply')); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && widget.data == '{}', + ), + findsOneWidget, + ); + }); + testWidgets('cancel', (tester) async { + await tester.pumpWidget(const MaterialAppProjectListDialog()); + await tester.pump(); + + await tester.tap(find.text('Open dialog')); + await tester.pump(); + + expect(find.byType(Dialog), findsOneWidget); + await tester.tap(find.text('project1')); + await tester.pump(); + + await tester.tap(find.text('Cancel')); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && widget.data == '{}', + ), + findsOneWidget, + ); + }); + }); +}