diff --git a/analysis_options.yaml b/analysis_options.yaml index 49018fa3..cf1f1210 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,6 +2,7 @@ analyzer: exclude: - bricks/** - build/** + - "**/*.mocks.dart" formatter: page_width: 120 diff --git a/packages/flutter_deck/test/src/controls/actions/toggle_drawer_action_test.dart b/packages/flutter_deck/test/src/controls/actions/toggle_drawer_action_test.dart new file mode 100644 index 00000000..8f4a21ae --- /dev/null +++ b/packages/flutter_deck/test/src/controls/actions/toggle_drawer_action_test.dart @@ -0,0 +1,24 @@ +import 'package:flutter_deck/src/controls/actions/actions.dart'; +import 'package:flutter_deck/src/controls/controls.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'toggle_drawer_action_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('ToggleDrawerAction', () { + late final FlutterDeckControlsNotifier mockNotifier; + + setUp(() { + mockNotifier = MockFlutterDeckControlsNotifier(); + }); + + test('invoke should call toggleDrawer on notifier', () { + ToggleDrawerAction(mockNotifier).invoke(const ToggleDrawerIntent()); + + verify(mockNotifier.toggleDrawer()).called(1); + }); + }); +} diff --git a/packages/flutter_deck/test/src/controls/actions/toggle_marker_action_test.dart b/packages/flutter_deck/test/src/controls/actions/toggle_marker_action_test.dart new file mode 100644 index 00000000..f5c7d074 --- /dev/null +++ b/packages/flutter_deck/test/src/controls/actions/toggle_marker_action_test.dart @@ -0,0 +1,24 @@ +import 'package:flutter_deck/src/controls/actions/actions.dart'; +import 'package:flutter_deck/src/controls/controls.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'toggle_marker_action_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('ToggleMarkerAction', () { + late final FlutterDeckControlsNotifier mockNotifier; + + setUp(() { + mockNotifier = MockFlutterDeckControlsNotifier(); + }); + + test('invoke should call toggleMarker on notifier', () { + ToggleMarkerAction(mockNotifier).invoke(const ToggleMarkerIntent()); + + verify(mockNotifier.toggleMarker()).called(1); + }); + }); +} diff --git a/packages/flutter_deck/test/src/controls/l10n/flutter_deck_localization_notifier_test.dart b/packages/flutter_deck/test/src/controls/l10n/flutter_deck_localization_notifier_test.dart new file mode 100644 index 00000000..760eca8e --- /dev/null +++ b/packages/flutter_deck/test/src/controls/l10n/flutter_deck_localization_notifier_test.dart @@ -0,0 +1,21 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_deck/src/controls/l10n/flutter_deck_localization_notifier.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('FlutterDeckLocalizationNotifier', () { + test('update should change value', () { + const localeEn = Locale('en'); + const localeEs = Locale('es'); + + final notifier = FlutterDeckLocalizationNotifier(locale: localeEn, supportedLocales: const [localeEn, localeEs]); + + expect(notifier.value, localeEn); + expect(notifier.supportedLocales.length, 2); + + notifier.update(localeEs); + + expect(notifier.value, localeEs); + }); + }); +} diff --git a/packages/flutter_deck/test/src/controls/localized_shortcut_labeler_test.dart b/packages/flutter_deck/test/src/controls/localized_shortcut_labeler_test.dart new file mode 100644 index 00000000..92510e0e --- /dev/null +++ b/packages/flutter_deck/test/src/controls/localized_shortcut_labeler_test.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_deck/src/controls/localized_shortcut_labeler.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('LocalizedShortcutLabeler', () { + testWidgets('getShortcutLabel uses default labeler logic', (tester) async { + await tester.pumpWidget(const MaterialApp(home: SizedBox.shrink())); + + final context = tester.element(find.byType(SizedBox)); + final localizations = MaterialLocalizations.of(context); + + final labeler = LocalizedShortcutLabeler.instance; + final label = labeler.getShortcutLabel( + const SingleActivator(LogicalKeyboardKey.keyA, control: true), + localizations, + ); + + expect(label, 'Ctrl+A'); + }); + }); +} diff --git a/packages/flutter_deck/test/src/plugins/autoplay/flutter_deck_autoplay_notifier_test.dart b/packages/flutter_deck/test/src/plugins/autoplay/flutter_deck_autoplay_notifier_test.dart new file mode 100644 index 00000000..819f99a2 --- /dev/null +++ b/packages/flutter_deck/test/src/plugins/autoplay/flutter_deck_autoplay_notifier_test.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_deck/src/configuration/configuration.dart'; +import 'package:flutter_deck/src/flutter_deck_router.dart'; +import 'package:flutter_deck/src/plugins/autoplay/autoplay.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'flutter_deck_autoplay_notifier_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('FlutterDeckAutoplayNotifier', () { + late MockFlutterDeckRouter mockRouter; + + setUp(() { + mockRouter = MockFlutterDeckRouter(); + + when(mockRouter.currentSlideIndex).thenReturn(0); + when(mockRouter.currentStep).thenReturn(1); + when(mockRouter.slides).thenReturn([ + const FlutterDeckRouterSlide( + route: '/1', + widget: SizedBox(), + configuration: FlutterDeckSlideConfiguration(route: '/1'), + ), + ]); + when(mockRouter.currentSlideConfiguration).thenReturn(const FlutterDeckSlideConfiguration(route: '/1')); + }); + + test('initial state is correct', () { + final notifier = FlutterDeckAutoplayNotifier(router: mockRouter); + + expect(notifier.isPlaying, isFalse); + expect(notifier.isLooping, isFalse); + expect(notifier.autoplayDuration, const Duration(seconds: 5)); + }); + + test('play changes isPlaying to true', () { + final notifier = FlutterDeckAutoplayNotifier(router: mockRouter); + + var listenerCalled = false; + + notifier + ..addListener(() => listenerCalled = true) + ..play(); + + expect(notifier.isPlaying, isTrue); + expect(listenerCalled, isTrue); + + notifier.pause(); + }); + + test('pause changes isPlaying to false', () { + final notifier = FlutterDeckAutoplayNotifier(router: mockRouter)..play(); + + var listenerCalled = false; + + notifier + ..addListener(() => listenerCalled = true) + ..pause(); + + expect(notifier.isPlaying, isFalse); + expect(listenerCalled, isTrue); + }); + + test('toggleLooping toggles isLooping', () { + final notifier = FlutterDeckAutoplayNotifier(router: mockRouter); + + var listenerCalled = false; + + notifier + ..addListener(() => listenerCalled = true) + ..toggleLooping(); + + expect(notifier.isLooping, isTrue); + expect(listenerCalled, isTrue); + + notifier.toggleLooping(); + + expect(notifier.isLooping, isFalse); + }); + + test('updateAutoplayDuration updates duration and restarts if playing', () { + final notifier = FlutterDeckAutoplayNotifier(router: mockRouter)..play(); + + var listenerCalled = false; + + notifier + ..addListener(() => listenerCalled = true) + ..updateAutoplayDuration(const Duration(seconds: 10)); + + expect(notifier.autoplayDuration, const Duration(seconds: 10)); + expect(listenerCalled, isTrue); + expect(notifier.isPlaying, isTrue); + + notifier.pause(); + }); + }); +} diff --git a/packages/flutter_deck/test/src/plugins/autoplay/flutter_deck_autoplay_plugin_test.dart b/packages/flutter_deck/test/src/plugins/autoplay/flutter_deck_autoplay_plugin_test.dart new file mode 100644 index 00000000..cbc6dca7 --- /dev/null +++ b/packages/flutter_deck/test/src/plugins/autoplay/flutter_deck_autoplay_plugin_test.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_deck/src/controls/controls.dart'; +import 'package:flutter_deck/src/flutter_deck.dart'; +import 'package:flutter_deck/src/flutter_deck_router.dart'; +import 'package:flutter_deck/src/plugins/autoplay/autoplay.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'flutter_deck_autoplay_plugin_test.mocks.dart'; + +class MockBuildContext extends Mock implements BuildContext {} + +@GenerateNiceMocks([MockSpec(), MockSpec(), MockSpec()]) +void main() { + group('FlutterDeckAutoplayPlugin', () { + late MockFlutterDeck mockFlutterDeck; + late MockFlutterDeckRouter mockRouter; + late MockFlutterDeckControlsNotifier mockControlsNotifier; + + setUp(() { + mockFlutterDeck = MockFlutterDeck(); + mockRouter = MockFlutterDeckRouter(); + mockControlsNotifier = MockFlutterDeckControlsNotifier(); + + when(mockFlutterDeck.router).thenReturn(mockRouter); + when(mockFlutterDeck.controlsNotifier).thenReturn(mockControlsNotifier); + when(mockControlsNotifier.controlsVisible).thenReturn(false); + }); + + test('init and dispose manage listeners', () { + final plugin = FlutterDeckAutoplayPlugin()..init(mockFlutterDeck); + verify(mockControlsNotifier.addListener(any)).called(1); + + plugin.dispose(); + verify(mockControlsNotifier.removeListener(any)).called(1); + }); + + testWidgets('wrap provides AutoplayProvider', (tester) async { + final plugin = FlutterDeckAutoplayPlugin()..init(mockFlutterDeck); + + await tester.pumpWidget(MaterialApp(home: plugin.wrap(MockBuildContext(), const Text('ChildWidget')))); + + expect(find.byType(FlutterDeckAutoplayProvider), findsOneWidget); + expect(find.text('ChildWidget'), findsOneWidget); + }); + + test('buildControls returns menu items', () { + final plugin = FlutterDeckAutoplayPlugin()..init(mockFlutterDeck); + + final controls = plugin.buildControls(MockBuildContext(), ( + context, { + required String label, + required VoidCallback? onPressed, + Widget? icon, + bool? closeOnActivate, + }) { + return const SizedBox(); + }); + + expect(controls.length, 1); + }); + }); +} diff --git a/packages/flutter_deck/test/src/plugins/autoplay/flutter_deck_autoplay_provider_test.dart b/packages/flutter_deck/test/src/plugins/autoplay/flutter_deck_autoplay_provider_test.dart new file mode 100644 index 00000000..57dcdf04 --- /dev/null +++ b/packages/flutter_deck/test/src/plugins/autoplay/flutter_deck_autoplay_provider_test.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_deck/src/flutter_deck_router.dart'; +import 'package:flutter_deck/src/plugins/autoplay/autoplay.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; + +import 'flutter_deck_autoplay_provider_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('FlutterDeckAutoplayProvider', () { + late MockFlutterDeckRouter mockRouter; + + setUp(() { + mockRouter = MockFlutterDeckRouter(); + }); + + testWidgets('provides notifier down the tree', (tester) async { + final notifier = FlutterDeckAutoplayNotifier(router: mockRouter); + + await tester.pumpWidget( + MaterialApp( + home: FlutterDeckAutoplayProvider( + notifier: notifier, + child: Builder( + builder: (context) { + final providedNotifier = FlutterDeckAutoplayProvider.of(context); + expect(providedNotifier, equals(notifier)); + return const SizedBox(); + }, + ), + ), + ), + ); + }); + + testWidgets('throws if notifier not found', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + FlutterDeckAutoplayProvider.of(context); + return const SizedBox(); + }, + ), + ), + ); + expect(tester.takeException(), isAssertionError); + }); + }); +} diff --git a/packages/flutter_deck/test/src/plugins/flutter_deck_plugin_test.dart b/packages/flutter_deck/test/src/plugins/flutter_deck_plugin_test.dart new file mode 100644 index 00000000..1348bdaa --- /dev/null +++ b/packages/flutter_deck/test/src/plugins/flutter_deck_plugin_test.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_deck/src/plugins/flutter_deck_plugin.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class _TestPlugin extends FlutterDeckPlugin { + const _TestPlugin(); +} + +class _MockBuildContext extends BuildContext { + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +void main() { + group('FlutterDeckPlugin', () { + test('default methods do nothing or return default values', () { + const plugin = _TestPlugin(); + + final mockContext = _MockBuildContext(); + + plugin.dispose(); // Should not throw + + final controls = plugin.buildControls(mockContext, ( + context, { + required String label, + required VoidCallback? onPressed, + Widget? icon, + bool? closeOnActivate, + }) { + return const SizedBox(); + }); + expect(controls, isEmpty); + + final wrappedChild = plugin.wrap(mockContext, const Text('Child')); + expect(wrappedChild, isA()); + }); + }); +} diff --git a/packages/flutter_deck/test/src/presenter/flutter_deck_presenter_controller_test.dart b/packages/flutter_deck/test/src/presenter/flutter_deck_presenter_controller_test.dart new file mode 100644 index 00000000..9681ea03 --- /dev/null +++ b/packages/flutter_deck/test/src/presenter/flutter_deck_presenter_controller_test.dart @@ -0,0 +1,208 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_deck/src/controls/controls.dart'; +import 'package:flutter_deck/src/flutter_deck_router.dart'; +import 'package:flutter_deck/src/presenter/flutter_deck_presenter_controller.dart'; +import 'package:flutter_deck/src/theme/flutter_deck_theme_notifier.dart'; +import 'package:flutter_deck/src/widgets/internal/internal.dart'; +import 'package:flutter_deck_client/flutter_deck_client.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'flutter_deck_presenter_controller_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), +]) +void main() { + group('FlutterDeckPresenterController', () { + late MockFlutterDeckControlsNotifier mockControlsNotifier; + late MockFlutterDeckLocalizationNotifier mockLocalizationNotifier; + late MockFlutterDeckMarkerNotifier mockMarkerNotifier; + late MockFlutterDeckThemeNotifier mockThemeNotifier; + late MockFlutterDeckRouter mockRouter; + late MockFlutterDeckClient mockClient; + late StreamController stateController; + + setUp(() { + mockControlsNotifier = MockFlutterDeckControlsNotifier(); + mockLocalizationNotifier = MockFlutterDeckLocalizationNotifier(); + mockMarkerNotifier = MockFlutterDeckMarkerNotifier(); + mockThemeNotifier = MockFlutterDeckThemeNotifier(); + mockRouter = MockFlutterDeckRouter(); + mockClient = MockFlutterDeckClient(); + stateController = StreamController.broadcast(); + + when(mockLocalizationNotifier.value).thenReturn(const Locale('en')); + when(mockThemeNotifier.value).thenReturn(ThemeMode.dark); + when(mockRouter.currentSlideIndex).thenReturn(0); + when(mockRouter.currentStep).thenReturn(1); + when(mockRouter.isPresenterView).thenReturn(false); + when(mockClient.flutterDeckStateStream).thenAnswer((_) => stateController.stream); + }); + + tearDown(() { + stateController.close(); + }); + + test('init should do nothing if client is null', () { + final controller = FlutterDeckPresenterController( + controlsNotifier: mockControlsNotifier, + localizationNotifier: mockLocalizationNotifier, + markerNotifier: mockMarkerNotifier, + themeNotifier: mockThemeNotifier, + router: mockRouter, + )..init(); + + expect(controller.available, isFalse); + }); + + test('init should initialize client and add listeners', () { + final controller = FlutterDeckPresenterController( + controlsNotifier: mockControlsNotifier, + localizationNotifier: mockLocalizationNotifier, + markerNotifier: mockMarkerNotifier, + themeNotifier: mockThemeNotifier, + router: mockRouter, + client: mockClient, + )..init(); + + expect(controller.available, isTrue); + + verify(mockClient.init(any)).called(1); + verify(mockLocalizationNotifier.addListener(any)).called(1); + verify(mockMarkerNotifier.addListener(any)).called(1); + verify(mockThemeNotifier.addListener(any)).called(1); + verify(mockRouter.addListener(any)).called(1); + }); + + test('init when already initialized should update state if not presenter view', () { + final controller = FlutterDeckPresenterController( + controlsNotifier: mockControlsNotifier, + localizationNotifier: mockLocalizationNotifier, + markerNotifier: mockMarkerNotifier, + themeNotifier: mockThemeNotifier, + router: mockRouter, + client: mockClient, + )..init(); + verify(mockClient.init(any)).called(1); + + controller.init(); + verify(mockClient.updateState(any)).called(1); + }); + + test('dispose should cancel subscription and remove listeners', () { + FlutterDeckPresenterController( + controlsNotifier: mockControlsNotifier, + localizationNotifier: mockLocalizationNotifier, + markerNotifier: mockMarkerNotifier, + themeNotifier: mockThemeNotifier, + router: mockRouter, + client: mockClient, + ) + ..init() + ..dispose(); + + verify(mockClient.dispose()).called(1); + verify(mockLocalizationNotifier.removeListener(any)).called(1); + verify(mockMarkerNotifier.removeListener(any)).called(1); + verify(mockThemeNotifier.removeListener(any)).called(1); + verify(mockRouter.removeListener(any)).called(1); + }); + + test('open should open presenter view', () { + FlutterDeckPresenterController( + controlsNotifier: mockControlsNotifier, + localizationNotifier: mockLocalizationNotifier, + markerNotifier: mockMarkerNotifier, + themeNotifier: mockThemeNotifier, + router: mockRouter, + client: mockClient, + ).open(); + + verify(mockClient.openPresenterView()).called(1); + }); + + test('listeners should notify client with updated state', () { + FlutterDeckPresenterController( + controlsNotifier: mockControlsNotifier, + localizationNotifier: mockLocalizationNotifier, + markerNotifier: mockMarkerNotifier, + themeNotifier: mockThemeNotifier, + router: mockRouter, + client: mockClient, + ).init(); + + // Clear the initial client.init call if we want to just check updateState + clearInteractions(mockClient); + + // Simulate route change + when(mockRouter.currentSlideIndex).thenReturn(1); + + final onRouteChanged = verify(mockRouter.addListener(captureAny)).captured.first as VoidCallback; + onRouteChanged(); + + verify(mockClient.updateState(any)).called(1); + + // Simulate localization change + when(mockLocalizationNotifier.value).thenReturn(const Locale('es')); + + final onLocalizationChanged = + verify(mockLocalizationNotifier.addListener(captureAny)).captured.first as VoidCallback; + onLocalizationChanged(); + + verify(mockClient.updateState(any)).called(1); + + // Simulate marker change + when(mockMarkerNotifier.enabled).thenReturn(true); + + final onMarkerStateChanged = verify(mockMarkerNotifier.addListener(captureAny)).captured.first as VoidCallback; + onMarkerStateChanged(); + + verify(mockClient.updateState(any)).called(1); + + // Simulate theme change + when(mockThemeNotifier.value).thenReturn(ThemeMode.light); + + final onThemeChanged = verify(mockThemeNotifier.addListener(captureAny)).captured.first as VoidCallback; + onThemeChanged(); + + verify(mockClient.updateState(any)).called(1); + }); + + test('_onStateChanged updates local notifiers', () async { + FlutterDeckPresenterController( + controlsNotifier: mockControlsNotifier, + localizationNotifier: mockLocalizationNotifier, + markerNotifier: mockMarkerNotifier, + themeNotifier: mockThemeNotifier, + router: mockRouter, + client: mockClient, + ).init(); + + const newState = FlutterDeckState( + slideIndex: 2, + slideStep: 3, + locale: 'es', + markerEnabled: true, + themeMode: 'light', + ); + + stateController.add(newState); + await Future.delayed(Duration.zero); // Wait for stream event + + verify(mockControlsNotifier.goToSlide(3)).called(1); + verify(mockControlsNotifier.goToStep(3)).called(1); + verify(mockLocalizationNotifier.update(const Locale('es'))).called(1); + verify(mockControlsNotifier.toggleMarker()).called(1); + verify(mockThemeNotifier.update(ThemeMode.light)).called(1); + }); + }); +} diff --git a/packages/flutter_deck/test/src/presenter/widgets/flutter_deck_presenter_slide_preview_test.dart b/packages/flutter_deck/test/src/presenter/widgets/flutter_deck_presenter_slide_preview_test.dart new file mode 100644 index 00000000..b564b1b5 --- /dev/null +++ b/packages/flutter_deck/test/src/presenter/widgets/flutter_deck_presenter_slide_preview_test.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_deck/src/configuration/configuration.dart'; +import 'package:flutter_deck/src/flutter_deck.dart'; +import 'package:flutter_deck/src/flutter_deck_router.dart'; +import 'package:flutter_deck/src/presenter/widgets/flutter_deck_presenter_slide_preview.dart'; +import 'package:flutter_deck/src/theme/flutter_deck_theme.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'flutter_deck_presenter_slide_preview_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec()]) +void main() { + group('FlutterDeckPresenterSlidePreview', () { + late MockFlutterDeckRouter mockRouter; + late MockFlutterDeck mockDeck; + + setUp(() { + mockRouter = MockFlutterDeckRouter(); + mockDeck = MockFlutterDeck(); + + when(mockRouter.currentSlideIndex).thenReturn(0); + when(mockRouter.currentStep).thenReturn(1); + when(mockRouter.currentSlideConfiguration).thenReturn(const FlutterDeckSlideConfiguration(route: '/1', steps: 2)); + when(mockRouter.slides).thenReturn([ + const FlutterDeckRouterSlide( + route: '/1', + widget: SizedBox(), + configuration: FlutterDeckSlideConfiguration(route: '/1'), + ), + const FlutterDeckRouterSlide( + route: '/2', + widget: SizedBox(), + configuration: FlutterDeckSlideConfiguration(route: '/2'), + ), + ]); + when(mockDeck.router).thenReturn(mockRouter); + when(mockDeck.globalConfiguration).thenReturn(const FlutterDeckConfiguration()); + }); + + testWidgets('builds slide preview headers correctly', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FlutterDeckTheme( + data: FlutterDeckThemeData.light(), + child: FlutterDeckProvider(flutterDeck: mockDeck, child: FlutterDeckPresenterSlidePreview()), + ), + ), + ), + ); + + expect(find.text('Current: Slide 1 of 2 (step 1 of 2)'), findsOneWidget); + expect(find.text('Next: Slide 2 of 2'), findsOneWidget); + }); + }); +} diff --git a/packages/flutter_deck/test/src/presenter/widgets/flutter_deck_presenter_timer_test.dart b/packages/flutter_deck/test/src/presenter/widgets/flutter_deck_presenter_timer_test.dart new file mode 100644 index 00000000..0e6fe53f --- /dev/null +++ b/packages/flutter_deck/test/src/presenter/widgets/flutter_deck_presenter_timer_test.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_deck/src/presenter/widgets/flutter_deck_presenter_timer.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('FlutterDeckPresenterTimer', () { + testWidgets('builds timer', (tester) async { + await tester.pumpWidget(const MaterialApp(home: Scaffold(body: FlutterDeckPresenterTimer()))); + + expect(find.byType(FlutterDeckPresenterTimer), findsOneWidget); + }); + }); +} diff --git a/packages/flutter_deck/test/src/presenter/widgets/flutter_deck_presenter_view_test.dart b/packages/flutter_deck/test/src/presenter/widgets/flutter_deck_presenter_view_test.dart new file mode 100644 index 00000000..af57d6eb --- /dev/null +++ b/packages/flutter_deck/test/src/presenter/widgets/flutter_deck_presenter_view_test.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_deck/src/configuration/configuration.dart'; +import 'package:flutter_deck/src/controls/controls.dart'; +import 'package:flutter_deck/src/flutter_deck.dart'; +import 'package:flutter_deck/src/flutter_deck_router.dart'; +import 'package:flutter_deck/src/presenter/presenter.dart'; +import 'package:flutter_deck/src/presenter/widgets/flutter_deck_presenter_slide_preview.dart'; +import 'package:flutter_deck/src/presenter/widgets/flutter_deck_presenter_timer.dart'; +import 'package:flutter_deck/src/presenter/widgets/flutter_deck_speaker_notes.dart'; +import 'package:flutter_deck/src/theme/flutter_deck_theme.dart'; +import 'package:flutter_deck/src/widgets/internal/internal.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'flutter_deck_presenter_view_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), +]) +void main() { + group('PresenterView', () { + late MockFlutterDeck mockDeck; + late MockFlutterDeckRouter mockRouter; + late MockFlutterDeckPresenterController mockController; + late MockFlutterDeckControlsNotifier mockControlsNotifier; + late MockFlutterDeckDrawerNotifier mockDrawerNotifier; + + setUp(() { + mockDeck = MockFlutterDeck(); + mockRouter = MockFlutterDeckRouter(); + mockController = MockFlutterDeckPresenterController(); + mockControlsNotifier = MockFlutterDeckControlsNotifier(); + mockDrawerNotifier = MockFlutterDeckDrawerNotifier(); + + when(mockRouter.currentSlideIndex).thenReturn(0); + when(mockRouter.currentStep).thenReturn(1); + when( + mockRouter.currentSlideConfiguration, + ).thenReturn(const FlutterDeckSlideConfiguration(route: '/1', steps: 1, speakerNotes: 'Some notes')); + when(mockRouter.slides).thenReturn([ + const FlutterDeckRouterSlide( + route: '/1', + widget: SizedBox(), + configuration: FlutterDeckSlideConfiguration(route: '/1', speakerNotes: 'Some notes'), + ), + ]); + + when(mockDeck.router).thenReturn(mockRouter); + when(mockDeck.presenterController).thenReturn(mockController); + when(mockDeck.controlsNotifier).thenReturn(mockControlsNotifier); + when(mockDeck.drawerNotifier).thenReturn(mockDrawerNotifier); + when(mockDeck.globalConfiguration).thenReturn(const FlutterDeckConfiguration()); + + when(mockControlsNotifier.controlsVisible).thenReturn(false); + }); + + testWidgets('builds presenterView correctly', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: FlutterDeckTheme( + data: FlutterDeckThemeData.light(), + child: FlutterDeckProvider(flutterDeck: mockDeck, child: const PresenterView()), + ), + ), + ); + + verify(mockController.init()).called(1); + + expect(find.byType(FlutterDeckPresenterTimer), findsOneWidget); + expect(find.byType(FlutterDeckPresenterSlidePreview), findsOneWidget); + expect(find.byType(FlutterDeckSpeakerNotes), findsOneWidget); + + expect(find.text('Some notes'), findsOneWidget); + }); + + testWidgets('disposes controller correctly', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: FlutterDeckTheme( + data: FlutterDeckThemeData.light(), + child: FlutterDeckProvider(flutterDeck: mockDeck, child: const PresenterView()), + ), + ), + ); + + verify(mockController.init()).called(1); + + await tester.pumpWidget(const SizedBox()); + + verify(mockController.dispose()).called(1); + }); + }); +} diff --git a/packages/flutter_deck/test/src/presenter/widgets/flutter_deck_speaker_notes_test.dart b/packages/flutter_deck/test/src/presenter/widgets/flutter_deck_speaker_notes_test.dart new file mode 100644 index 00000000..c49b004a --- /dev/null +++ b/packages/flutter_deck/test/src/presenter/widgets/flutter_deck_speaker_notes_test.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_deck/src/configuration/configuration.dart'; +import 'package:flutter_deck/src/flutter_deck.dart'; +import 'package:flutter_deck/src/flutter_deck_router.dart'; +import 'package:flutter_deck/src/presenter/widgets/flutter_deck_speaker_notes.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'flutter_deck_speaker_notes_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec()]) +void main() { + group('FlutterDeckSpeakerNotes', () { + late MockFlutterDeck mockDeck; + late MockFlutterDeckRouter mockRouter; + + setUp(() { + mockDeck = MockFlutterDeck(); + mockRouter = MockFlutterDeckRouter(); + + when( + mockRouter.currentSlideConfiguration, + ).thenReturn(const FlutterDeckSlideConfiguration(route: '/1', speakerNotes: 'test note')); + when(mockDeck.router).thenReturn(mockRouter); + }); + + testWidgets('builds notes', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FlutterDeckProvider(flutterDeck: mockDeck, child: const FlutterDeckSpeakerNotes()), + ), + ), + ); + + expect(find.byType(FlutterDeckSpeakerNotes), findsOneWidget); + expect(find.text('test note'), findsOneWidget); + }); + }); +} diff --git a/packages/flutter_deck/test/src/renderers/flutter_deck_slide_image_renderer_test.dart b/packages/flutter_deck/test/src/renderers/flutter_deck_slide_image_renderer_test.dart new file mode 100644 index 00000000..8f8d344d --- /dev/null +++ b/packages/flutter_deck/test/src/renderers/flutter_deck_slide_image_renderer_test.dart @@ -0,0 +1,23 @@ +import 'package:flutter_deck/src/flutter_deck.dart'; +import 'package:flutter_deck/src/renderers/flutter_deck_slide_image_renderer.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; + +import 'flutter_deck_slide_image_renderer_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('FlutterDeckSlideImageRenderer', () { + late MockFlutterDeck mockFlutterDeck; + + setUp(() { + mockFlutterDeck = MockFlutterDeck(); + }); + + test('default instantiation', () { + final renderer = FlutterDeckSlideImageRenderer(flutterDeck: mockFlutterDeck); + + expect(renderer, isNotNull); + }); + }); +} diff --git a/packages/flutter_deck/test/src/widgets/flutter_deck_slide_steps_test.dart b/packages/flutter_deck/test/src/widgets/flutter_deck_slide_steps_test.dart new file mode 100644 index 00000000..1416601c --- /dev/null +++ b/packages/flutter_deck/test/src/widgets/flutter_deck_slide_steps_test.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_deck/src/flutter_deck.dart'; +import 'package:flutter_deck/src/flutter_deck_router.dart'; +import 'package:flutter_deck/src/widgets/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'flutter_deck_slide_steps_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec()]) +void main() { + group('FlutterDeckSlideSteps', () { + late MockFlutterDeck mockDeck; + late MockFlutterDeckRouter mockRouter; + + setUp(() { + mockDeck = MockFlutterDeck(); + mockRouter = MockFlutterDeckRouter(); + + when(mockRouter.currentStep).thenReturn(1); + when(mockRouter.currentSlideIndex).thenReturn(0); + when(mockDeck.router).thenReturn(mockRouter); + when(mockDeck.stepNumber).thenAnswer((_) => mockRouter.currentStep); + when(mockDeck.slideNumber).thenAnswer((_) => mockRouter.currentSlideIndex + 1); + }); + + testWidgets('builder and listener build properly', (tester) async { + final routerListeners = []; + when(mockRouter.addListener(any)).thenAnswer((invocation) { + routerListeners.add(invocation.positionalArguments[0]); + }); + + var listenerCalled = false; + + await tester.pumpWidget( + MaterialApp( + home: FlutterDeckProvider( + flutterDeck: mockDeck, + child: Scaffold( + body: FlutterDeckSlideStepsListener( + listener: (context, step) { + listenerCalled = true; + }, + child: FlutterDeckSlideStepsBuilder(builder: (context, step) => Text('Step $step')), + ), + ), + ), + ), + ); + + expect(find.text('Step 1'), findsOneWidget); + expect(listenerCalled, isFalse); + + when(mockRouter.currentStep).thenReturn(2); + for (final listener in routerListeners) { + listener(); + } + + await tester.pumpAndSettle(); + + expect(find.text('Step 2'), findsOneWidget); + expect(listenerCalled, isTrue); + }); + }); +} diff --git a/packages/flutter_deck/test/src/widgets/internal/drawer/flutter_deck_drawer_notifier_test.dart b/packages/flutter_deck/test/src/widgets/internal/drawer/flutter_deck_drawer_notifier_test.dart new file mode 100644 index 00000000..d2e5a660 --- /dev/null +++ b/packages/flutter_deck/test/src/widgets/internal/drawer/flutter_deck_drawer_notifier_test.dart @@ -0,0 +1,18 @@ +import 'package:flutter_deck/src/widgets/internal/drawer/flutter_deck_drawer_notifier.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('FlutterDeckDrawerNotifier', () { + test('toggle should notify listeners', () { + final notifier = FlutterDeckDrawerNotifier(); + + var listenerCalled = false; + + notifier + ..addListener(() => listenerCalled = true) + ..toggle(); + + expect(listenerCalled, isTrue); + }); + }); +} diff --git a/packages/flutter_deck/test/src/widgets/internal/drawer/flutter_deck_drawer_test.dart b/packages/flutter_deck/test/src/widgets/internal/drawer/flutter_deck_drawer_test.dart new file mode 100644 index 00000000..101dcb46 --- /dev/null +++ b/packages/flutter_deck/test/src/widgets/internal/drawer/flutter_deck_drawer_test.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_deck/src/flutter_deck.dart'; +import 'package:flutter_deck/src/flutter_deck_router.dart'; +import 'package:flutter_deck/src/widgets/internal/drawer/flutter_deck_drawer.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'flutter_deck_drawer_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec()]) +void main() { + group('FlutterDeckDrawer', () { + late MockFlutterDeck mockDeck; + late MockFlutterDeckRouter mockRouter; + + setUp(() { + mockDeck = MockFlutterDeck(); + mockRouter = MockFlutterDeckRouter(); + + when(mockDeck.router).thenReturn(mockRouter); + when(mockRouter.slides).thenReturn([]); + }); + + testWidgets('builds the drawer correctly', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + drawer: FlutterDeckProvider(flutterDeck: mockDeck, child: const FlutterDeckDrawer()), + ), + ), + ); + + tester.state(find.byType(Scaffold)).openDrawer(); + + await tester.pumpAndSettle(); + + expect(find.byType(FlutterDeckDrawer), findsOneWidget); + }); + }); +} diff --git a/packages/flutter_deck/test/src/widgets/internal/marker/flutter_deck_marker_test.dart b/packages/flutter_deck/test/src/widgets/internal/marker/flutter_deck_marker_test.dart new file mode 100644 index 00000000..cc8ee9cc --- /dev/null +++ b/packages/flutter_deck/test/src/widgets/internal/marker/flutter_deck_marker_test.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_deck/src/configuration/configuration.dart'; +import 'package:flutter_deck/src/flutter_deck.dart'; +import 'package:flutter_deck/src/widgets/internal/marker/flutter_deck_marker.dart'; +import 'package:flutter_deck/src/widgets/internal/marker/flutter_deck_marker_notifier.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'flutter_deck_marker_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec()]) +void main() { + group('FlutterDeckMarker', () { + late MockFlutterDeck mockDeck; + late MockFlutterDeckConfiguration mockConfig; + + setUp(() { + mockDeck = MockFlutterDeck(); + mockConfig = MockFlutterDeckConfiguration(); + + when(mockConfig.marker).thenReturn(const FlutterDeckMarkerConfiguration()); + when(mockDeck.globalConfiguration).thenReturn(mockConfig); + }); + + testWidgets('builds when enabled and draws on canvas', (tester) async { + final notifier = FlutterDeckMarkerNotifier()..toggle(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FlutterDeckProvider( + flutterDeck: mockDeck, + child: FlutterDeckMarker(notifier: notifier, child: const SizedBox(width: 100, height: 100)), + ), + ), + ), + ); + + expect(find.byType(GestureDetector), findsOneWidget); + expect(find.byType(CustomPaint), findsWidgets); + + final gestureDetector = find.byType(GestureDetector).first; + + await tester.tap(gestureDetector); + await tester.pumpAndSettle(); + }); + + testWidgets('does not build gesture detector when disabled', (tester) async { + final notifier = FlutterDeckMarkerNotifier(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FlutterDeckProvider( + flutterDeck: mockDeck, + child: FlutterDeckMarker(notifier: notifier, child: const SizedBox(width: 100, height: 100)), + ), + ), + ), + ); + + expect(find.byType(GestureDetector), findsNothing); + }); + }); +} diff --git a/packages/flutter_deck_client/test/flutter_deck_state_test.dart b/packages/flutter_deck_client/test/flutter_deck_state_test.dart new file mode 100644 index 00000000..409cec0b --- /dev/null +++ b/packages/flutter_deck_client/test/flutter_deck_state_test.dart @@ -0,0 +1,33 @@ +import 'package:flutter_deck_client/flutter_deck_client.dart'; +import 'package:test/test.dart'; + +void main() { + group('FlutterDeckState', () { + test('fromJson and toJson', () { + const state = FlutterDeckState(locale: 'en', themeMode: 'dark', markerEnabled: true, slideIndex: 2, slideStep: 3); + + final json = state.toJson(); + final newState = FlutterDeckState.fromJson(json); + + expect(newState, equals(state)); + expect(newState.hashCode, equals(state.hashCode)); + + final newState2 = state.copyWith(slideIndex: 10, slideStep: 5); + + expect(newState2.slideIndex, equals(10)); + expect(newState2.slideStep, equals(5)); + }); + + test('copyWith', () { + const state = FlutterDeckState(locale: 'en', themeMode: 'dark'); + + final newState = state.copyWith(locale: 'es', markerEnabled: true); + + expect(newState.locale, equals('es')); + expect(newState.themeMode, equals('dark')); + expect(newState.markerEnabled, isTrue); + expect(newState.slideIndex, equals(0)); + expect(newState.slideStep, equals(1)); + }); + }); +}