diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..4206c51d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: Flutter Package CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Clone repository + uses: actions/checkout@v3 + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + + - name: Flutter version + run: flutter --version + + - name: Install dependencies + run: flutter pub get + + - name: Verify formatting + run: dart format --output=none --set-exit-if-changed . + + - name: Analyze project source + run: flutter analyze + + - name: Run tests + run: flutter test -r github diff --git a/.github/workflows/code-cov.yml b/.github/workflows/code-cov.yml new file mode 100644 index 00000000..3fae1b36 --- /dev/null +++ b/.github/workflows/code-cov.yml @@ -0,0 +1,28 @@ +name: Code Coverage + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + upload: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + - name: Get packages + run: flutter pub get + - name: Generate coverage file + run: flutter test --coverage + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true + files: ./coverage/lcov.info + flags: flutter diff --git a/.gitignore b/.gitignore index ec0a1326..b4db0517 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ .history .svn/ migrate_working_dir/ +coverage/ # IntelliJ related *.iml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..3ebc5e7a --- /dev/null +++ b/codecov.yml @@ -0,0 +1,3 @@ +coverage: + ignore: + - "**._()" # Ignore all private constructors diff --git a/lib/src/built_in/toastification_type.dart b/lib/src/built_in/toastification_type.dart index 6d366c83..4ef669b8 100644 --- a/lib/src/built_in/toastification_type.dart +++ b/lib/src/built_in/toastification_type.dart @@ -18,10 +18,8 @@ class ToastificationType { static const error = ToastificationType._('error', errorColor, Iconsax.close_circle_copy); - // Factory for custom types - static ToastificationType custom(String name, Color color, IconData icon) { - return ToastificationType._(name, color, icon); - } + const factory ToastificationType.custom( + String name, Color color, IconData icon) = ToastificationType._; static List get defaultValues => [info, success, warning, error]; diff --git a/lib/src/core/toastification.dart b/lib/src/core/toastification.dart index 54b84a22..d1daa0b6 100644 --- a/lib/src/core/toastification.dart +++ b/lib/src/core/toastification.dart @@ -240,11 +240,11 @@ class Toastification { OverlayState? overlayState, AlignmentGeometry? alignment, Duration? autoCloseDuration, + Duration? animationDuration, ToastificationAnimationBuilder? animationBuilder, ToastificationType? type, ToastificationStyle? style, Widget? title, - Duration? animationDuration, Widget? description, Widget? icon, Color? primaryColor, diff --git a/lib/src/core/toastification_callbacks.dart b/lib/src/core/toastification_callbacks.dart index 7a20c10e..add72c2d 100644 --- a/lib/src/core/toastification_callbacks.dart +++ b/lib/src/core/toastification_callbacks.dart @@ -1,3 +1,4 @@ +import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:toastification/toastification.dart'; @@ -5,7 +6,7 @@ import 'package:toastification/toastification.dart'; /// Used to listen for various events in the lifecycle of the [Toastification]. /// All of the callbacks are optional. /// -class ToastificationCallbacks { +class ToastificationCallbacks extends Equatable { /// Creates a set of callbacks that you can provide to a [Toastification] widget. const ToastificationCallbacks({ this.onTap, @@ -28,4 +29,12 @@ class ToastificationCallbacks { /// Called when the toast is dismissed by a user gesture (e.g. a swipe). final ValueChanged? onDismissed; + + @override + List get props => [ + onTap, + onCloseButtonTap, + onAutoCompleteCompleted, + onDismissed, + ]; } diff --git a/lib/src/core/toastification_config.dart b/lib/src/core/toastification_config.dart index 93944470..773f0c68 100644 --- a/lib/src/core/toastification_config.dart +++ b/lib/src/core/toastification_config.dart @@ -2,10 +2,17 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:toastification/toastification.dart'; -const _defaultAlignment = AlignmentDirectional.topEnd; -const _itemAnimationDuration = Duration(milliseconds: 600); -const _defaultWidth = 400.0; -const _defaultClipBehavior = Clip.none; +@visibleForTesting +const defaultAlignment = AlignmentDirectional.topEnd; + +@visibleForTesting +const defaultItemAnimationDuration = Duration(milliseconds: 600); + +@visibleForTesting +const defaultWidth = 400.0; + +@visibleForTesting +const defaultClipBehavior = Clip.none; typedef ToastificationMarginBuilder = EdgeInsetsGeometry Function( BuildContext context, @@ -24,12 +31,12 @@ typedef ToastificationMarginBuilder = EdgeInsetsGeometry Function( /// class ToastificationConfig extends Equatable { const ToastificationConfig({ - this.alignment = _defaultAlignment, - this.itemWidth = _defaultWidth, - this.clipBehavior = _defaultClipBehavior, - this.animationDuration = _itemAnimationDuration, - this.animationBuilder = _defaultAnimationBuilderConfig, - this.marginBuilder = _defaultMarginBuilder, + this.alignment = defaultAlignment, + this.itemWidth = defaultWidth, + this.clipBehavior = defaultClipBehavior, + this.animationDuration = defaultItemAnimationDuration, + this.animationBuilder = defaultAnimationBuilderConfig, + this.marginBuilder = defaultMarginBuilder, this.applyMediaQueryViewInsets = true, this.maxToastLimit = 10, }); @@ -68,6 +75,7 @@ class ToastificationConfig extends Equatable { Duration? animationDuration, ToastificationAnimationBuilder? animationBuilder, ToastificationMarginBuilder? marginBuilder, + int? maxToastLimit, bool? applyMediaQueryViewInsets, }) { return ToastificationConfig( @@ -77,6 +85,7 @@ class ToastificationConfig extends Equatable { animationDuration: animationDuration ?? this.animationDuration, animationBuilder: animationBuilder ?? this.animationBuilder, marginBuilder: marginBuilder ?? this.marginBuilder, + maxToastLimit: maxToastLimit ?? this.maxToastLimit, applyMediaQueryViewInsets: applyMediaQueryViewInsets ?? this.applyMediaQueryViewInsets, ); @@ -89,12 +98,14 @@ class ToastificationConfig extends Equatable { clipBehavior, animationDuration, marginBuilder, + maxToastLimit, applyMediaQueryViewInsets, ]; } /// Default animation builder for [Toastification] -Widget _defaultAnimationBuilderConfig( +@visibleForTesting +Widget defaultAnimationBuilderConfig( BuildContext context, Animation animation, Alignment alignment, @@ -108,7 +119,8 @@ Widget _defaultAnimationBuilderConfig( } /// Default margin builder for [Toastification] -EdgeInsetsGeometry _defaultMarginBuilder( +@visibleForTesting +EdgeInsetsGeometry defaultMarginBuilder( BuildContext context, AlignmentGeometry alignment, ) { diff --git a/lib/src/core/toastification_manager.dart b/lib/src/core/toastification_manager.dart index f23860fb..bb63e11a 100644 --- a/lib/src/core/toastification_manager.dart +++ b/lib/src/core/toastification_manager.dart @@ -14,6 +14,15 @@ class ToastificationManager { required this.config, }); + /// this is the delay for showing the overlay entry + /// We need this delay because we want to show the item animation after + /// the overlay created + /// + /// When we want to show first toast, we need to wait for the overlay to be created + /// and then show the toast item. + @visibleForTesting + static final kCreateOverlayDelay = const Duration(milliseconds: 100); + final Alignment alignment; final ToastificationConfig config; @@ -27,14 +36,6 @@ class ToastificationManager { /// if the list is empty, the overlay entry will be removed final List _notifications = []; - /// this is the delay for showing the overlay entry - /// We need this delay because we want to show the item animation after - /// the overlay created - /// - /// When we want to show first toast, we need to wait for the overlay to be created - /// and then show the toast item. - final _createOverlayDelay = const Duration(milliseconds: 100); - /// this is the delay for removing the overlay entry /// /// when we want to remove the last toast, we need to wait for the animation @@ -69,7 +70,7 @@ class ToastificationManager { if (_overlayEntry == null) { _createNotificationHolder(overlayState); - delay = _createOverlayDelay; + delay = kCreateOverlayDelay; } Future.delayed( diff --git a/test/src/built_in/layout/standard/style/factory_test.dart b/test/src/built_in/layout/standard/style/factory_test.dart new file mode 100644 index 00000000..6d72415d --- /dev/null +++ b/test/src/built_in/layout/standard/style/factory_test.dart @@ -0,0 +1,51 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:toastification/toastification.dart'; + +void main() { + group('StandardToastStyleFactory', () { + test('creates MinimalStandardToastStyle', () { + final style = StandardToastStyleFactory.createStyle( + style: StandardStyle.minimal, + type: ToastificationType.info, + ); + + expect(style, isA()); + }); + + test('creates FilledStandardToastStyle', () { + final style = StandardToastStyleFactory.createStyle( + style: StandardStyle.fillColored, + type: ToastificationType.success, + ); + + expect(style, isA()); + }); + + test('creates FlatStandardColoredToastStyle', () { + final style = StandardToastStyleFactory.createStyle( + style: StandardStyle.flatColored, + type: ToastificationType.warning, + ); + + expect(style, isA()); + }); + + test('creates FlatStandardToastStyle', () { + final style = StandardToastStyleFactory.createStyle( + style: StandardStyle.flat, + type: ToastificationType.error, + ); + + expect(style, isA()); + }); + + test('creates SimpleStandardToastStyle', () { + final style = StandardToastStyleFactory.createStyle( + style: StandardStyle.simple, + type: ToastificationType.info, + ); + + expect(style, isA()); + }); + }); +} diff --git a/test/src/built_in/layout/standard/style/style_test.dart b/test/src/built_in/layout/standard/style/style_test.dart new file mode 100644 index 00000000..b5ac0128 --- /dev/null +++ b/test/src/built_in/layout/standard/style/style_test.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:toastification/toastification.dart'; + +void main() { + group('StandardStyleValues', () { + test('should have correct properties', () { + const values = StandardStyleValues( + primaryColor: Colors.blue, + surfaceLight: Colors.white, + surfaceDark: Colors.black, + padding: EdgeInsets.all(8.0), + borderSide: BorderSide(color: Colors.red), + borderRadius: BorderRadius.all(Radius.circular(8.0)), + progressIndicatorStrokeWidth: 4.0, + ); + + expect(values.primaryColor, Colors.blue); + expect(values.surfaceLight, Colors.white); + expect(values.surfaceDark, Colors.black); + expect(values.padding, EdgeInsets.all(8.0)); + expect(values.borderSide, BorderSide(color: Colors.red)); + expect(values.borderRadius, BorderRadius.all(Radius.circular(8.0))); + expect(values.progressIndicatorStrokeWidth, 4.0); + }); + + test('should support value equality', () { + const values1 = StandardStyleValues( + primaryColor: Colors.blue, + surfaceLight: Colors.white, + surfaceDark: Colors.black, + ); + const values2 = StandardStyleValues( + primaryColor: Colors.blue, + surfaceLight: Colors.white, + surfaceDark: Colors.black, + ); + + expect(values1, values2); + }); + }); + + group('DefaultStyleValues', () { + test('should have correct default properties', () { + const values = DefaultStyleValues( + primaryColor: Colors.blue, + surfaceLight: Colors.white, + surfaceDark: Colors.black, + ); + + expect(values.primaryColor, Colors.blue); + expect(values.surfaceLight, Colors.white); + expect(values.surfaceDark, Colors.black); + expect(values.padding, EdgeInsetsDirectional.fromSTEB(20, 16, 12, 16)); + expect(values.borderSide, BorderSide(color: Colors.black12)); + expect(values.borderRadius, BorderRadius.all(Radius.circular(12))); + expect(values.progressIndicatorStrokeWidth, 2.0); + }); + }); + + group('BaseStandardToastStyle', () { + test('should have correct default properties', () { + final style = TestToastStyle( + type: ToastificationType.success, + providedValues: const StandardStyleValues( + primaryColor: Colors.blue, + surfaceLight: Colors.white, + surfaceDark: Colors.black, + ), + ); + + expect(style.primaryColor, Colors.blue); + expect(style.backgroundColor, Colors.white); + expect(style.foregroundColor, Colors.black); + expect(style.icon, ToastificationType.success.icon); + expect(style.closeIcon, Icons.close); + expect(style.closeIconColor, Colors.black.withAlpha(102)); + expect(style.padding, EdgeInsetsDirectional.fromSTEB(20, 16, 12, 16)); + expect(style.borderSide, BorderSide(color: Colors.black12)); + expect(style.borderRadius, BorderRadius.all(Radius.circular(12))); + expect(style.elevation, 0.0); + expect(style.boxShadow, []); + expect(style.progressIndicatorStrokeWidth, 2.0); + }); + }); +} + +class TestToastStyle extends BaseStandardToastStyle { + const TestToastStyle({ + required super.type, + super.providedValues, + super.flutterTheme, + }); + + @override + DefaultStyleValues get defaults => const DefaultStyleValues( + primaryColor: Colors.blue, + surfaceLight: Colors.white, + surfaceDark: Colors.black, + ); + + @override + Color get iconColor => Colors.red; +} diff --git a/test/src/built_in/layout/standard/toast/factory_test.dart b/test/src/built_in/layout/standard/toast/factory_test.dart new file mode 100644 index 00000000..92111ccf --- /dev/null +++ b/test/src/built_in/layout/standard/toast/factory_test.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:toastification/toastification.dart'; + +void main() { + group('StandardToastWidgetFactory', () { + group('Unit Tests', () { + test( + 'returns DefaultStandardToastWidget for StandardStyle.flat, flatColored, fillColored', + () { + // A helper function to reduce repeated logic + Widget createWidget(StandardStyle style) { + return StandardToastWidgetFactory.createStandardToastWidget( + style: style, + title: const Text('Title'), + description: const Text('Description'), + icon: const Icon(Icons.info), + onCloseTap: () {}, + ); + } + + final flatWidget = createWidget(StandardStyle.flat); + final flatColoredWidget = createWidget(StandardStyle.flatColored); + final fillColoredWidget = createWidget(StandardStyle.fillColored); + + // Verify each is DefaultStandardToastWidget + expect(flatWidget, isA()); + expect(flatColoredWidget, isA()); + expect(fillColoredWidget, isA()); + }); + + test('returns MinimalStandardToastWidget for StandardStyle.minimal', () { + final widget = StandardToastWidgetFactory.createStandardToastWidget( + style: StandardStyle.minimal, + title: const Text('Title'), + description: const Text('Description'), + icon: const Icon(Icons.info), + onCloseTap: () {}, + ); + + expect(widget, isA()); + }); + + test('returns SimpleStandardToastWidget for StandardStyle.simple', () { + final widget = StandardToastWidgetFactory.createStandardToastWidget( + style: StandardStyle.simple, + title: const Text('Title'), + onCloseTap: () {}, + ); + + expect(widget, isA()); + }); + + test('passes correct parameters to DefaultStandardToastWidget', () { + final widget = StandardToastWidgetFactory.createStandardToastWidget( + style: StandardStyle.flat, // Produces DefaultStandardToastWidget + title: const Text('My Title'), + description: const Text('My Description'), + icon: const Icon(Icons.warning), + onCloseTap: () {}, + showCloseButton: false, + closeButton: const ToastCloseButton( + showType: CloseButtonShowType.onHover, + ), + progressBarWidget: const LinearProgressIndicator(), + ); + + expect(widget, isA()); + + // Cast to DefaultStandardToastWidget to check fields + final defaultWidget = widget as DefaultStandardToastWidget; + expect(defaultWidget.title, isA()); + expect((defaultWidget.title! as Text).data, 'My Title'); + + expect(defaultWidget.description, isA()); + expect((defaultWidget.description! as Text).data, 'My Description'); + + expect(defaultWidget.icon, isA()); + expect(defaultWidget.showCloseButton, false); + + // We can’t directly test the callback logic with a pure unit test, + // but we can confirm it’s not null + expect(defaultWidget.onCloseTap, isNotNull); + + expect(defaultWidget.closeButton, isA()); + expect(defaultWidget.closeButton.showType, + equals(CloseButtonShowType.onHover)); + expect(defaultWidget.progressBarWidget, isA()); + }); + + test('passes correct parameters to MinimalStandardToastWidget', () { + final widget = StandardToastWidgetFactory.createStandardToastWidget( + style: StandardStyle.minimal, // Produces MinimalStandardToastWidget + title: const Text('My Title'), + description: const Text('My Description'), + icon: const Icon(Icons.warning), + onCloseTap: () {}, + showCloseButton: false, + closeButton: const ToastCloseButton( + showType: CloseButtonShowType.onHover, + ), + progressBarWidget: const LinearProgressIndicator(), + ); + + expect(widget, isA()); + + // Cast to MinimalStandardToastWidget to check fields + final minimalWidget = widget as MinimalStandardToastWidget; + expect(minimalWidget.title, isA()); + expect((minimalWidget.title! as Text).data, 'My Title'); + + expect(minimalWidget.description, isA()); + expect((minimalWidget.description! as Text).data, 'My Description'); + + expect(minimalWidget.icon, isA()); + expect(minimalWidget.showCloseButton, false); + + // We can’t directly test the callback logic with a pure unit test, + // but we can confirm it’s not null + expect(minimalWidget.onCloseTap, isNotNull); + + expect(minimalWidget.closeButton, isA()); + expect(minimalWidget.closeButton.showType, + equals(CloseButtonShowType.onHover)); + expect(minimalWidget.progressBarWidget, isA()); + }); + + test('respects default values when optional params are omitted', () { + final widget = StandardToastWidgetFactory.createStandardToastWidget( + style: StandardStyle.simple, + title: const Text('Title'), + onCloseTap: () {}, + ); + + // simple => SimpleStandardToastWidget + final simpleWidget = widget as SimpleStandardToastWidget; + + // By default: showCloseButton = true + expect(simpleWidget.showCloseButton, isTrue); + + // default closeButton is ToastCloseButton() + expect(simpleWidget.closeButton, isA()); + + // minimal or no description => in Simple widget, that’s normal + expect(simpleWidget.title, isA()); + }); + }); + }); +} diff --git a/test/src/built_in/theme/toastification_theme_data_test.dart b/test/src/built_in/theme/toastification_theme_data_test.dart new file mode 100644 index 00000000..665ada26 --- /dev/null +++ b/test/src/built_in/theme/toastification_theme_data_test.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:toastification/toastification.dart'; + +void main() { + late ThemeData theme; + late FlatStandardToastStyle flatStyle; + late MinimalStandardToastStyle minimalStyle; + + setUp(() { + theme = ThemeData.light(); + flatStyle = FlatStandardToastStyle( + type: ToastificationType.success, + flutterTheme: theme, + ); + minimalStyle = MinimalStandardToastStyle( + type: ToastificationType.success, + flutterTheme: theme, + ); + }); + + group('ToastificationThemeData', () { + group('initialization', () { + test('default values', () { + final themeData = ToastificationThemeData( + flutterTheme: theme, + direction: TextDirection.ltr, + ); + + expect(themeData.toastStyle, isNull); + expect(themeData.flutterTheme, equals(theme)); + expect(themeData.direction, equals(TextDirection.ltr)); + expect(themeData.showProgressBar, isFalse); + expect(themeData.applyBlurEffect, isFalse); + expect(themeData.showIcon, isTrue); + }); + + test('custom values', () { + final themeData = ToastificationThemeData( + toastStyle: flatStyle, + flutterTheme: theme, + direction: TextDirection.rtl, + showProgressBar: true, + applyBlurEffect: true, + showIcon: false, + ); + + expect(themeData.toastStyle, equals(flatStyle)); + expect(themeData.direction, equals(TextDirection.rtl)); + expect(themeData.showProgressBar, isTrue); + expect(themeData.applyBlurEffect, isTrue); + expect(themeData.showIcon, isFalse); + }); + }); + + group('copyWith', () { + test('no modifications', () { + final originalTheme = ToastificationThemeData( + flutterTheme: theme, + direction: TextDirection.ltr, + ); + + expect(originalTheme.copyWith(), equals(originalTheme)); + }); + + test('partial modifications', () { + final originalTheme = ToastificationThemeData( + flutterTheme: theme, + direction: TextDirection.ltr, + ); + + final modifiedTheme = originalTheme.copyWith( + toastStyle: flatStyle, + direction: TextDirection.rtl, + showProgressBar: true, + applyBlurEffect: true, + showIcon: false, + ); + + expect(modifiedTheme.toastStyle, equals(flatStyle)); + expect(modifiedTheme.direction, equals(TextDirection.rtl)); + expect(modifiedTheme.showProgressBar, isTrue); + expect(modifiedTheme.applyBlurEffect, isTrue); + expect(modifiedTheme.showIcon, isFalse); + }); + }); + + group('equality', () { + test('equal properties', () { + final instance1 = ToastificationThemeData( + toastStyle: flatStyle, + flutterTheme: theme, + direction: TextDirection.ltr, + ); + final instance2 = ToastificationThemeData( + toastStyle: flatStyle, + flutterTheme: theme, + direction: TextDirection.ltr, + ); + + expect(instance1, equals(instance2)); + expect(instance1.hashCode, equals(instance2.hashCode)); + }); + + test('different properties', () { + final baseTheme = ToastificationThemeData( + flutterTheme: theme, + direction: TextDirection.ltr, + ); + + final variations = [ + baseTheme.copyWith(direction: TextDirection.rtl), + baseTheme.copyWith(showProgressBar: true), + baseTheme.copyWith(applyBlurEffect: true), + baseTheme.copyWith(showIcon: false), + baseTheme.copyWith(toastStyle: flatStyle), + baseTheme.copyWith(toastStyle: minimalStyle), + ]; + + for (final variation in variations) { + expect(baseTheme, isNot(equals(variation))); + expect(variation, isNot(equals(baseTheme))); + } + }); + + test('different toast styles', () { + final baseTheme = ToastificationThemeData( + flutterTheme: theme, + direction: TextDirection.ltr, + ); + + final themeWithFlat = baseTheme.copyWith(toastStyle: flatStyle); + final themeWithMinimal = baseTheme.copyWith(toastStyle: minimalStyle); + + expect(themeWithFlat, isNot(equals(themeWithMinimal))); + }); + }); + }); +} diff --git a/test/src/built_in/theme/toastification_theme_test.dart b/test/src/built_in/theme/toastification_theme_test.dart new file mode 100644 index 00000000..2cbef0ee --- /dev/null +++ b/test/src/built_in/theme/toastification_theme_test.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:toastification/toastification.dart'; + +void main() { + late ThemeData flutterTheme; + late ToastificationThemeData themeData; + + setUp(() { + flutterTheme = ThemeData.light(); + themeData = ToastificationThemeData( + flutterTheme: flutterTheme, + direction: TextDirection.ltr, + ); + }); + + group('ToastificationTheme', () { + testWidgets('can be accessed using of() method', (tester) async { + late ToastificationThemeData retrievedTheme; + + await tester.pumpWidget( + ToastificationTheme( + themeData: themeData, + child: Builder( + builder: (context) { + retrievedTheme = ToastificationTheme.of(context); + return const SizedBox(); + }, + ), + ), + ); + + expect(retrievedTheme, equals(themeData)); + }); + + testWidgets('updates when theme data changes', (tester) async { + late ToastificationThemeData retrievedTheme; + + Widget buildWidget(ToastificationThemeData data) { + return ToastificationTheme( + themeData: data, + child: Builder( + builder: (context) { + retrievedTheme = ToastificationTheme.of(context); + return const SizedBox(); + }, + ), + ); + } + + await tester.pumpWidget(buildWidget(themeData)); + expect(retrievedTheme, equals(themeData)); + + final updatedTheme = themeData.copyWith( + direction: TextDirection.rtl, + showProgressBar: true, + ); + + await tester.pumpWidget(buildWidget(updatedTheme)); + expect(retrievedTheme, equals(updatedTheme)); + expect(retrievedTheme, isNot(equals(themeData))); + }); + + testWidgets('throws assertion error when no theme is found', + (tester) async { + await tester.pumpWidget( + Builder( + builder: (context) { + expect( + () => ToastificationTheme.of(context), + throwsAssertionError, + ); + return const SizedBox(); + }, + ), + ); + }); + + testWidgets('wrap creates new ToastificationTheme instance', + (tester) async { + late BuildContext testContext; + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + testContext = context; + return const SizedBox(); + }, + ), + ), + ); + + final theme = ToastificationTheme( + themeData: themeData, + child: const SizedBox(), + ); + + final wrapped = theme.wrap( + testContext, + const SizedBox(), + ); + + expect(wrapped, isA()); + expect((wrapped as ToastificationTheme).themeData, equals(themeData)); + }); + }); +} diff --git a/test/src/core/toastification_callbacks_test.dart b/test/src/core/toastification_callbacks_test.dart new file mode 100644 index 00000000..8a4adc30 --- /dev/null +++ b/test/src/core/toastification_callbacks_test.dart @@ -0,0 +1,121 @@ +// ignore_for_file: prefer_function_declarations_over_variables + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:toastification/toastification.dart'; + +void main() { + group('ToastificationCallbacks', () { + test('should create an instance with all callbacks', () { + final ValueChanged onTap = + (ToastificationItem item) {}; + final ValueChanged onCloseButtonTap = + (ToastificationItem item) {}; + final ValueChanged onAutoCompleteCompleted = + (ToastificationItem item) {}; + final ValueChanged onDismissed = + (ToastificationItem item) {}; + + final callbacks = ToastificationCallbacks( + onTap: onTap, + onCloseButtonTap: onCloseButtonTap, + onAutoCompleteCompleted: onAutoCompleteCompleted, + onDismissed: onDismissed, + ); + + expect(callbacks.onTap, equals(onTap)); + expect(callbacks.onCloseButtonTap, equals(onCloseButtonTap)); + expect( + callbacks.onAutoCompleteCompleted, equals(onAutoCompleteCompleted)); + expect(callbacks.onDismissed, equals(onDismissed)); + }); + + test('should create an instance with no callbacks', () { + final callbacks = const ToastificationCallbacks(); + + expect(callbacks.onTap, isNull); + expect(callbacks.onCloseButtonTap, isNull); + expect(callbacks.onAutoCompleteCompleted, isNull); + expect(callbacks.onDismissed, isNull); + }); + + test('props should contain all callbacks', () { + final ValueChanged onTap = + (ToastificationItem item) {}; + final ValueChanged onCloseButtonTap = + (ToastificationItem item) {}; + final ValueChanged onAutoCompleteCompleted = + (ToastificationItem item) {}; + final ValueChanged onDismissed = + (ToastificationItem item) {}; + + final callbacks = ToastificationCallbacks( + onTap: onTap, + onCloseButtonTap: onCloseButtonTap, + onAutoCompleteCompleted: onAutoCompleteCompleted, + onDismissed: onDismissed, + ); + + expect(callbacks.props, [ + onTap, + onCloseButtonTap, + onAutoCompleteCompleted, + onDismissed, + ]); + }); + + test('should be equal if all properties are equal', () { + final ValueChanged onTap = + (ToastificationItem item) {}; + final ValueChanged onCloseButtonTap = + (ToastificationItem item) {}; + final ValueChanged onAutoCompleteCompleted = + (ToastificationItem item) {}; + final ValueChanged onDismissed = + (ToastificationItem item) {}; + + final callbacks1 = ToastificationCallbacks( + onTap: onTap, + onCloseButtonTap: onCloseButtonTap, + onAutoCompleteCompleted: onAutoCompleteCompleted, + onDismissed: onDismissed, + ); + + final callbacks2 = ToastificationCallbacks( + onTap: onTap, + onCloseButtonTap: onCloseButtonTap, + onAutoCompleteCompleted: onAutoCompleteCompleted, + onDismissed: onDismissed, + ); + + expect(callbacks1, equals(callbacks2)); + }); + + test('should have the same hashcode if all properties are equal', () { + final ValueChanged onTap = + (ToastificationItem item) {}; + final ValueChanged onCloseButtonTap = + (ToastificationItem item) {}; + final ValueChanged onAutoCompleteCompleted = + (ToastificationItem item) {}; + final ValueChanged onDismissed = + (ToastificationItem item) {}; + + final callbacks1 = ToastificationCallbacks( + onTap: onTap, + onCloseButtonTap: onCloseButtonTap, + onAutoCompleteCompleted: onAutoCompleteCompleted, + onDismissed: onDismissed, + ); + + final callbacks2 = ToastificationCallbacks( + onTap: onTap, + onCloseButtonTap: onCloseButtonTap, + onAutoCompleteCompleted: onAutoCompleteCompleted, + onDismissed: onDismissed, + ); + + expect(callbacks1.hashCode, equals(callbacks2.hashCode)); + }); + }); +} diff --git a/test/src/core/toastification_config_test.dart b/test/src/core/toastification_config_test.dart new file mode 100644 index 00000000..7fc88bc2 --- /dev/null +++ b/test/src/core/toastification_config_test.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:toastification/toastification.dart'; + +void main() { + group('ToastificationConfig', () { + test('default values', () { + const config = ToastificationConfig(); + expect(config.alignment, defaultAlignment); + expect(config.itemWidth, defaultWidth); + expect(config.clipBehavior, defaultClipBehavior); + expect(config.animationDuration, defaultItemAnimationDuration); + expect(config.marginBuilder, defaultMarginBuilder); + expect(config.maxToastLimit, 10); + expect(config.applyMediaQueryViewInsets, true); + }); + + test('custom constructor', () { + final config = ToastificationConfig( + alignment: Alignment.bottomCenter, + itemWidth: 500, + clipBehavior: Clip.antiAlias, + animationDuration: Duration(seconds: 1), + animationBuilder: (context, animation, alignment, child) => child, + marginBuilder: (context, alignment) => EdgeInsets.zero, + maxToastLimit: 5, + applyMediaQueryViewInsets: false, + ); + + expect(config.alignment, Alignment.bottomCenter); + expect(config.itemWidth, 500); + expect(config.clipBehavior, Clip.antiAlias); + expect(config.animationDuration, Duration(seconds: 1)); + expect(config.marginBuilder, isNotNull); + expect(config.maxToastLimit, 5); + expect(config.applyMediaQueryViewInsets, false); + }); + + test('copyWith updates properties', () { + const config = ToastificationConfig(); + final updated = config.copyWith( + alignment: Alignment.bottomCenter, + itemWidth: 300, + clipBehavior: Clip.antiAlias, + animationBuilder: (context, animation, alignment, child) => child, + marginBuilder: (context, alignment) => EdgeInsets.zero, + animationDuration: Duration(seconds: 1), + maxToastLimit: 5, + applyMediaQueryViewInsets: false, + ); + + expect(updated.itemWidth, 300); + expect(updated.maxToastLimit, 5); + expect(updated.applyMediaQueryViewInsets, false); + expect(updated.alignment, Alignment.bottomCenter); + expect(updated.clipBehavior, Clip.antiAlias); + expect(updated.animationDuration, Duration(seconds: 1)); + expect(updated.marginBuilder, isNotNull); + }); + + test('equals | returns true for unchanged copyWith check', () { + const config = ToastificationConfig(); + final updated = config.copyWith(); + expect(config, updated); + }); + + test('equals | returns false for changed copyWith check', () { + const config = ToastificationConfig(); + final updated = config.copyWith( + alignment: Alignment.bottomCenter, + itemWidth: 300, + clipBehavior: Clip.antiAlias, + animationBuilder: (context, animation, alignment, child) => child, + marginBuilder: (context, alignment) => EdgeInsets.zero, + animationDuration: Duration(seconds: 1), + maxToastLimit: 5, + applyMediaQueryViewInsets: false, + ); + + expect(config == updated, false); + expect(config, isNot(equals(updated))); + }); + + test('hashCode', () { + const config = ToastificationConfig(); + final updated = config.copyWith( + alignment: Alignment.bottomCenter, + itemWidth: 300, + clipBehavior: Clip.antiAlias, + animationBuilder: (context, animation, alignment, child) => child, + marginBuilder: (context, alignment) => EdgeInsets.zero, + animationDuration: Duration(seconds: 1), + maxToastLimit: 5, + applyMediaQueryViewInsets: false, + ); + + expect(config.hashCode, isNot(updated.hashCode)); + }); + }); +} diff --git a/test/src/core/toastification_item_test.dart b/test/src/core/toastification_item_test.dart new file mode 100644 index 00000000..aa223051 --- /dev/null +++ b/test/src/core/toastification_item_test.dart @@ -0,0 +1,194 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:toastification/toastification.dart'; + +void main() { + late ToastificationItem toastification; + + Widget mockBuilder(BuildContext context, ToastificationItem holder) { + return const SizedBox(); + } + + group('ToastificationItem Creation', () { + test('creates with required parameters only', () { + toastification = ToastificationItem( + builder: mockBuilder, + alignment: Alignment.topCenter, + ); + + expect(toastification.id, isNotEmpty); + expect(toastification.alignment, Alignment.topCenter); + expect(toastification.autoCloseDuration, isNull); + expect(toastification.hasTimer, isFalse); + expect(toastification.timeStatus, ToastTimeStatus.init); + }); + + test('creates with auto close duration', () { + toastification = ToastificationItem( + builder: mockBuilder, + alignment: Alignment.topCenter, + autoCloseDuration: const Duration(seconds: 2), + ); + + expect(toastification.hasTimer, isTrue); + expect(toastification.timeStatus, ToastTimeStatus.started); + expect(toastification.originalDuration, const Duration(seconds: 2)); + }); + + test('generates unique IDs for each instance', () { + final uniqueIds = {}; + const instanceCount = 10; + + // Create multiple instances and collect their IDs + for (var i = 0; i < instanceCount; i++) { + final toast = ToastificationItem( + builder: mockBuilder, + alignment: Alignment.topCenter, + ); + uniqueIds.add(toast.id); + } + + // If all IDs were unique, the Set size should equal the number of instances + expect(uniqueIds.length, equals(instanceCount)); + }); + }); + + group('Timer Controls', () { + setUp(() { + toastification = ToastificationItem( + builder: mockBuilder, + alignment: Alignment.topCenter, + autoCloseDuration: const Duration(seconds: 2), + ); + }); + + test('pause() changes status to paused', () { + expect(toastification.timeStatus, ToastTimeStatus.started); + toastification.pause(); + expect(toastification.timeStatus, ToastTimeStatus.paused); + expect(toastification.isRunning, isTrue); + }); + + test('start() changes status to started', () { + toastification.pause(); + expect(toastification.timeStatus, ToastTimeStatus.paused); + toastification.start(); + expect(toastification.timeStatus, ToastTimeStatus.started); + expect(toastification.isRunning, isTrue); + }); + + test('stop() changes status to stopped', () { + expect(toastification.timeStatus, ToastTimeStatus.started); + toastification.stop(); + expect(toastification.timeStatus, ToastTimeStatus.stopped); + expect(toastification.isRunning, isFalse); + }); + }); + + group('Timer Status Listener', () { + late ToastificationItem toastification; + late int listenerCallCount; + + setUp(() { + listenerCallCount = 0; + toastification = ToastificationItem( + builder: mockBuilder, + alignment: Alignment.topCenter, + autoCloseDuration: const Duration(seconds: 2), + ); + }); + + test('listener is called when status changes', () { + void listener() { + listenerCallCount++; + } + + toastification.addListenerOnTimeStatus(listener); + + toastification.pause(); + expect(listenerCallCount, 1); + + toastification.start(); + expect(listenerCallCount, 2); + + toastification.stop(); + expect(listenerCallCount, 3); + + toastification.removeListenerOnTimeStatus(listener); + }); + }); + + group('Auto Complete Callback', () { + test('onAutoCompleteCompleted is called when timer finishes', () async { + bool callbackCalled = false; + + toastification = ToastificationItem( + builder: mockBuilder, + alignment: Alignment.topCenter, + autoCloseDuration: const Duration(milliseconds: 100), + onAutoCompleteCompleted: (_) { + callbackCalled = true; + }, + ); + + await Future.delayed(const Duration(milliseconds: 150)); + + expect(callbackCalled, isTrue); + expect(toastification.timeStatus, ToastTimeStatus.finished); + }); + }); + + group('Equatable Implementation', () { + test('two toastifications with different IDs are not equal', () { + final toast1 = ToastificationItem( + builder: mockBuilder, + alignment: Alignment.topCenter, + ); + + final toast2 = ToastificationItem( + builder: mockBuilder, + alignment: Alignment.topCenter, + ); + + expect(toast1 == toast2, isFalse); + }); + }); + + group('Edge Cases', () { + test('timer controls do nothing when no autoCloseDuration', () { + toastification = ToastificationItem( + builder: mockBuilder, + alignment: Alignment.topCenter, + ); + + expect(toastification.hasTimer, isFalse); + + // These should not throw errors + toastification.start(); + expect(toastification.timeStatus, ToastTimeStatus.init); + + toastification.pause(); + expect(toastification.timeStatus, ToastTimeStatus.init); + + toastification.stop(); + expect(toastification.timeStatus, ToastTimeStatus.init); + }); + + test('toString returns correct format', () { + toastification = ToastificationItem( + builder: mockBuilder, + alignment: Alignment.topCenter, + autoCloseDuration: const Duration(seconds: 2), + ); + + expect( + toastification.toString(), + contains('id: ${toastification.id}'), + ); + expect( + toastification.toString(), + contains('timerDuration: ${toastification.originalDuration}'), + ); + }); + }); +} diff --git a/test/src/core/toastification_overlay_state_test.dart b/test/src/core/toastification_overlay_state_test.dart new file mode 100644 index 00000000..9db32141 --- /dev/null +++ b/test/src/core/toastification_overlay_state_test.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:toastification/src/core/toastification_overlay_state.dart'; +import 'package:toastification/toastification.dart'; + +void main() { + group('ToastificationOverlayState', () { + testWidgets('ToastificationWrapper initializes correctly', + (WidgetTester tester) async { + await tester.pumpWidget( + ToastificationWrapper( + child: MaterialApp( + home: Scaffold( + body: Container(), + ), + ), + ), + ); + + expect(find.byType(ToastificationWrapper), findsOneWidget); + expect(find.byType(MaterialApp), findsOneWidget); + }); + + testWidgets('ToastificationWrapper with custom config', + (WidgetTester tester) async { + final config = ToastificationConfig( + alignment: Alignment.topLeft, + animationDuration: const Duration(milliseconds: 300), + ); + + late final ToastificationConfig providedConfig; + + await tester.pumpWidget( + ToastificationWrapper( + config: config, + child: MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + providedConfig = + ToastificationConfigProvider.of(context).config; + + return Container(); + }, + ), + ), + ), + ), + ); + + expect(providedConfig, isNotNull); + expect(providedConfig, equals(config)); + expect(providedConfig.alignment, equals(Alignment.topLeft)); + expect( + providedConfig.animationDuration, + equals(const Duration(milliseconds: 300)), + ); + }); + + testWidgets('throws error when no Navigator is found', + (WidgetTester tester) async { + await tester.pumpWidget( + ToastificationWrapper( + child: Container(), + ), + ); + + expect( + () => findToastificationOverlayState().overlayState, + throwsAssertionError, + ); + }); + + testWidgets('findToastificationOverlayState throws when not initialized', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Container(), + ), + ); + + expect( + () => findToastificationOverlayState(), + throwsAssertionError, + ); + }); + + testWidgets('overlayState is accessible when properly initialized', + (WidgetTester tester) async { + await tester.pumpWidget( + ToastificationWrapper( + child: MaterialApp( + home: Scaffold( + body: Container(), + ), + ), + ), + ); + + final overlayState = findToastificationOverlayState().overlayState; + + expect(overlayState, isNotNull); + }); + + testWidgets('global config is accessible', (WidgetTester tester) async { + final testConfig = ToastificationConfig( + alignment: Alignment.bottomCenter, + animationDuration: const Duration(seconds: 1), + ); + + await tester.pumpWidget( + ToastificationWrapper( + config: testConfig, + child: MaterialApp( + home: Scaffold( + body: Container(), + ), + ), + ), + ); + + final globalConfig = findToastificationOverlayState().globalConfig; + expect(globalConfig, isNotNull); + expect(globalConfig?.alignment, equals(Alignment.bottomCenter)); + expect( + globalConfig?.animationDuration, equals(const Duration(seconds: 1))); + }); + }); +} diff --git a/test/src/core/toastification_test.dart b/test/src/core/toastification_test.dart new file mode 100644 index 00000000..9ad83341 --- /dev/null +++ b/test/src/core/toastification_test.dart @@ -0,0 +1,719 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:toastification/src/built_in/built_in_builder.dart'; +import 'package:toastification/src/core/toastification_manager.dart'; +import 'package:toastification/toastification.dart'; + +/// Helper function to clean up any remaining toasts after each test +/// +/// This ensures that toasts from one test don't interfere with another test. +/// The function: +/// 1. Dismisses all active toasts +/// 2. Waits for dismissal animations +/// 3. Ensures widget tree is stable +Future cleanUpToasts(WidgetTester tester) async { + toastification.dismissAll(delayForAnimation: false); + await tester.pump(const Duration(milliseconds: 500)); + await tester.pumpAndSettle(); +} + +void main() { + /// Tests for the basic [show] method functionality + /// These tests verify the core features of displaying built-in toasts + group('Toastification - show()', () { + /// Verifies that a basic toast appears with correct title and description + /// This is the most fundamental test case for the show() method + testWidgets( + 'should display toast with title and description when show is called', + (tester) async { + const kShowBasicToastButton = Key('show_basic_toast'); + const kBasicTitle = 'Basic Title'; + const kBasicDescription = 'Basic Description'; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) => Center( + child: Column( + children: [ + ElevatedButton( + key: kShowBasicToastButton, + onPressed: () { + toastification.show( + context: context, + type: ToastificationType.success, + style: ToastificationStyle.flat, + title: const Text(kBasicTitle), + description: const Text(kBasicDescription), + animationDuration: const Duration(milliseconds: 100), + ); + }, + child: const Text('Show Basic Toast'), + ), + ], + ), + ), + ), + ), + ), + ); + + // Verify that the toast is not displayed initially + expect(find.text(kBasicTitle), findsNothing); + + // Tap the button to show the toast + await tester.tap(find.byKey(kShowBasicToastButton)); + + // Using pump to rebuild and start animations we have + await tester.pump(); + + // Using pumpAndSettle to wait for the animation to complete + await tester.pumpAndSettle(); + + // Verify that the toast is displayed with the correct title and description + expect(find.text(kBasicTitle), findsOneWidget); + expect(find.text(kBasicDescription), findsOneWidget); + + // Clean up the toast after the test + await cleanUpToasts(tester); + }); + + /// Verifies that toast cleanup works correctly + /// This ensures toasts can be properly removed from the widget tree + testWidgets('should remove toast from widget tree when dismissed', + (tester) async { + const kShowBasicToastButton = Key('show_basic_toast'); + const kBasicTitle = 'Basic Title'; + const kBasicDescription = 'Basic Description'; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) => Center( + child: Column( + children: [ + ElevatedButton( + key: kShowBasicToastButton, + onPressed: () { + toastification.show( + context: context, + style: ToastificationStyle.flat, + title: const Text(kBasicTitle), + description: const Text(kBasicDescription), + type: ToastificationType.success, + animationDuration: const Duration(milliseconds: 100), + ); + }, + child: const Text('Show Basic Toast'), + ), + ], + ), + ), + ), + ), + ), + ); + + expect(find.text(kBasicTitle), findsNothing); + + await tester.tap(find.byKey(kShowBasicToastButton)); + + await tester.pump(); + await tester.pumpAndSettle(); + + expect(find.byType(BuiltInBuilder), findsOneWidget); + + await cleanUpToasts(tester); + + expect(find.byType(BuiltInBuilder), findsNothing); + }); + + /// Verifies automatic closure functionality + /// Tests that toasts respect their autoCloseDuration parameter + testWidgets( + 'should automatically remove toast from widget tree after autoCloseDuration', + (tester) async { + const kAutoCloseTitle = 'AutoClose Title'; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) => Center( + child: ElevatedButton( + key: const Key('show_auto_close'), + onPressed: () { + toastification.show( + context: context, + title: const Text(kAutoCloseTitle), + showProgressBar: false, + autoCloseDuration: const Duration(milliseconds: 1000), + animationDuration: const Duration(milliseconds: 200), + ); + }, + child: const Text('Show AutoClose Toast'), + ), + ), + ), + ), + ), + ); + + await tester.tap(find.byKey(const Key('show_auto_close'))); + + await tester.pump(); + await tester.pumpAndSettle(); + + // Verify that the toast is displayed + expect(find.text(kAutoCloseTitle), findsOneWidget); + + // Wait for the autoCloseDuration to pass and start close animation + await tester.pump(const Duration(milliseconds: 1200)); + + // Wait for the close animation to complete + await tester.pumpAndSettle(); + + // Verify that the toast is removed from the widget tree + expect(find.text(kAutoCloseTitle), findsNothing); + }); + + /// Verifies progress bar functionality + /// Tests that the progress indicator appears when requested + testWidgets('should display progress bar when showProgressBar is true', + (tester) async { + const kShowProgressBarButton = Key('show_progress_bar'); + const kProgressBarTitle = 'Progress Bar Toast'; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) => Center( + child: ElevatedButton( + key: kShowProgressBarButton, + onPressed: () { + toastification.show( + context: context, + showProgressBar: true, + title: const Text(kProgressBarTitle), + ); + }, + child: const Text('Show Progress Toast'), + ), + ), + ), + ), + ), + ); + + await tester.tap(find.byKey(kShowProgressBarButton)); + + await tester.pump(); + await tester.pumpAndSettle(); + + expect(find.text(kProgressBarTitle), findsOneWidget); + + // Verify that the progress indicator is displayed + expect(find.byType(LinearProgressIndicator), findsOneWidget); + + await cleanUpToasts(tester); + }); + }); + + /// Tests for the [showCustom] method functionality + /// These tests verify the ability to display custom toast widgets + group('Toastification - showCustom()', () { + /// Verifies that custom widgets can be displayed as toasts + /// Tests the basic custom builder functionality + testWidgets('should display custom widget when showCustom is called', + (tester) async { + const kShowCustomToastButton = Key('show_custom_toast'); + const kCustomToastContainer = Key('custom_toast_container'); + const kCustomToastText = 'Custom Toast'; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) => Center( + child: ElevatedButton( + key: kShowCustomToastButton, + onPressed: () { + toastification.showCustom( + context: context, + builder: (context, toast) => Container( + key: kCustomToastContainer, + padding: const EdgeInsets.all(16), + color: Colors.blue, + child: const Text(kCustomToastText), + ), + ); + }, + child: const Text('Show Custom Toast'), + ), + ), + ), + ), + ), + ); + + expect(find.byKey(kCustomToastContainer), findsNothing); + + await tester.tap(find.byKey(kShowCustomToastButton)); + + await tester.pump(); + await tester.pumpAndSettle(); + + expect(find.byKey(kCustomToastContainer), findsOneWidget); + expect(find.text(kCustomToastText), findsOneWidget); + + await cleanUpToasts(tester); + }); + + /// Verifies alignment positioning system + /// Tests that toasts appear in the specified screen position + testWidgets('should respect custom alignment when specified', + (tester) async { + const kShowCustomToastButton = Key('show_custom_toast'); + const kCustomToastContainer = Key('custom_toast_container'); + const kCustomToastText = 'Custom Toast'; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) => Center( + child: ElevatedButton( + key: kShowCustomToastButton, + onPressed: () { + toastification.showCustom( + context: context, + alignment: Alignment.bottomCenter, + builder: (context, toast) => Container( + key: kCustomToastContainer, + child: const Text(kCustomToastText), + ), + ); + }, + child: const Text('Show Custom Toast'), + ), + ), + ), + ), + ), + ); + + await tester.tap(find.byKey(kShowCustomToastButton)); + await tester.pump(); + await tester.pumpAndSettle(); + + final containerFinder = find.byKey(kCustomToastContainer); + expect(containerFinder, findsOneWidget); + + final renderBox = tester.renderObject(containerFinder) as RenderBox; + final position = renderBox.localToGlobal(Offset.zero); + expect(position.dy > tester.getCenter(find.byType(Scaffold)).dy, true); + + await cleanUpToasts(tester); + }); + + /// Verifies automatic closure for custom toasts + /// Tests that custom toasts respect autoCloseDuration + testWidgets('should auto-close with custom duration', (tester) async { + const kAutoCloseToast = Key('auto_close_toast'); + const kAutoCloseText = 'Auto Close Toast'; + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) => ElevatedButton( + onPressed: () { + toastification.showCustom( + context: context, + autoCloseDuration: const Duration(milliseconds: 1000), + animationDuration: const Duration(milliseconds: 200), + builder: (context, toast) => Container( + key: kAutoCloseToast, + child: const Text(kAutoCloseText), + ), + ); + }, + child: const Text('Show Toast'), + ), + ), + ), + ); + + await tester.tap(find.byType(ElevatedButton)); + + await tester.pump(); + await tester.pumpAndSettle(); + + expect(find.byKey(kAutoCloseToast), findsOneWidget); + + await tester.pump(const Duration(milliseconds: 1200)); + await tester.pumpAndSettle(); + + expect(find.byKey(kAutoCloseToast), findsNothing); + }); + }); + + group('Toastification - dismiss()', () { + testWidgets('should remove toast from overlay when dismiss method called', + (tester) async { + late ToastificationItem toastItem; + + const kShowBasicToastButton = Key('show_basic_toast'); + const kBasicTitle = 'Basic Title'; + const kBasicDescription = 'Basic Description'; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) => Center( + child: Column( + children: [ + ElevatedButton( + key: kShowBasicToastButton, + onPressed: () { + toastItem = toastification.show( + context: context, + type: ToastificationType.success, + style: ToastificationStyle.flat, + title: const Text(kBasicTitle), + description: const Text(kBasicDescription), + animationDuration: const Duration(milliseconds: 100), + ); + }, + child: const Text('Show Basic Toast'), + ), + ], + ), + ), + ), + ), + ), + ); + + // Verify that the toast is not displayed initially + expect(find.text(kBasicTitle), findsNothing); + + // Tap the button to show the toast + await tester.tap(find.byKey(kShowBasicToastButton)); + + expect(toastItem, isNotNull); + log('ToastItem: $toastItem'); + + // Using pump to rebuild and start animations we have + await tester.pump(); + + // Using pumpAndSettle to wait for the animation to complete + await tester.pumpAndSettle(); + + // Verify that the toast is displayed with the correct title and description + expect(find.text(kBasicTitle), findsOneWidget); + expect(find.text(kBasicDescription), findsOneWidget); + + toastification.dismiss(toastItem); + + // Using pump to rebuild and start close animations + await tester.pump(); + await tester.pumpAndSettle(); + + // Verify that the toast is removed from the widget tree + expect(find.text(kBasicTitle), findsNothing); + }); + + testWidgets('should remove toast when dismissById is called', + (tester) async { + late ToastificationItem toastItem; + const kBasicTitle = 'Basic Title'; + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) => ElevatedButton( + onPressed: () { + toastItem = toastification.show( + context: context, + title: const Text(kBasicTitle), + ); + }, + child: const Text('Show Toast'), + ), + ), + ), + ); + + await tester.tap(find.byType(ElevatedButton)); + await tester.pump(); + await tester.pumpAndSettle(); + + expect(find.text(kBasicTitle), findsOneWidget); + + toastification.dismissById(toastItem.id); + + await tester.pump(); + await tester.pumpAndSettle(); + + expect(find.text(kBasicTitle), findsNothing); + }); + + testWidgets('should remove all toasts when dismissAll is called', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) => Column( + children: [ + ElevatedButton( + onPressed: () => toastification.show( + context: context, + title: const Text('Toast 1'), + ), + child: const Text('Show Toast 1'), + ), + ElevatedButton( + onPressed: () => toastification.show( + context: context, + title: const Text('Toast 2'), + ), + child: const Text('Show Toast 2'), + ), + ], + ), + ), + ), + ); + + // Show both toasts + await tester.tap(find.text('Show Toast 1')); + await tester.pump(); + await tester.tap(find.text('Show Toast 2')); + await tester.pump(); + await tester.pumpAndSettle(); + + // Verify both toasts are visible + expect(find.text('Toast 1'), findsOneWidget); + expect(find.text('Toast 2'), findsOneWidget); + + // Dismiss all toasts + toastification.dismissAll(delayForAnimation: false); + await tester.pump(); + await tester.pumpAndSettle(); + + // Verify all toasts are removed + expect(find.text('Toast 1'), findsNothing); + expect(find.text('Toast 2'), findsNothing); + }); + + testWidgets('should handle dismissing non-existent toast gracefully', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) => Container(), + ), + ), + ); + + // Should not throw when dismissing non-existent toast + expect( + () => toastification.dismissById('non-existent-id'), + returnsNormally, + ); + }); + + testWidgets('should not affect other toasts when dismissing specific toast', + (tester) async { + late ToastificationItem toast1; + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) => Column( + children: [ + ElevatedButton( + onPressed: () { + toast1 = toastification.show( + context: context, + title: const Text('Toast 1'), + ); + }, + child: const Text('Show Toast 1'), + ), + ElevatedButton( + onPressed: () => toastification.show( + context: context, + title: const Text('Toast 2'), + ), + child: const Text('Show Toast 2'), + ), + ], + ), + ), + ), + ); + + // Show both toasts + await tester.tap(find.text('Show Toast 1')); + await tester.pump(); + await tester.tap(find.text('Show Toast 2')); + await tester.pump(); + await tester.pumpAndSettle(); + + // Dismiss only first toast + toastification.dismiss(toast1); + await tester.pump(); + await tester.pumpAndSettle(); + + // Verify only first toast is removed + expect(find.text('Toast 1'), findsNothing); + expect(find.text('Toast 2'), findsOneWidget); + + await cleanUpToasts(tester); + }); + }); + + group('Toastification - findToastificationItem()', () { + testWidgets('should find existing toast by id', (tester) async { + late ToastificationItem originalToast; + const kBasicTitle = 'Basic Title'; + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) => ElevatedButton( + onPressed: () { + originalToast = toastification.show( + context: context, + title: const Text(kBasicTitle), + ); + }, + child: const Text('Show Toast'), + ), + ), + ), + ); + + await tester.tap(find.byType(ElevatedButton)); + await tester.pump(); + await tester.pumpAndSettle(); + + final foundToast = + toastification.findToastificationItem(originalToast.id); + expect(foundToast, isNotNull); + expect(foundToast?.id, equals(originalToast.id)); + + await cleanUpToasts(tester); + }); + + testWidgets('should return null for non-existent toast id', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Container(), + ), + ); + + final foundToast = + toastification.findToastificationItem('non-existent-id'); + expect(foundToast, isNull); + }); + + testWidgets('should find correct toast when multiple toasts exist', + (tester) async { + late ToastificationItem toast1; + late ToastificationItem toast2; + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) => Column( + children: [ + ElevatedButton( + onPressed: () { + toast1 = toastification.show( + context: context, + title: const Text('Toast 1'), + ); + }, + child: const Text('Show Toast 1'), + ), + ElevatedButton( + onPressed: () { + toast2 = toastification.show( + context: context, + title: const Text('Toast 2'), + ); + }, + child: const Text('Show Toast 2'), + ), + ], + ), + ), + ), + ); + + // Show both toasts + await tester.tap(find.text('Show Toast 1')); + await tester.pump(); + await tester.tap(find.text('Show Toast 2')); + await tester.pump(); + await tester.pumpAndSettle(); + + // Find first toast + final foundToast1 = toastification.findToastificationItem(toast1.id); + expect(foundToast1?.id, equals(toast1.id)); + + // Find second toast + final foundToast2 = toastification.findToastificationItem(toast2.id); + expect(foundToast2?.id, equals(toast2.id)); + + // Verify they're different + expect(foundToast1?.id, isNot(equals(foundToast2?.id))); + + await cleanUpToasts(tester); + }); + + testWidgets('should still find toast before animation completes', + (tester) async { + late ToastificationItem toast; + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) => ElevatedButton( + onPressed: () { + toast = toastification.show( + context: context, + title: const Text('Toast'), + animationDuration: const Duration(milliseconds: 500), + ); + }, + child: const Text('Show Toast'), + ), + ), + ), + ); + + await tester.tap(find.byType(ElevatedButton)); + // This waiting is for the OverlayEntry to be added to the tree + // we don't wait for the animation to completed + await tester.pump(ToastificationManager.kCreateOverlayDelay); + // Don't wait for animation to complete + + final foundToast = toastification.findToastificationItem(toast.id); + expect(foundToast, isNotNull); + expect(foundToast?.id, equals(toast.id)); + + await tester.pumpAndSettle(); // Clean up + + await cleanUpToasts(tester); + }); + }); +} diff --git a/test/src/core/widget/toastification_config_provider_test.dart b/test/src/core/widget/toastification_config_provider_test.dart new file mode 100644 index 00000000..54e7703f --- /dev/null +++ b/test/src/core/widget/toastification_config_provider_test.dart @@ -0,0 +1,79 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:toastification/src/core/toastification_config.dart'; +import 'package:toastification/src/core/widget/toastification_config_provider.dart'; + +void main() { + group('ToastificationConfigProvider', () { + final config = ToastificationConfig(); + final differentConfig = ToastificationConfig( + animationDuration: const Duration(seconds: 1), + alignment: Alignment.topCenter, + ); + + testWidgets('provides config to widget tree', (tester) async { + ToastificationConfig? result; + + await tester.pumpWidget( + ToastificationConfigProvider( + config: config, + child: Builder( + builder: (context) { + result = ToastificationConfigProvider.of(context).config; + return const SizedBox(); + }, + ), + ), + ); + + expect(result, equals(config)); + }); + + testWidgets('maybeOf returns null when no provider exists', (tester) async { + ToastificationConfigProvider? result; + + await tester.pumpWidget( + Builder( + builder: (context) { + result = ToastificationConfigProvider.maybeOf(context); + return const SizedBox(); + }, + ), + ); + + expect(result, isNull); + }); + + testWidgets('of throws assertion error when no provider exists', + (tester) async { + await tester.pumpWidget( + Builder( + builder: (context) { + expect( + () => ToastificationConfigProvider.of(context), + throwsAssertionError, + ); + return const SizedBox(); + }, + ), + ); + }); + + testWidgets('updateShouldNotify returns true when config changes', + (tester) async { + final provider = ToastificationConfigProvider( + config: config, + child: const SizedBox(), + ); + + final shouldUpdate = provider.updateShouldNotify( + ToastificationConfigProvider( + config: differentConfig, + child: const SizedBox(), + ), + ); + + expect(shouldUpdate, isTrue); + }); + }); +} diff --git a/test/src/utils/common_utils_test.dart b/test/src/utils/common_utils_test.dart new file mode 100644 index 00000000..c0e657d2 --- /dev/null +++ b/test/src/utils/common_utils_test.dart @@ -0,0 +1,54 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:toastification/src/utils/common_utils.dart'; + +void main() { + group('CommonUtils', () { + group('convertRange', () { + test('should convert value from one range to another correctly', () { + // Test case 1: Convert 50 from range 0-100 to range 0-1 + expect( + CommonUtils.convertRange(0, 100, 0, 1, 50), + equals(0.5), + ); + + // Test case 2: Convert 75 from range 0-100 to range 0-1 + expect( + CommonUtils.convertRange(0, 100, 0, 1, 75), + equals(0.75), + ); + + // Test case 3: Convert value with negative ranges + expect( + CommonUtils.convertRange(-100, 100, -1, 1, 0), + equals(0.0), + ); + + // Test case 4: Convert value with decimal input + expect( + CommonUtils.convertRange(0, 10, 0, 100, 5.5), + equals(55.0), + ); + }); + + test('should handle edge cases', () { + // Test minimum value + expect( + CommonUtils.convertRange(0, 100, 0, 1, 0), + equals(0.0), + ); + + // Test maximum value + expect( + CommonUtils.convertRange(0, 100, 0, 1, 100), + equals(1.0), + ); + + // Test value outside range (should still work based on the formula) + expect( + CommonUtils.convertRange(0, 100, 0, 1, 150), + equals(1.5), + ); + }); + }); + }); +} diff --git a/test/toastification_test.dart b/test/toastification_test.dart deleted file mode 100644 index 0da434d9..00000000 --- a/test/toastification_test.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -void main() { - test('adds one to input values', () {}); -} diff --git a/test/toastification_type_test.dart b/test/toastification_type_test.dart new file mode 100644 index 00000000..71e1ddcd --- /dev/null +++ b/test/toastification_type_test.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:iconsax_flutter/iconsax_flutter.dart'; +import 'package:toastification/src/built_in/toastification_type.dart'; +import 'package:toastification/src/utils/constants_values.dart'; + +void main() { + group('ToastificationType', () { + test('Default types', () { + expect(ToastificationType.info.name, 'info'); + expect(ToastificationType.info.color, infoColor); + expect(ToastificationType.info.icon, Iconsax.info_circle_copy); + + expect(ToastificationType.success.name, 'success'); + expect(ToastificationType.success.color, successColor); + expect(ToastificationType.success.icon, Iconsax.tick_circle_copy); + + expect(ToastificationType.warning.name, 'warning'); + expect(ToastificationType.warning.color, warningColor); + expect(ToastificationType.warning.icon, Iconsax.danger_copy); + + expect(ToastificationType.error.name, 'error'); + expect(ToastificationType.error.color, errorColor); + expect(ToastificationType.error.icon, Iconsax.close_circle_copy); + }); + + test('Default types: equality and hashCode', () { + expect(ToastificationType.info, ToastificationType.info); + expect( + ToastificationType.info.hashCode, ToastificationType.info.hashCode); + + expect(ToastificationType.success, ToastificationType.success); + expect(ToastificationType.success.hashCode, + ToastificationType.success.hashCode); + + expect(ToastificationType.warning, ToastificationType.warning); + expect(ToastificationType.warning.hashCode, + ToastificationType.warning.hashCode); + + expect(ToastificationType.error, ToastificationType.error); + expect( + ToastificationType.error.hashCode, ToastificationType.error.hashCode); + }); + + test('Custom types: creation', () { + const customName = 'custom'; + const customColor = Colors.purple; + const customIcon = Iconsax.star; + + final customType = + ToastificationType.custom(customName, customColor, customIcon); + + expect(customType.name, customName); + expect(customType.color, customColor); + expect(customType.icon, customIcon); + }); + + test('Custom types: equality and hashCode', () { + const customType1 = + ToastificationType.custom('custom', Colors.purple, Iconsax.star); + const customType2 = + ToastificationType.custom('custom', Colors.purple, Iconsax.star); + const customType3 = + ToastificationType.custom('different', Colors.red, Iconsax.heart); + + expect(customType1, customType2); + expect(customType1.hashCode, customType2.hashCode); + expect(customType1, isNot(customType3)); + expect(customType1.hashCode, isNot(customType3.hashCode)); + }); + + test('defaultValues contains all built-in types', () { + final defaults = ToastificationType.defaultValues; + expect(defaults.length, 4); + expect(defaults.contains(ToastificationType.info), true); + expect(defaults.contains(ToastificationType.success), true); + expect(defaults.contains(ToastificationType.warning), true); + expect(defaults.contains(ToastificationType.error), true); + }); + + test('toString returns correct string representation', () { + expect(ToastificationType.info.toString(), 'ToastificationType.info'); + expect( + ToastificationType.success.toString(), 'ToastificationType.success'); + expect( + ToastificationType.warning.toString(), 'ToastificationType.warning'); + expect(ToastificationType.error.toString(), 'ToastificationType.error'); + + const customType = + ToastificationType.custom('custom', Colors.purple, Iconsax.star); + expect(customType.toString(), 'ToastificationType.custom'); + }); + }); +}