diff --git a/.github/workflows/on-main.yml b/.github/workflows/on-main.yml index bb3b9aa4..15fb2e0e 100644 --- a/.github/workflows/on-main.yml +++ b/.github/workflows/on-main.yml @@ -33,6 +33,7 @@ jobs: dart format . -l 120 dart fix --apply flutter analyze + flutter test cd example && flutter test - name: Check for modified files id: git-check diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index dab856c5..72c1158a 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -66,6 +66,7 @@ jobs: dart format . -l 120 dart fix --apply flutter analyze + flutter test cd example && flutter test - name: Check for modified files id: git-check diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b53985d..24c127c2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,6 +34,127 @@ For more information on widgetbook, [read the docs](https://docs.widgetbook.io/) We should also create a test for each widget created. +### Contributing Guide for Writing Tests + +#### Folder Structure + +To maintain consistency and ease of navigation, follow the same folder structure in the `test` directory as in the `lib` directory. For example, if you have a file located at `lib/src/components/tooltip/tooltip.dart`, your test file should be located at `test/src/components/tooltip/tooltip_test.dart`. + +##### Example Folder Structure + +``` +lib/ +└── src/ + └── components/ + └── tooltip/ + └── tooltip.dart + +test/ +└── src/ + └── components/ + └── tooltip/ + └── tooltip_test.dart +``` + +#### Writing Tests + +1. **Unit Tests**: Test individual functions and classes. +2. **Widget Tests**: Test individual widgets and their interactions. +3. **Integration Tests**: Test the complete app or large parts of it. + +##### Guidelines + +- Use descriptive test names. +- Test one thing per test. +- Mock dependencies using `mockito`. +- Write tests for edge cases. + +#### Measuring Code Coverage + +To ensure high code coverage (at least > 96%), use the following steps: + +##### With 'lcov' +1. **Run the script coverage.sh from the project root, which make use of `lcov` to generate the coverage report**: + ```sh + sh coverage.sh + ``` +##### Alternatively, with 'genhtml' +1. **Install dependencies**: + ```sh + flutter pub add --dev test coverage + ``` + +2. **Run tests with coverage**: + ```sh + flutter test --coverage + ``` + +3. **Generate coverage report**: + ```sh + genhtml coverage/lcov.info -o coverage/html + ``` + +4. **View coverage report**: + Open `coverage/html/index.html` in a web browser. + + +##### Maximizing Coverage + +- Write tests for all public methods and classes. +- Include tests for edge cases and error handling. +- Avoid excluding files from coverage unless necessary. + +#### Golden Tests + +Golden tests are used to ensure that the visual output of your widgets remains consistent over time. They are particularly useful for complex UI components. + +##### Why Golden Tests? + +In the `tooltip` example, the direction of the arrows in the tooltip is crucial. Golden tests help to ensure that any changes to the tooltip do not unintentionally alter the direction of the arrows or other visual aspects. + +##### Adding Golden Tests + +1. **Set up golden tests**: + ```dart + import 'package:flutter_test/flutter_test.dart'; + import 'package:zeta_flutter/zeta_flutter.dart'; + + import '../../../test_utils/test_app.dart'; + + void main() { + testWidgets('renders with arrow correctly in up direction', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: Scaffold( + body: ZetaTooltip( + arrowDirection: ZetaTooltipArrowDirection.up, + child: Text('Tooltip up'), + ), + ), + ), + ); + + expect(find.text('Tooltip up'), findsOneWidget); + + // Verifying the CustomPaint with different arrow directions. + await expectLater( + find.byType(ZetaTooltip), + matchesGoldenFile(p.join('golden', 'arrow_up.png')), + ); + }); + } + ``` + +2. **Run golden tests**: + ```sh + flutter test --update-goldens + ``` + +3. **Verify golden tests**: + Ensure that the generated golden files are correct and commit them to the repository. + + + ## Code reviews All submissions, including submissions by project members, require review. We use GitHub pull requests (PRs) for this purpose. Consult [GitHub Help](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) for more information on using pull requests. diff --git a/coverage.sh b/coverage.sh new file mode 100644 index 00000000..a20e51f0 --- /dev/null +++ b/coverage.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Check if lcov is installed +if ! command -v lcov &> /dev/null +then + echo "lcov could not be found, please install it first." + exit +fi + +# Run the tests with coverage +flutter test --coverage + +# Generate the LCOV report +lcov --capture --directory coverage --output-file coverage/lcov.info + +# Remove unnecessary files from the report +lcov --remove coverage/lcov.info 'lib/*/*.g.dart' 'lib/*/*.freezed.dart' -o coverage/lcov.info + +# Generate the HTML report +genhtml coverage/lcov.info --output-directory coverage/html + +# Open the coverage report in the default browser +if [ "$(uname)" == "Darwin" ]; then + open coverage/html/index.html +elif [ "$(uname)" == "Linux" ]; then + xdg-open coverage/html/index.html +elif [ "$(uname)" == "CYGWIN" ] || [ "$(uname)" == "MINGW32" ] || [ "$(uname)" == "MINGW64" ]; then + start coverage/html/index.html +fi + +echo "Coverage report generated and opened in the default browser." diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 9625e105..7c569640 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/example/ios/Podfile b/example/ios/Podfile index 88359b22..279576f3 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '11.0' +# platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index f6342e74..15073017 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -6,11 +6,18 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS + - sqflite (0.0.3): + - Flutter + - FlutterMacOS + - url_launcher_ios (0.0.1): + - Flutter DEPENDENCIES: - Flutter (from `Flutter`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqflite (from `.symlinks/plugins/sqflite/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) EXTERNAL SOURCES: Flutter: @@ -19,12 +26,18 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/darwin" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + sqflite: + :path: ".symlinks/plugins/sqflite/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe -PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 +PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011 -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 83853dd6..62e10fdd 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -156,7 +156,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -343,7 +343,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -421,7 +421,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -470,7 +470,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a6b826db..5e31d3d3 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ ( value: e.value.identifier, alignment: Alignment.center, - child: CircleAvatar( + child: ZetaAvatar( + size: ZetaAvatarSize.xxs, backgroundColor: color.surface, - foregroundColor: color, - child: Icon(Icons.color_lens, color: color), + image: Icon(Icons.color_lens, color: color), ), ); }).toList(), diff --git a/example/lib/utils/theme_constrast_switch.dart b/example/lib/utils/theme_constrast_switch.dart index 780acd01..7e7c060d 100644 --- a/example/lib/utils/theme_constrast_switch.dart +++ b/example/lib/utils/theme_constrast_switch.dart @@ -34,12 +34,14 @@ class ZetaThemeContrastSwitch extends StatelessWidget { return DropdownMenuItem( value: e, alignment: Alignment.center, - child: CircleAvatar( + child: ZetaAvatar( + size: ZetaAvatarSize.xxs, backgroundColor: colors.primary.surface, - foregroundColor: colors.primary, - child: Text( - e == ZetaContrast.aa ? 'AA' : 'AAA', - style: ZetaTextStyles.bodyMedium.copyWith(color: colors.primary, fontWeight: FontWeight.w700), + image: Center( + child: Text( + e == ZetaContrast.aa ? 'AA' : 'AAA', + style: ZetaTextStyles.bodyMedium.copyWith(color: colors.primary, fontWeight: FontWeight.w700), + ), ), ), ); diff --git a/example/lib/utils/theme_mode_switch.dart b/example/lib/utils/theme_mode_switch.dart index fb9a22e1..200ceeba 100644 --- a/example/lib/utils/theme_mode_switch.dart +++ b/example/lib/utils/theme_mode_switch.dart @@ -36,10 +36,10 @@ class ZetaThemeModeSwitch extends StatelessWidget { return DropdownMenuItem( value: e, alignment: Alignment.center, - child: CircleAvatar( + child: ZetaAvatar( + size: ZetaAvatarSize.xxs, backgroundColor: colors.primary.surface, - foregroundColor: colors.primary, - child: Icon( + image: Icon( e == ThemeMode.system ? Icons.system_security_update_good : e == ThemeMode.light diff --git a/golden/arrow_down.png b/golden/arrow_down.png new file mode 100644 index 00000000..aa995748 Binary files /dev/null and b/golden/arrow_down.png differ diff --git a/golden/arrow_left.png b/golden/arrow_left.png new file mode 100644 index 00000000..7f66b33e Binary files /dev/null and b/golden/arrow_left.png differ diff --git a/golden/arrow_right.png b/golden/arrow_right.png new file mode 100644 index 00000000..30d99000 Binary files /dev/null and b/golden/arrow_right.png differ diff --git a/golden/arrow_up.png b/golden/arrow_up.png new file mode 100644 index 00000000..da590cb5 Binary files /dev/null and b/golden/arrow_up.png differ diff --git a/lib/src/theme/color_swatch.dart b/lib/src/theme/color_swatch.dart index 470eb16d..20c3ddb1 100644 --- a/lib/src/theme/color_swatch.dart +++ b/lib/src/theme/color_swatch.dart @@ -1,3 +1,4 @@ +import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'color_extensions.dart'; @@ -6,7 +7,7 @@ import 'contrast.dart'; /// A swatch of colors with values from 10 (light) to 100 (dark). @immutable -class ZetaColorSwatch extends ColorSwatch { +class ZetaColorSwatch extends ColorSwatch with EquatableMixin { /// Constructs a [ZetaColorSwatch]. /// /// See also: @@ -142,7 +143,8 @@ class ZetaColorSwatch extends ColorSwatch { /// Returns the color shade for a subtle visual element depending on the ZetaContrast value. /// - /// For both [ZetaContrast.aa] and [ZetaContrast.aaa], it returns 40. + /// For [ZetaContrast.aa], it returns 40. + /// For [ZetaContrast.aaa], it returns 60. Color get subtle => shade(contrast.subtle); /// Returns the color shade for a surface depending on the ZetaContrast value. @@ -185,14 +187,19 @@ class ZetaColorSwatch extends ColorSwatch { } @override - bool operator ==(Object other) => - identical(this, other) || - super == other && - other is ZetaColorSwatch && - runtimeType == other.runtimeType && - brightness == other.brightness && - contrast == other.contrast; - - @override - int get hashCode => super.hashCode ^ brightness.hashCode ^ contrast.hashCode; + List get props => [ + super.value, + brightness, + contrast, + shade10, + shade20, + shade30, + shade40, + shade50, + shade60, + shade70, + shade80, + shade90, + shade100, + ]; } diff --git a/lib/src/theme/colors.dart b/lib/src/theme/colors.dart index b6e5354b..685aa2f8 100644 --- a/lib/src/theme/colors.dart +++ b/lib/src/theme/colors.dart @@ -1,3 +1,4 @@ +import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'color_extensions.dart'; @@ -10,7 +11,7 @@ import 'contrast.dart'; /// /// A customizable, token-based color palette, adapting Zeta colors to Flutter's colorScheme. @immutable -class ZetaColors { +class ZetaColors extends Equatable { /// Default constructor for instance of [ZetaColors]. ZetaColors({ this.brightness = Brightness.light, @@ -305,7 +306,7 @@ class ZetaColors { /// Defaults to `ZetaColors.cool.90`. /// /// {@macro zeta-color-dark} - Color get iconDefault => textDefault; + Color get iconDefault => cool.shade90; /// Subtle icon color. /// @@ -314,14 +315,14 @@ class ZetaColors { /// Maps to [ColorScheme.onSurface]. /// /// {@macro zeta-color-dark} - Color get iconSubtle => textSubtle; + Color get iconSubtle => cool.shade70; /// Disabled icon color. /// /// Defaults to `ZetaColors.cool.50`. /// /// {@macro zeta-color-dark} - Color get iconDisabled => textDisabled; + Color get iconDisabled => cool.shade50; /// Inverse icon color. /// @@ -330,7 +331,7 @@ class ZetaColors { /// Defaults to `ZetaColors.cool.20`. /// /// {@macro zeta-color-dark} - Color get iconInverse => textInverse; + Color get iconInverse => cool.shade20; /// Default Surface Color /// @@ -350,12 +351,12 @@ class ZetaColors { /// Selected Surface Color /// /// {@macro zeta-color-dark} - Color get surfaceSelected => blue.shade(10); + Color get surfaceSelected => primary.shade(10); /// Selected-hover Surface Color /// /// {@macro zeta-color-dark} - Color get surfaceSelectedHover => blue.shade(20); + Color get surfaceSelectedHover => primary.shade(20); /// Disabled Surface Color /// @@ -375,7 +376,7 @@ class ZetaColors { /// Primary-subtle Surface Color /// /// {@macro zeta-color-dark} - Color get surfacePrimarySubtle => blue.shade(10); + Color get surfacePrimarySubtle => primary.shade(10); /// Avatar Avatar Surface Color /// @@ -415,7 +416,7 @@ class ZetaColors { /// Secondary-subtle Surface Color /// /// {@macro zeta-color-dark} - Color get surfaceSecondarySubtle => yellow.shade(10); + Color get surfaceSecondarySubtle => secondary.shade(10); /// Positive Surface Color /// @@ -490,17 +491,17 @@ class ZetaColors { /// Primary-main Border Color /// /// {@macro zeta-color-dark} - ZetaColorSwatch get borderPrimaryMain => blue; + ZetaColorSwatch get borderPrimaryMain => primary; /// Primary Border Color /// /// {@macro zeta-color-dark} - Color get borderPrimary => blue.shade(50); + Color get borderPrimary => primary.shade(50); /// Secondary Border Color /// /// {@macro zeta-color-dark} - Color get borderSecondary => yellow.shade(50); + Color get borderSecondary => secondary.shade(50); /// Positive Border Color /// @@ -688,53 +689,20 @@ class ZetaColors { } @override - bool operator ==(Object other) => - identical(this, other) || - other is ZetaColors && - runtimeType == other.runtimeType && - brightness == other.brightness && - contrast == other.contrast && - primary == other.primary && - secondary == other.secondary && - error == other.error && - cool == other.cool && - warm == other.warm && - white == other.white && - black == other.black && - surfacePrimary == other.surfacePrimary && - surfaceSecondary == other.surfaceSecondary && - surfaceTertiary == other.surfaceTertiary && - blue == other.blue && - green == other.green && - red == other.red && - orange == other.orange && - purple == other.purple && - yellow == other.yellow && - teal == other.teal && - pink == other.pink; - - @override - int get hashCode => - brightness.hashCode ^ - contrast.hashCode ^ - primary.hashCode ^ - secondary.hashCode ^ - error.hashCode ^ - cool.hashCode ^ - warm.hashCode ^ - white.hashCode ^ - black.hashCode ^ - surfacePrimary.hashCode ^ - surfaceSecondary.hashCode ^ - surfaceTertiary.hashCode ^ - blue.hashCode ^ - green.hashCode ^ - red.hashCode ^ - orange.hashCode ^ - purple.hashCode ^ - yellow.hashCode ^ - teal.hashCode ^ - pink.hashCode; + List get props => [ + brightness, + contrast, + primary, + secondary, + error, + cool, + warm, + white, + black, + surfacePrimary, + surfaceSecondary, + surfaceTertiary, + ]; } enum _ZetaColorProperties { @@ -1026,11 +994,13 @@ extension ZetaColorGetters on ColorScheme { T _resolveDefault(_ZetaColorProperties property) { switch (property) { case _ZetaColorProperties.primarySwatch: - case _ZetaColorProperties.secondarySwatch: return ZetaColorBase.blue.apply(brightness: brightness, contrast: contrast) as T; + case _ZetaColorProperties.secondarySwatch: + return ZetaColorBase.yellow.apply(brightness: brightness, contrast: contrast) as T; case _ZetaColorProperties.cool: return ZetaColorBase.cool.apply(brightness: brightness, contrast: contrast) as T; case _ZetaColorProperties.warm: + return ZetaColorBase.warm.apply(brightness: brightness, contrast: contrast) as T; case _ZetaColorProperties.textDefault: return ZetaColorBase.cool.apply(brightness: brightness, contrast: contrast).shade90 as T; case _ZetaColorProperties.textSubtle: diff --git a/lib/src/theme/colors_base.dart b/lib/src/theme/colors_base.dart index 56919cd5..62a3235e 100644 --- a/lib/src/theme/colors_base.dart +++ b/lib/src/theme/colors_base.dart @@ -16,9 +16,7 @@ import '../../zeta_flutter.dart'; /// * [warm] /// * [pure] /// * [cool]. -class ZetaColorBase { - ZetaColorBase._(); - +abstract final class ZetaColorBase { /// Link color for light mode. @Deprecated('This color has been deprecated as of v0.10.0') static const Color linkLight = Color(0xFF0257AC); diff --git a/lib/src/theme/theme_data.dart b/lib/src/theme/theme_data.dart index 9d95d801..de74be0f 100644 --- a/lib/src/theme/theme_data.dart +++ b/lib/src/theme/theme_data.dart @@ -1,3 +1,4 @@ +import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'color_extensions.dart'; @@ -14,7 +15,7 @@ export 'constants.dart'; /// /// This class encapsulates the colors and fonts used for the Zeta theme in both light and dark modes. @immutable -class ZetaThemeData { +class ZetaThemeData extends Equatable { /// Constructs a [ZetaThemeData]. /// /// If [primary] and/or [secondary] colors are provided, they will be used to create the light and dark Zeta color palettes. @@ -83,15 +84,10 @@ class ZetaThemeData { } @override - bool operator ==(Object other) => - identical(this, other) || - other is ZetaThemeData && - runtimeType == other.runtimeType && - fontFamily == other.fontFamily && - identifier == other.identifier && - _colorsLight == other._colorsLight && - _colorsDark == other._colorsDark; - - @override - int get hashCode => fontFamily.hashCode ^ identifier.hashCode ^ _colorsLight.hashCode ^ _colorsDark.hashCode; + List get props => [ + fontFamily, + identifier, + _colorsLight, + _colorsDark, + ]; } diff --git a/lib/src/utils/utils.dart b/lib/src/utils/debounce.dart similarity index 100% rename from lib/src/utils/utils.dart rename to lib/src/utils/debounce.dart diff --git a/lib/src/utils/extensions.dart b/lib/src/utils/extensions.dart index 486b3709..12d1a65d 100644 --- a/lib/src/utils/extensions.dart +++ b/lib/src/utils/extensions.dart @@ -93,7 +93,7 @@ extension ColorSwatches on ZetaWidgetStatus { extension StringExtensions on String? { /// Returns initials from a name. String get initials { - if (this == null) return ''; + if (this == null || (this?.isEmpty ?? true)) return ''; final List nameParts = this!.split(RegExp(r'\W+'))..removeWhere((item) => item.isEmpty); if (nameParts.isEmpty) return ''; return (nameParts.length > 1 @@ -104,9 +104,10 @@ extension StringExtensions on String? { .toUpperCase(); } - /// Capitalizes fist letter of string. + /// Capitalizes first letter of string. String capitalize() { - if (this == null) return ''; + if (this == null || this!.isEmpty) return ''; + if (this!.length == 1) return this!.toUpperCase(); return '${this![0].toUpperCase()}${this!.substring(1).toLowerCase()}'; } } diff --git a/lib/src/zeta.dart b/lib/src/zeta.dart index e28de7e3..cae027e8 100644 --- a/lib/src/zeta.dart +++ b/lib/src/zeta.dart @@ -1,11 +1,8 @@ -import 'dart:async'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'theme/contrast.dart'; import 'theme/theme_data.dart'; -import 'theme/theme_service.dart'; /// An [InheritedWidget] that provides access to Zeta theme settings. /// @@ -116,248 +113,3 @@ class Zeta extends InheritedWidget { ..add(EnumProperty('brightness', brightness)); } } - -/// A typedef for the ZetaAppBuilder function which takes [BuildContext], [ZetaThemeData], -/// and [ThemeMode] and returns a [Widget]. -typedef ZetaAppBuilder = Widget Function(BuildContext context, ZetaThemeData themeData, ThemeMode themeMode); - -/// A widget that provides Zeta theming and contrast data down the widget tree. -class ZetaProvider extends StatefulWidget with Diagnosticable { - /// Constructs a [ZetaProvider] widget. - /// - /// The [builder] argument is required. The [initialThemeMode], [initialContrast], - /// and [initialThemeData] arguments provide initial values. - ZetaProvider({ - required this.builder, - this.initialThemeMode = ThemeMode.system, - this.initialContrast = ZetaContrast.aa, - this.themeService, - ZetaThemeData? initialThemeData, - super.key, - }) : initialThemeData = initialThemeData ?? ZetaThemeData(); - - /// Specifies the initial theme mode for the app. - /// - /// It can be one of the values: [ThemeMode.system], [ThemeMode.light], or [ThemeMode.dark]. - /// Defaults to [ThemeMode.system]. - final ThemeMode initialThemeMode; - - /// Provides the initial theme data for the app. - /// - /// This contains all the theming information. If not provided, - /// it defaults to a basic [ZetaThemeData] instance. - final ZetaThemeData initialThemeData; - - /// Specifies the initial contrast setting for the app. - /// - /// Defaults to [ZetaContrast.aa]. - final ZetaContrast initialContrast; - - /// A builder function to construct the widget tree using the provided theming information. - /// - /// It receives the [BuildContext], [ZetaThemeData], and [ThemeMode] as arguments - /// and is expected to return a [Widget]. - final ZetaAppBuilder builder; - - /// A `ZetaThemeService` - /// - /// It provides the structure for loading and saving themes in Zeta application. - final ZetaThemeService? themeService; - - @override - State createState() => ZetaProviderState(); - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('themeData', initialThemeData)) - ..add(ObjectFlagProperty.has('builder', builder)) - ..add(EnumProperty('initialThemeMode', initialThemeMode)) - ..add(EnumProperty('initialContrast', initialContrast)) - ..add(DiagnosticsProperty('themeService', themeService)); - } - - /// Retrieves the [ZetaProviderState] from the provided context. - static ZetaProviderState of(BuildContext context) { - final zetaState = context.findAncestorStateOfType(); - if (zetaState != null) { - return zetaState; - } else { - throw FlutterError.fromParts( - [ - ErrorDescription('Unable to find ZetaProviderState in the widget tree.'), - ErrorHint( - 'Ensure that the context passed to ZetaProvider.of() is a descendant of a ZetaProvider widget. This usually means that ZetaProviderState should be an ancestor of the widget which uses this context.', - ), - ErrorSpacer(), - ErrorDescription('The widget for the context used was:'), - DiagnosticsProperty('widget', context.widget, showName: false), - ErrorSpacer(), - ErrorHint( - 'If you recently changed the type of that widget, or the widget tree, ensure the ZetaProvider widget is still an ancestor.', - ), - ], - ); - } - } -} - -/// The state associated with [ZetaProvider]. -class ZetaProviderState extends State with Diagnosticable, WidgetsBindingObserver { - // Fields for ZetaThemeManager. - - /// Represents the late initialization of the ZetaContrast value. - late ZetaContrast _contrast; - - /// Represents the late initialization of the ThemeMode value. - late ThemeMode _themeMode; - - /// Represents the late initialization of the ZetaThemeData object. - late ZetaThemeData _themeData; - - /// Represents the late initialization of the system's current brightness (dark or light mode). - late Brightness _platformBrightness; - - /// Represents a nullable brightness value to be used for brightness change debouncing. - Brightness? _debounceBrightness; - - /// Timer used for debouncing brightness changes. - Timer? _debounceTimer; - - /// Represents the duration for the debounce timer. - static const _debounceDuration = Duration(milliseconds: 500); - - /// This method is called when this object is inserted into the tree. - /// - /// Here, it also adds this object as an observer in [WidgetsBinding] instance - /// and initializes various fields related to the theme, contrast, and brightness of the app. - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addObserver(this); - - // Set the initial brightness with the system's current brightness from the first view of the platform dispatcher. - _platformBrightness = MediaQueryData.fromView(PlatformDispatcher.instance.views.first).platformBrightness; - - // Set the initial theme mode. - _themeMode = widget.initialThemeMode; - - // Set the initial contrast. - _contrast = widget.initialContrast; - - // Apply the initial contrast to the theme data. - _themeData = widget.initialThemeData.apply(contrast: _contrast); - } - - /// Clean up function to be called when this object is removed from the tree. - /// - /// This also removes this object as an observer from the [WidgetsBinding] instance. - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - super.dispose(); - } - - /// Overrides the [didChangePlatformBrightness] method from the parent class. - /// - /// This method gets information about the platform's brightness and updates the app if it ever changes. - /// The changes are debounced with a timer to avoid them being too frequent. - @override - void didChangePlatformBrightness() { - super.didChangePlatformBrightness(); - - // Get the platform brightness from the first view of the platform dispatcher - // `_debounceBrightness` will then hold the new brightness - _debounceBrightness = MediaQueryData.fromView(PlatformDispatcher.instance.views.first).platformBrightness; - - // If the current stored brightness value is different from the newly fetched value - if (_platformBrightness != _debounceBrightness) { - // If brightness has changed, cancel the existing timer and start a new one - - // Cancel existing timer if any - _debounceTimer?.cancel(); - - // Start a new timer with `_debounceDuration` delay - _debounceTimer = Timer(_debounceDuration, () { - // Once timer fires, check if brightness is still different and not null - if (_debounceBrightness != null && _platformBrightness != _debounceBrightness) { - // If brightness value has indeed changed, update the state - setState(() { - // Set the new brightness value - _platformBrightness = _debounceBrightness!; - }); - } - }); - } - } - - @override - Widget build(BuildContext context) { - return Zeta( - themeMode: _themeMode, - themeData: _themeData, - contrast: _contrast, - mediaBrightness: _platformBrightness, - child: widget.builder(context, _themeData, _themeMode), - ); - } - - @override - void didUpdateWidget(ZetaProvider oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.initialContrast != widget.initialContrast || - oldWidget.initialThemeMode != widget.initialThemeMode || - oldWidget.initialThemeData != widget.initialThemeData) { - setState(() { - _themeMode = widget.initialThemeMode; - _contrast = widget.initialContrast; - _themeData = widget.initialThemeData.apply(contrast: _contrast); - }); - } - } - - /// Updates the current theme mode. - void updateThemeMode(ThemeMode themeMode) { - setState(() { - _themeMode = themeMode; - _saveThemeChange(); - }); - } - - /// Updates the current theme data. - void updateThemeData(ZetaThemeData data) { - setState(() { - _themeData = data.apply(contrast: _contrast); - _saveThemeChange(); - }); - } - - /// Updates the current contrast. - void updateContrast(ZetaContrast contrast) { - setState(() { - _contrast = contrast; - _themeData = _themeData.apply(contrast: contrast); - _saveThemeChange(); - }); - } - - void _saveThemeChange() { - unawaited( - widget.themeService?.saveTheme( - themeData: _themeData, - themeMode: _themeMode, - contrast: _contrast, - ), - ); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('themeData', _themeData)) - ..add(EnumProperty('contrast', _contrast)) - ..add(EnumProperty('themeMode', _themeMode)); - } -} diff --git a/lib/src/zeta_provider.dart b/lib/src/zeta_provider.dart new file mode 100644 index 00000000..92eeb198 --- /dev/null +++ b/lib/src/zeta_provider.dart @@ -0,0 +1,257 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'theme/contrast.dart'; +import 'theme/theme_data.dart'; +import 'theme/theme_service.dart'; +import 'zeta.dart'; + +/// A typedef for the ZetaAppBuilder function which takes [BuildContext], [ZetaThemeData], +/// and [ThemeMode] and returns a [Widget]. +typedef ZetaAppBuilder = Widget Function(BuildContext context, ZetaThemeData themeData, ThemeMode themeMode); + +/// A widget that provides Zeta theming and contrast data down the widget tree. +class ZetaProvider extends StatefulWidget with Diagnosticable { + /// Constructs a [ZetaProvider] widget. + /// + /// The [builder] argument is required. The [initialThemeMode], [initialContrast], + /// and [initialThemeData] arguments provide initial values. + ZetaProvider({ + required this.builder, + this.initialThemeMode = ThemeMode.system, + this.initialContrast = ZetaContrast.aa, + this.themeService, + ZetaThemeData? initialThemeData, + super.key, + }) : initialThemeData = initialThemeData ?? ZetaThemeData(); + + /// Specifies the initial theme mode for the app. + /// + /// It can be one of the values: [ThemeMode.system], [ThemeMode.light], or [ThemeMode.dark]. + /// Defaults to [ThemeMode.system]. + final ThemeMode initialThemeMode; + + /// Provides the initial theme data for the app. + /// + /// This contains all the theming information. If not provided, + /// it defaults to a basic [ZetaThemeData] instance. + final ZetaThemeData initialThemeData; + + /// Specifies the initial contrast setting for the app. + /// + /// Defaults to [ZetaContrast.aa]. + final ZetaContrast initialContrast; + + /// A builder function to construct the widget tree using the provided theming information. + /// + /// It receives the [BuildContext], [ZetaThemeData], and [ThemeMode] as arguments + /// and is expected to return a [Widget]. + final ZetaAppBuilder builder; + + /// A `ZetaThemeService` + /// + /// It provides the structure for loading and saving themes in Zeta application. + final ZetaThemeService? themeService; + + @override + State createState() => ZetaProviderState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('themeData', initialThemeData)) + ..add(ObjectFlagProperty.has('builder', builder)) + ..add(EnumProperty('initialThemeMode', initialThemeMode)) + ..add(EnumProperty('initialContrast', initialContrast)) + ..add(DiagnosticsProperty('themeService', themeService)); + } + + /// Retrieves the [ZetaProviderState] from the provided context. + static ZetaProviderState of(BuildContext context) { + final zetaState = context.findAncestorStateOfType(); + if (zetaState != null) { + return zetaState; + } else { + throw FlutterError.fromParts( + [ + ErrorDescription('Unable to find ZetaProviderState in the widget tree.'), + ErrorHint( + 'Ensure that the context passed to ZetaProvider.of() is a descendant of a ZetaProvider widget. This usually means that ZetaProviderState should be an ancestor of the widget which uses this context.', + ), + ErrorSpacer(), + ErrorDescription('The widget for the context used was:'), + DiagnosticsProperty('widget', context.widget, showName: false), + ErrorSpacer(), + ErrorHint( + 'If you recently changed the type of that widget, or the widget tree, ensure the ZetaProvider widget is still an ancestor.', + ), + ], + ); + } + } +} + +/// The state associated with [ZetaProvider]. +class ZetaProviderState extends State with Diagnosticable, WidgetsBindingObserver { + // Fields for ZetaThemeManager. + + /// Represents the late initialization of the ZetaContrast value. + late ZetaContrast _contrast; + + /// Represents the late initialization of the ThemeMode value. + late ThemeMode _themeMode; + + /// Represents the late initialization of the ZetaThemeData object. + late ZetaThemeData _themeData; + + /// Represents the late initialization of the system's current brightness (dark or light mode). + late Brightness _platformBrightness; + + /// Represents a nullable brightness value to be used for brightness change debouncing. + Brightness? _debounceBrightness; + + /// Timer used for debouncing brightness changes. + Timer? _debounceTimer; + + /// Represents the duration for the debounce timer. + static const _debounceDuration = Duration(milliseconds: 250); + + /// This method is called when this object is inserted into the tree. + /// + /// Here, it also adds this object as an observer in [WidgetsBinding] instance + /// and initializes various fields related to the theme, contrast, and brightness of the app. + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + + // Set the initial brightness with the system's current brightness from the first view of the platform dispatcher. + _platformBrightness = MediaQueryData.fromView(PlatformDispatcher.instance.views.first).platformBrightness; + + // Set the initial theme mode. + _themeMode = widget.initialThemeMode; + + // Set the initial contrast. + _contrast = widget.initialContrast; + + // Apply the initial contrast to the theme data. + _themeData = widget.initialThemeData.apply(contrast: _contrast); + } + + /// Clean up function to be called when this object is removed from the tree. + /// + /// This also removes this object as an observer from the [WidgetsBinding] instance. + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + /// Overrides the [didChangePlatformBrightness] method from the parent class. + /// + /// This method gets information about the platform's brightness and updates the app if it ever changes. + /// The changes are debounced with a timer to avoid them being too frequent. + @override + void didChangePlatformBrightness() { + super.didChangePlatformBrightness(); + + // Get the binding instance + final binding = WidgetsFlutterBinding.ensureInitialized(); + + // Get the platform brightness from the PlatformDispatcher + // `_debounceBrightness` will then hold the new brightness + _debounceBrightness = binding.platformDispatcher.platformBrightness; + + // If the current stored brightness value is different from the newly fetched value + if (_platformBrightness != _debounceBrightness) { + // If brightness has changed, cancel the existing timer and start a new one + + // Cancel existing timer if any + _debounceTimer?.cancel(); + + // Start a new timer with `_debounceDuration` delay + _debounceTimer = Timer(_debounceDuration, () { + // Once timer fires, check if brightness is still different and not null + if (_debounceBrightness != null && _platformBrightness != _debounceBrightness) { + // If brightness value has indeed changed, update the state + setState(() { + // Set the new brightness value + _platformBrightness = _debounceBrightness!; + }); + } + }); + } + } + + @override + Widget build(BuildContext context) { + return Zeta( + themeMode: _themeMode, + themeData: _themeData, + contrast: _contrast, + mediaBrightness: _platformBrightness, + child: widget.builder(context, _themeData, _themeMode), + ); + } + + @override + void didUpdateWidget(ZetaProvider oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.initialContrast != widget.initialContrast || + oldWidget.initialThemeMode != widget.initialThemeMode || + oldWidget.initialThemeData != widget.initialThemeData) { + setState(() { + _themeMode = widget.initialThemeMode; + _contrast = widget.initialContrast; + _themeData = widget.initialThemeData.apply(contrast: _contrast); + }); + } + } + + /// Updates the current theme mode. + void updateThemeMode(ThemeMode themeMode) { + setState(() { + _themeMode = themeMode; + _saveThemeChange(); + }); + } + + /// Updates the current theme data. + void updateThemeData(ZetaThemeData data) { + setState(() { + _themeData = data.apply(contrast: _contrast); + _saveThemeChange(); + }); + } + + /// Updates the current contrast. + void updateContrast(ZetaContrast contrast) { + setState(() { + _contrast = contrast; + _themeData = _themeData.apply(contrast: contrast); + _saveThemeChange(); + }); + } + + void _saveThemeChange() { + unawaited( + widget.themeService?.saveTheme( + themeData: _themeData, + themeMode: _themeMode, + contrast: _contrast, + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('themeData', _themeData)) + ..add(EnumProperty('contrast', _contrast)) + ..add(EnumProperty('themeMode', _themeMode)); + } +} diff --git a/lib/zeta_flutter.dart b/lib/zeta_flutter.dart index 558d3477..c2bdba22 100644 --- a/lib/zeta_flutter.dart +++ b/lib/zeta_flutter.dart @@ -64,7 +64,8 @@ export 'src/theme/theme_data.dart'; export 'src/theme/theme_service.dart'; export 'src/theme/tokens.dart'; export 'src/theme/typography.dart'; +export 'src/utils/debounce.dart'; export 'src/utils/enums.dart'; export 'src/utils/extensions.dart'; -export 'src/utils/utils.dart'; export 'src/zeta.dart'; +export 'src/zeta_provider.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index fd05f6cf..a11b7581 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,6 +22,7 @@ environment: dependencies: collection: ^1.18.0 + equatable: ^2.0.5 flutter: sdk: flutter flutter_slidable: ^3.1.0 @@ -30,6 +31,12 @@ dependencies: web: ^0.5.1 dev_dependencies: + build_runner: ^2.4.10 + flutter_test: + sdk: flutter + image: ^4.2.0 + mockito: ^5.4.4 + path: ^1.9.0 zds_analysis: ^1.0.0 flutter: diff --git a/test/src/components/tooltip/golden/arrow_down.png b/test/src/components/tooltip/golden/arrow_down.png new file mode 100644 index 00000000..aa995748 Binary files /dev/null and b/test/src/components/tooltip/golden/arrow_down.png differ diff --git a/test/src/components/tooltip/golden/arrow_left.png b/test/src/components/tooltip/golden/arrow_left.png new file mode 100644 index 00000000..7f66b33e Binary files /dev/null and b/test/src/components/tooltip/golden/arrow_left.png differ diff --git a/test/src/components/tooltip/golden/arrow_right.png b/test/src/components/tooltip/golden/arrow_right.png new file mode 100644 index 00000000..30d99000 Binary files /dev/null and b/test/src/components/tooltip/golden/arrow_right.png differ diff --git a/test/src/components/tooltip/golden/arrow_up.png b/test/src/components/tooltip/golden/arrow_up.png new file mode 100644 index 00000000..da590cb5 Binary files /dev/null and b/test/src/components/tooltip/golden/arrow_up.png differ diff --git a/test/src/components/tooltip/tooltip_test.dart b/test/src/components/tooltip/tooltip_test.dart new file mode 100644 index 00000000..92931961 --- /dev/null +++ b/test/src/components/tooltip/tooltip_test.dart @@ -0,0 +1,247 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:path/path.dart' as p; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../../test_utils/test_app.dart'; +import '../../../test_utils/tolerant_comparator.dart'; + +void main() { + setUpAll(() { + final testUri = Uri.parse(p.join(Directory.current.path, 'golden').replaceAll(r'\', '/')); + goldenFileComparator = TolerantComparator(testUri, tolerance: 0.01); + }); + + group('ZetaTooltip Widget Tests', () { + testWidgets('renders with default properties', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: Scaffold( + body: ZetaTooltip( + child: Text('Tooltip text'), + ), + ), + ), + ); + + expect(find.text('Tooltip text'), findsOneWidget); + expect(find.byType(ZetaTooltip), findsOneWidget); + }); + + testWidgets('renders with custom color and padding', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: Scaffold( + body: ZetaTooltip( + color: Colors.red, + padding: EdgeInsets.all(20), + child: Text('Tooltip text'), + ), + ), + ), + ); + + final tooltipBox = tester.widget( + find.descendant( + of: find.byType(ZetaTooltip), + matching: find.byType(DecoratedBox), + ), + ); + + expect((tooltipBox.decoration as BoxDecoration).color, Colors.red); + + final padding = tester.widget( + find.descendant( + of: find.byType(ZetaTooltip), + matching: find.byType(Padding), + ), + ); + + expect(padding.padding, const EdgeInsets.all(20)); + }); + + testWidgets('renders with custom text style', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: Scaffold( + body: ZetaTooltip( + textStyle: TextStyle(fontSize: 24, color: Colors.blue), + child: Text('Tooltip text'), + ), + ), + ), + ); + + // Find the RichText widget, which is a descendant of the Text widget + final richTextFinder = find.descendant( + of: find.text('Tooltip text'), + matching: find.byType(RichText), + ); + + expect(richTextFinder, findsOneWidget); + + // Verify the styles + final RichText richTextWidget = tester.widget(richTextFinder); + final TextSpan textSpan = richTextWidget.text as TextSpan; + + expect(textSpan.style?.fontSize, 24); + expect(textSpan.style?.color, Colors.blue); + }); + + testWidgets('renders with arrow correctly in up direction', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: Scaffold( + body: ZetaTooltip( + arrowDirection: ZetaTooltipArrowDirection.up, + child: Text('Tooltip up'), + ), + ), + ), + ); + + expect(find.text('Tooltip up'), findsOneWidget); + + // Verifying the CustomPaint with different arrow directions. + await expectLater( + find.byType(ZetaTooltip), + matchesGoldenFile(p.join('golden', 'arrow_up.png')), + ); + }); + + testWidgets('renders with arrow correctly in down direction', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: Scaffold( + body: ZetaTooltip( + child: Text('Tooltip down'), + ), + ), + ), + ); + + expect(find.text('Tooltip down'), findsOneWidget); + + // Verifying the CustomPaint with different arrow directions. + await expectLater( + find.byType(ZetaTooltip), + matchesGoldenFile(p.join('golden', 'arrow_down.png')), + ); + }); + + testWidgets('renders with arrow correctly in left direction', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: Scaffold( + body: ZetaTooltip( + arrowDirection: ZetaTooltipArrowDirection.left, + child: Text('Tooltip left'), + ), + ), + ), + ); + + expect(find.text('Tooltip left'), findsOneWidget); + + // Verifying the CustomPaint with different arrow directions. + await expectLater( + find.byType(ZetaTooltip), + matchesGoldenFile(p.join('golden', 'arrow_left.png')), + ); + }); + + testWidgets('renders with arrow correctly in right direction', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: Scaffold( + body: ZetaTooltip( + arrowDirection: ZetaTooltipArrowDirection.right, + child: Text('Tooltip right'), + ), + ), + ), + ); + + expect(find.text('Tooltip right'), findsOneWidget); + + // Verifying the CustomPaint with different arrow directions. + await expectLater( + find.byType(ZetaTooltip), + matchesGoldenFile(p.join('golden', 'arrow_right.png')), + ); + }); + + testWidgets('renders with rounded and sharp corners', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: Scaffold( + body: Column( + children: [ + ZetaTooltip( + child: Text('Rounded tooltip'), + ), + ZetaTooltip( + rounded: false, + child: Text('Sharp tooltip'), + ), + ], + ), + ), + ), + ); + + expect(find.text('Rounded tooltip'), findsOneWidget); + expect(find.text('Sharp tooltip'), findsOneWidget); + + final roundedTooltipBox = tester.widget( + find.descendant( + of: find.widgetWithText(ZetaTooltip, 'Rounded tooltip'), + matching: find.byType(DecoratedBox), + ), + ); + + final sharpTooltipBox = tester.widget( + find.descendant( + of: find.widgetWithText(ZetaTooltip, 'Sharp tooltip'), + matching: find.byType(DecoratedBox), + ), + ); + + expect((roundedTooltipBox.decoration as BoxDecoration).borderRadius, ZetaRadius.minimal); + expect((sharpTooltipBox.decoration as BoxDecoration).borderRadius, null); + }); + + testWidgets('debugFillProperties works correctly', (WidgetTester tester) async { + final diagnostics = DiagnosticPropertiesBuilder(); + const ZetaTooltip( + padding: EdgeInsets.all(8), + color: Colors.amber, + textStyle: TextStyle(fontSize: 9), + maxWidth: 170, + child: Text('Rounded tooltip'), + ).debugFillProperties(diagnostics); + + final rounded = diagnostics.properties.where((p) => p.name == 'rounded').map((p) => p.toDescription()).first; + expect(rounded, 'true'); + + final padding = diagnostics.properties.where((p) => p.name == 'padding').map((p) => p.toDescription()).first; + expect(padding, 'EdgeInsets.all(8.0)'); + + final color = diagnostics.properties.where((p) => p.name == 'color').map((p) => p.toDescription()).first; + expect(color.toLowerCase(), contains(Colors.amber.hexCode.toLowerCase())); + + final textStyle = diagnostics.properties.where((p) => p.name == 'textStyle').map((p) => p.toDescription()).first; + expect(textStyle, contains('size: 9.0')); + + final direction = + diagnostics.properties.where((p) => p.name == 'arrowDirection').map((p) => p.toDescription()).first; + expect(direction, 'down'); + + final maxWidth = diagnostics.properties.where((p) => p.name == 'maxWidth').map((p) => p.toDescription()).first; + expect(maxWidth, '170.0'); + }); + }); +} diff --git a/test/src/theme/breakpoints_test.dart b/test/src/theme/breakpoints_test.dart new file mode 100644 index 00000000..43f5b6fd --- /dev/null +++ b/test/src/theme/breakpoints_test.dart @@ -0,0 +1,137 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zeta_flutter/src/theme/breakpoints.dart'; + +import '../../test_utils/test_app.dart'; + +void main() { + group('BreakpointLocal extension', () { + test('returns DeviceType.mobilePortrait for widths <= 479', () { + const constraints = BoxConstraints(maxWidth: 479); + expect(constraints.deviceType, DeviceType.mobilePortrait); + }); + + test('returns DeviceType.mobileLandscape for widths <= 767', () { + const constraints = BoxConstraints(maxWidth: 767); + expect(constraints.deviceType, DeviceType.mobileLandscape); + }); + + test('returns DeviceType.tablet for widths <= 991', () { + const constraints = BoxConstraints(maxWidth: 991); + expect(constraints.deviceType, DeviceType.tablet); + }); + + test('returns DeviceType.desktop for widths <= 1279', () { + const constraints = BoxConstraints(maxWidth: 1279); + expect(constraints.deviceType, DeviceType.desktop); + }); + + test('returns DeviceType.desktopL for widths <= 1439', () { + const constraints = BoxConstraints(maxWidth: 1439); + expect(constraints.deviceType, DeviceType.desktopL); + }); + + test('returns DeviceType.desktopXL for widths > 1439', () { + const constraints = BoxConstraints(maxWidth: 1920); + expect(constraints.deviceType, DeviceType.desktopXL); + }); + }); + + group('BreakpointFull extension', () { + testWidgets('returns DeviceType.mobilePortrait for widths <= 479', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: MediaQuery( + data: const MediaQueryData(size: Size(479, 800)), + child: Builder( + builder: (context) { + expect(context.deviceType, DeviceType.mobilePortrait); + return Container(); + }, + ), + ), + ), + ); + }); + + testWidgets('returns DeviceType.mobileLandscape for widths <= 767', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: MediaQuery( + data: const MediaQueryData(size: Size(767, 800)), + child: Builder( + builder: (context) { + expect(context.deviceType, DeviceType.mobileLandscape); + return Container(); + }, + ), + ), + ), + ); + }); + + testWidgets('returns DeviceType.tablet for widths <= 991', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: MediaQuery( + data: const MediaQueryData(size: Size(991, 800)), + child: Builder( + builder: (context) { + expect(context.deviceType, DeviceType.tablet); + return Container(); + }, + ), + ), + ), + ); + }); + + testWidgets('returns DeviceType.desktop for widths <= 1279', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: MediaQuery( + data: const MediaQueryData(size: Size(1279, 800)), + child: Builder( + builder: (context) { + expect(context.deviceType, DeviceType.desktop); + return Container(); + }, + ), + ), + ), + ); + }); + + testWidgets('returns DeviceType.desktopL for widths <= 1439', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: MediaQuery( + data: const MediaQueryData(size: Size(1439, 800)), + child: Builder( + builder: (context) { + expect(context.deviceType, DeviceType.desktopL); + return Container(); + }, + ), + ), + ), + ); + }); + + testWidgets('returns DeviceType.desktopXL for widths > 1439', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: MediaQuery( + data: const MediaQueryData(size: Size(1920, 800)), + child: Builder( + builder: (context) { + expect(context.deviceType, DeviceType.desktopXL); + return Container(); + }, + ), + ), + ), + ); + }); + }); +} diff --git a/test/src/theme/color_extensions_test.dart b/test/src/theme/color_extensions_test.dart new file mode 100644 index 00000000..be017c94 --- /dev/null +++ b/test/src/theme/color_extensions_test.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +void main() { + group('ZetaColorExtensions', () { + test('zetaColorSwatch returns correct ZetaColorSwatch', () { + const color = Colors.blue; + final swatch = color.zetaColorSwatch; + expect(swatch.runtimeType, ZetaColorSwatch); + }); + + test('brighten brightens the color correctly', () { + const color = Color(0xFF123456); + final brightenedColor = color.brighten(); + expect(brightenedColor, isNot(color)); + expect(brightenedColor.computeLuminance(), greaterThan(color.computeLuminance())); + }); + + test('lighten lightens the color correctly', () { + const color = Color(0xFF123456); + final lightenedColor = color.lighten(); + expect(lightenedColor, isNot(color)); + expect(HSLColor.fromColor(lightenedColor).lightness, greaterThan(HSLColor.fromColor(color).lightness)); + }); + + test('darken darkens the color correctly', () { + const color = Color(0xFF123456); + final darkenedColor = color.darken(); + expect(darkenedColor, isNot(color)); + expect(darkenedColor.computeLuminance(), lessThan(color.computeLuminance())); + }); + + test('onColor returns the correct on color', () { + const color = Colors.blue; + expect(color.onColor, ZetaColorBase.white); + + const lightColor = Colors.white; + expect(lightColor.onColor, ZetaColorBase.cool.shade90); + }); + + test('isLight returns true for light colors', () { + const color = Colors.white; + expect(color.isLight, isTrue); + }); + + test('isDark returns true for dark colors', () { + const color = Colors.black; + expect(color.isDark, isTrue); + }); + + test('blend blends two colors correctly', () { + const color1 = Colors.red; + const color2 = Colors.blue; + final blendedColor = color1.blend(color2, 50); + expect(blendedColor, isNot(color1)); + expect(blendedColor, isNot(color2)); + }); + + test('blendAlpha blends two colors with alpha correctly', () { + const color1 = Colors.red; + const color2 = Colors.blue; + final blendedColor = color1.blendAlpha(color2, 0x80); + expect(blendedColor, isNot(color1)); + expect(blendedColor, isNot(color2)); + }); + + test('getShadeColor returns correct shade color', () { + const color = Colors.blue; + final shadeColor = color.getShadeColor(); + expect(shadeColor, isNot(color)); + }); + + test('getShadeColor returns correct shade color for dark', () { + const color = Colors.blue; + final shadeColor = color.getShadeColor(lighten: false); + expect(shadeColor, isNot(color)); + }); + + test('hexCode returns correct hex code', () { + const color = Color(0xFF123456); + expect(color.hexCode, 'FF123456'); + }); + + test('withLightness changes lightness correctly', () { + const color = Colors.blue; + final newColor = color.withLightness(0.5); + expect(newColor, isNot(color)); + }); + + test('lightness returns correct lightness', () { + const color = Colors.blue; + final lightness = color.lightness; + expect(lightness, isNotNull); + }); + + test('contrastRatio returns correct ratio', () { + const color1 = Colors.red; + const color2 = Colors.blue; + final contrastRatio = color1.contrastRatio(color2); + expect(contrastRatio, isNotNull); + }); + + test('contrastRatio returns correct ratio for dark color', () { + const color1 = Colors.blue; + const color2 = Colors.red; + final contrastRatio = color1.contrastRatio(color2); + expect(contrastRatio, isNotNull); + }); + + test('adjustContrast adjusts contrast correctly', () { + const color = Colors.blue; + const onColor = Colors.white; + final adjustedColor = color.adjustContrast(on: onColor, target: 4.5); + expect(adjustedColor, isNot(color)); + }); + + test('generateSwatch generates correct swatch', () { + const color = Colors.blue; + final swatch = color.generateSwatch(); + expect(swatch, isNotEmpty); + }); + + test('ensureAccessibility ensures accessibility correctly', () { + const color = Colors.blue; + const onColor = Colors.white; + final accessibleColor = color.ensureAccessibility(on: onColor); + expect(accessibleColor, isNot(color)); + }); + }); +} diff --git a/test/src/theme/color_scheme_test.dart b/test/src/theme/color_scheme_test.dart new file mode 100644 index 00000000..0e2cb063 --- /dev/null +++ b/test/src/theme/color_scheme_test.dart @@ -0,0 +1,108 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +void main() { + group('ZetaColorScheme', () { + const String fontFamily = 'TestFontFamily'; + final ZetaColors zetaColors = ZetaColors( + primary: ZetaColorBase.blue, + secondary: ZetaColorBase.green, + surfacePrimary: ZetaColorBase.white, + surfaceTertiary: ZetaColorBase.white, + error: ZetaColorBase.red, + ); + + final ZetaColorScheme zetaColorScheme = ZetaColorScheme( + zetaColors: zetaColors, + fontFamily: fontFamily, + brightness: Brightness.light, + primary: Colors.blue, + onPrimary: Colors.white, + secondary: Colors.green, + onSecondary: Colors.white, + error: Colors.red, + onError: Colors.white, + surface: Colors.grey, + onSurface: Colors.black, + ); + + test('initialization of properties', () { + expect(zetaColorScheme.zetaColors, zetaColors); + expect(zetaColorScheme.fontFamily, fontFamily); + expect(zetaColorScheme.brightness, Brightness.light); + expect(zetaColorScheme.primary, Colors.blue); + expect(zetaColorScheme.onPrimary, Colors.white); + expect(zetaColorScheme.secondary, Colors.green); + expect(zetaColorScheme.onSecondary, Colors.white); + expect(zetaColorScheme.error, Colors.red); + expect(zetaColorScheme.onError, Colors.white); + expect(zetaColorScheme.surface, Colors.grey); + expect(zetaColorScheme.onSurface, Colors.black); + }); + + test('copyWith copies and overrides properties correctly', () { + final newZetaColors = ZetaColors( + primary: ZetaColorBase.purple, + secondary: ZetaColorBase.orange, + surfacePrimary: ZetaColorBase.yellow, + surfaceTertiary: ZetaColorBase.yellow, + error: ZetaColorBase.pink, + ); + const newFontFamily = 'NewTestFontFamily'; + + final copied = zetaColorScheme.copyWith( + zetaColors: newZetaColors, + fontFamily: newFontFamily, + primary: Colors.purple, + onPrimary: Colors.black, + ); + + expect(copied.zetaColors, newZetaColors); + expect(copied.fontFamily, newFontFamily); + expect(copied.primary, Colors.purple); + expect(copied.onPrimary, Colors.black); + expect(copied.secondary, zetaColorScheme.secondary); // Unchanged property + }); + + test('equality operator works as expected', () { + final identicalColorScheme = ZetaColorScheme( + zetaColors: zetaColors, + fontFamily: fontFamily, + brightness: Brightness.light, + primary: Colors.blue, + onPrimary: Colors.white, + secondary: Colors.green, + onSecondary: Colors.white, + error: Colors.red, + onError: Colors.white, + surface: Colors.grey, + onSurface: Colors.black, + ); + + expect(zetaColorScheme, identicalColorScheme); + expect(zetaColorScheme.hashCode, identicalColorScheme.hashCode); + expect(zetaColorScheme == identicalColorScheme, isTrue); + }); + + test('debugFillProperties includes correct properties', () { + final DiagnosticPropertiesBuilder properties = DiagnosticPropertiesBuilder(); + zetaColorScheme.debugFillProperties(properties); + + final zetaColorsProperty = properties.properties.firstWhere( + (prop) => prop is DiagnosticsProperty && prop.name == 'zetaColors', + ) as DiagnosticsProperty?; + + final fontFamilyProperty = properties.properties.firstWhere( + (prop) => prop is StringProperty && prop.name == 'fontFamily', + ) as StringProperty?; + + expect(zetaColorsProperty, isNotNull); + expect(zetaColorsProperty?.value, zetaColors); + + expect(fontFamilyProperty, isNotNull); + expect(fontFamilyProperty?.value, fontFamily); + }); + }); +} diff --git a/test/src/theme/color_swatch_test.dart b/test/src/theme/color_swatch_test.dart new file mode 100644 index 00000000..8bf33200 --- /dev/null +++ b/test/src/theme/color_swatch_test.dart @@ -0,0 +1,163 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +void main() { + group('ZetaColorSwatch', () { + late ZetaColorSwatch zetaColorSwatch; + + setUp(() { + zetaColorSwatch = ZetaColorSwatch( + primary: Colors.blue.value, + swatch: { + 10: Colors.blue.shade100, + 20: Colors.blue.shade200, + 30: Colors.blue.shade300, + 40: Colors.blue.shade400, + 50: Colors.blue.shade500, + 60: Colors.blue, + 70: Colors.blue.shade700, + 80: Colors.blue.shade800, + 90: Colors.blue.shade900, + 100: Colors.blue.shade900, + }, + ); + }); + + test('initialization of properties', () { + expect(zetaColorSwatch.brightness, Brightness.light); + expect(zetaColorSwatch.contrast, ZetaContrast.aa); + expect(zetaColorSwatch.shade10, Colors.blue.shade100); + expect(zetaColorSwatch.shade20, Colors.blue.shade200); + expect(zetaColorSwatch.shade30, Colors.blue.shade300); + expect(zetaColorSwatch.shade40, Colors.blue.shade400); + expect(zetaColorSwatch.shade50, Colors.blue.shade500); + expect(zetaColorSwatch.shade60, Colors.blue); + expect(zetaColorSwatch.shade70, Colors.blue.shade700); + expect(zetaColorSwatch.shade80, Colors.blue.shade800); + expect(zetaColorSwatch.shade90, Colors.blue.shade900); + expect(zetaColorSwatch.shade100, Colors.blue.shade900); + }); + + test('fromColor factory constructor', () { + final swatch = ZetaColorSwatch.fromColor( + Colors.blue, + ); + + expect(swatch.value, Colors.blue.value); + expect(swatch.brightness, Brightness.light); + expect(swatch.contrast, ZetaContrast.aa); + }); + + test('apply method', () { + final appliedSwatch = zetaColorSwatch.apply( + brightness: Brightness.dark, + contrast: ZetaContrast.aaa, + ); + + expect(appliedSwatch.brightness, Brightness.dark); + expect(appliedSwatch.contrast, ZetaContrast.aaa); + expect(appliedSwatch.shade10, zetaColorSwatch.shade100); + expect(appliedSwatch.shade20, zetaColorSwatch.shade90); + expect(appliedSwatch.shade30, zetaColorSwatch.shade80); + expect(appliedSwatch.shade40, zetaColorSwatch.shade70); + expect(appliedSwatch.shade50, zetaColorSwatch.shade60); + expect(appliedSwatch.shade60, zetaColorSwatch.shade50); + expect(appliedSwatch.shade70, zetaColorSwatch.shade40); + expect(appliedSwatch.shade80, zetaColorSwatch.shade30); + expect(appliedSwatch.shade90, zetaColorSwatch.shade20); + expect(appliedSwatch.shade100, zetaColorSwatch.shade10); + }); + + test('equality operator works as expected', () { + final identicalSwatch = ZetaColorSwatch( + primary: Colors.blue.value, + swatch: { + 10: Colors.blue.shade100, + 20: Colors.blue.shade200, + 30: Colors.blue.shade300, + 40: Colors.blue.shade400, + 50: Colors.blue.shade500, + 60: Colors.blue, + 70: Colors.blue.shade700, + 80: Colors.blue.shade800, + 90: Colors.blue.shade900, + 100: Colors.blue.shade900, + }, + ); + + expect(zetaColorSwatch, identicalSwatch); + expect(zetaColorSwatch == identicalSwatch, isTrue); + }); + + test('hashCode method works as expected', () { + final identicalSwatch = ZetaColorSwatch( + primary: Colors.blue.value, + swatch: { + 10: Colors.blue.shade100, + 20: Colors.blue.shade200, + 30: Colors.blue.shade300, + 40: Colors.blue.shade400, + 50: Colors.blue.shade500, + 60: Colors.blue, + 70: Colors.blue.shade700, + 80: Colors.blue.shade800, + 90: Colors.blue.shade900, + 100: Colors.blue.shade900, + }, + ); + + expect(zetaColorSwatch.hashCode, identicalSwatch.hashCode); + }); + + test('shade getters works as expected', () { + expect(zetaColorSwatch.shade10, Colors.blue.shade100); + expect(zetaColorSwatch.shade20, Colors.blue.shade200); + expect(zetaColorSwatch.shade30, Colors.blue.shade300); + expect(zetaColorSwatch.shade40, Colors.blue.shade400); + expect(zetaColorSwatch.shade50, Colors.blue.shade500); + expect(zetaColorSwatch.shade60, Colors.blue); + expect(zetaColorSwatch.shade70, Colors.blue.shade700); + expect(zetaColorSwatch.shade80, Colors.blue.shade800); + expect(zetaColorSwatch.shade90, Colors.blue.shade900); + expect(zetaColorSwatch.shade100, Colors.blue.shade900); + }); + + test('token getters works as expected', () { + expect(zetaColorSwatch.text.value, Colors.blue.value); + expect(zetaColorSwatch.icon.value, Colors.blue.value); + expect(zetaColorSwatch.hover.value, Colors.blue.shade700.value); + expect(zetaColorSwatch.selected.value, Colors.blue.shade800.value); + expect(zetaColorSwatch.focus.value, Colors.blue.shade800.value); + expect(zetaColorSwatch.border.value, Colors.blue.value); + expect(zetaColorSwatch.subtle.value, Colors.blue.shade400.value); + expect(zetaColorSwatch.surface.value, Colors.blue.shade100.value); + + final aaaSwatch = ZetaColorSwatch( + contrast: ZetaContrast.aaa, + primary: Colors.blue.value, + swatch: { + 10: Colors.blue.shade100, + 20: Colors.blue.shade200, + 30: Colors.blue.shade300, + 40: Colors.blue.shade400, + 50: Colors.blue.shade500, + 60: Colors.blue, + 70: Colors.blue.shade700, + 80: Colors.blue.shade800, + 90: Colors.blue.shade900, + 100: Colors.blue.shade900, + }, + ); + + expect(aaaSwatch.text.value, Colors.blue.shade800.value); + expect(aaaSwatch.icon.value, Colors.blue.shade800.value); + expect(aaaSwatch.hover.value, Colors.blue.shade900.value); + expect(aaaSwatch.selected.value, Colors.blue.shade900.value); + expect(aaaSwatch.focus.value, Colors.blue.shade900.value); + expect(aaaSwatch.border.value, Colors.blue.shade800.value); + expect(aaaSwatch.subtle.value, Colors.blue.shade500.value); + expect(aaaSwatch.surface.value, Colors.blue.shade100.value); + }); + }); +} diff --git a/test/src/theme/colors_test.dart b/test/src/theme/colors_test.dart new file mode 100644 index 00000000..cb4d6367 --- /dev/null +++ b/test/src/theme/colors_test.dart @@ -0,0 +1,291 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +void main() { + group('ZetaColors', () { + test('default constructor initializes correctly', () { + final zetaColors = ZetaColors(); + + expect(zetaColors.brightness, Brightness.light); + expect(zetaColors.contrast, ZetaContrast.aa); + expect(zetaColors.white, ZetaColorBase.white); + expect(zetaColors.black, ZetaColorBase.black); + expect(zetaColors.primary, isNotNull); + expect(zetaColors.secondary, isNotNull); + expect(zetaColors.error, isNotNull); + expect(zetaColors.cool, isNotNull); + expect(zetaColors.warm, isNotNull); + expect(zetaColors.pure, isNotNull); + expect(zetaColors.surfacePrimary, ZetaColorBase.white); + expect(zetaColors.surfaceSecondary, isNotNull); + expect(zetaColors.surfaceTertiary, isNotNull); + }); + + test('light constructor initializes correctly', () { + final zetaColors = ZetaColors.light( + warm: ZetaColorBase.warm, + cool: ZetaColorBase.cool, + ); + + expect(zetaColors.brightness, Brightness.light); + expect(zetaColors.contrast, ZetaContrast.aa); + expect(zetaColors.white, ZetaColorBase.white); + expect(zetaColors.black, ZetaColorBase.black); + expect(zetaColors.primary, isNotNull); + expect(zetaColors.secondary, isNotNull); + expect(zetaColors.error, isNotNull); + expect(zetaColors.cool, isNotNull); + expect(zetaColors.warm, isNotNull); + expect(zetaColors.pure, isNotNull); + expect(zetaColors.surfacePrimary, ZetaColorBase.white); + expect(zetaColors.surfaceSecondary, isNotNull); + expect(zetaColors.surfaceTertiary, isNotNull); + }); + + test('dark constructor initializes correctly', () { + final zetaColors = ZetaColors.dark( + warm: ZetaColorBase.warm, + cool: ZetaColorBase.cool, + ); + + expect(zetaColors.brightness, Brightness.dark); + expect(zetaColors.contrast, ZetaContrast.aa); + expect(zetaColors.white, ZetaColorBase.white); + expect(zetaColors.black, ZetaColorBase.black); + expect(zetaColors.primary, isNotNull); + expect(zetaColors.secondary, isNotNull); + expect(zetaColors.error, isNotNull); + expect(zetaColors.cool, isNotNull); + expect(zetaColors.warm, isNotNull); + expect(zetaColors.pure, isNotNull); + expect(zetaColors.surfacePrimary, ZetaColorBase.black); + expect(zetaColors.surfaceSecondary, isNotNull); + expect(zetaColors.surfaceTertiary, isNotNull); + }); + + test('copyWith creates a new instance with updated properties', () { + final zetaColors = ZetaColors().copyWith(); + final newColors = zetaColors.copyWith( + brightness: Brightness.dark, + contrast: ZetaContrast.aaa, + primary: ZetaColorBase.green, + secondary: ZetaColorBase.orange, + ); + + expect(newColors.isDarkMode, true); + expect(newColors.brightness, Brightness.dark); + expect(newColors.contrast, ZetaContrast.aaa); + expect(newColors.primary, ZetaColorBase.green.apply(brightness: Brightness.dark, contrast: ZetaContrast.aaa)); + expect(newColors.secondary, ZetaColorBase.orange.apply(brightness: Brightness.dark, contrast: ZetaContrast.aaa)); + expect(newColors.white, zetaColors.white); + expect(newColors.black, zetaColors.black); + }); + + test('rainbow getters returns correct colors', () { + final zetaColors = ZetaColors(); + expect(zetaColors.rainbow[0], zetaColors.red); + expect(zetaColors.rainbow[1], zetaColors.orange); + expect(zetaColors.rainbow[2], zetaColors.yellow); + expect(zetaColors.rainbow[3], zetaColors.green); + expect(zetaColors.rainbow[4], zetaColors.blue); + expect(zetaColors.rainbow[5], zetaColors.teal); + expect(zetaColors.rainbow[6], zetaColors.pink); + + expect(zetaColors.rainbowMap['red'], zetaColors.red); + expect(zetaColors.rainbowMap['orange'], zetaColors.orange); + expect(zetaColors.rainbowMap['yellow'], zetaColors.yellow); + expect(zetaColors.rainbowMap['green'], zetaColors.green); + expect(zetaColors.rainbowMap['blue'], zetaColors.blue); + expect(zetaColors.rainbowMap['teal'], zetaColors.teal); + expect(zetaColors.rainbowMap['pink'], zetaColors.pink); + }); + + test('apply returns a new instance with updated contrast', () { + final zetaColors = ZetaColors(); + final newColors = zetaColors.apply(contrast: ZetaContrast.aaa); + + expect(newColors.contrast, ZetaContrast.aaa); + expect(newColors.primary, isNotNull); + expect(newColors.secondary, isNotNull); + expect(newColors.error, isNotNull); + expect(newColors.cool, isNotNull); + expect(newColors.warm, isNotNull); + expect(newColors.pure, isNotNull); + }); + + test('toScheme returns a ZetaColorScheme with correct values', () { + final zetaColors = ZetaColors(); + final scheme = zetaColors.toScheme(); + + expect(scheme.zetaColors, zetaColors); + expect(scheme.brightness, zetaColors.brightness); + expect(scheme.primary, zetaColors.primary.shade(zetaColors.contrast.primary)); + expect(scheme.secondary, zetaColors.secondary.shade(zetaColors.contrast.primary)); + expect(scheme.surface, zetaColors.surfacePrimary); + expect(scheme.error, zetaColors.error); + }); + + test('Color getter returns correct values', () { + final zetaColors = ZetaColors(); + + expect(zetaColors.textDefault, ZetaColorBase.cool.shade90); + expect(zetaColors.textSubtle, ZetaColorBase.cool.shade70); + expect(zetaColors.textDisabled, ZetaColorBase.cool.shade50); + expect(zetaColors.textInverse, ZetaColorBase.cool.shade20); + expect(zetaColors.iconDefault, ZetaColorBase.cool.shade90); + expect(zetaColors.iconSubtle, ZetaColorBase.cool.shade70); + expect(zetaColors.iconDisabled, ZetaColorBase.cool.shade50); + expect(zetaColors.iconInverse, ZetaColorBase.cool.shade20); + expect(zetaColors.surfaceDefault, ZetaColorBase.pure.shade(0)); + expect(zetaColors.surfaceDefaultInverse, ZetaColorBase.warm.shade(100)); + expect(zetaColors.surfaceHover, ZetaColorBase.cool.shade(20)); + expect(zetaColors.surfaceSelected, ZetaColorBase.blue.shade(10)); + expect(zetaColors.surfaceSelectedHover, ZetaColorBase.blue.shade(20)); + expect(zetaColors.surfaceDisabled, ZetaColorBase.cool.shade(30)); + expect(zetaColors.surfaceCool, ZetaColorBase.cool.shade(10)); + expect(zetaColors.surfaceWarm, ZetaColorBase.warm.shade(10)); + expect(zetaColors.surfacePrimarySubtle, ZetaColorBase.blue.shade(10)); + expect(zetaColors.surfaceAvatarBlue, ZetaColorBase.blue.shade(80)); + expect(zetaColors.surfaceAvatarOrange, ZetaColorBase.orange.shade(50)); + expect(zetaColors.surfaceAvatarPink, ZetaColorBase.pink.shade(80)); + expect(zetaColors.surfaceAvatarPurple, ZetaColorBase.purple.shade(80)); + expect(zetaColors.surfaceAvatarTeal, ZetaColorBase.teal.shade(80)); + expect(zetaColors.surfaceAvatarYellow, ZetaColorBase.yellow.shade(50)); + expect(zetaColors.surfaceSecondarySubtle, ZetaColorBase.yellow.shade(10)); + expect(zetaColors.surfacePositiveSubtle, ZetaColorBase.green.shade(10)); + expect(zetaColors.surfaceWarningSubtle, ZetaColorBase.orange.shade(10)); + expect(zetaColors.surfaceNegativeSubtle, ZetaColorBase.red.shade(10)); + expect(zetaColors.surfaceInfoSubtle, ZetaColorBase.purple.shade(10)); + expect(zetaColors.borderDefault, ZetaColorBase.cool.shade(40)); + expect(zetaColors.borderSubtle, ZetaColorBase.cool.shade(30)); + expect(zetaColors.borderHover, ZetaColorBase.cool.shade(90)); + expect(zetaColors.borderSelected, ZetaColorBase.cool.shade(90)); + expect(zetaColors.borderDisabled, ZetaColorBase.cool.shade(20)); + expect(zetaColors.borderPure, ZetaColorBase.pure.shade(0)); + expect(zetaColors.borderPrimary, ZetaColorBase.blue.shade(50)); + expect(zetaColors.borderSecondary, ZetaColorBase.yellow.shade(50)); + expect(zetaColors.borderPositive, ZetaColorBase.green.shade(50)); + expect(zetaColors.borderWarning, ZetaColorBase.orange.shade(50)); + expect(zetaColors.borderNegative, ZetaColorBase.red.shade(50)); + expect(zetaColors.borderInfo, ZetaColorBase.purple.shade(50)); + expect(zetaColors.surfacePositive, ZetaColorBase.green); + expect(zetaColors.surfaceWarning, ZetaColorBase.orange); + expect(zetaColors.surfaceNegative, ZetaColorBase.red); + expect(zetaColors.surfaceAvatarGreen, ZetaColorBase.green); + expect(zetaColors.surfaceInfo, ZetaColorBase.purple); + expect(zetaColors.borderPrimaryMain, ZetaColorBase.blue); + }); + + test('deprecated properties return correct values', () { + final zetaColors = ZetaColors(); + + expect(zetaColors.surfaceHovered, zetaColors.surfaceHover); + expect(zetaColors.surfaceSelectedHovered, zetaColors.surfaceSelectedHover); + expect(zetaColors.positive, zetaColors.surfacePositive); + expect(zetaColors.negative, zetaColors.surfaceNegative); + expect(zetaColors.warning, zetaColors.surfaceWarning); + expect(zetaColors.info, zetaColors.surfaceInfo); + expect(zetaColors.shadow, const Color(0x1A49505E)); + expect(zetaColors.link, ZetaColorBase.linkLight); + expect(zetaColors.linkVisited, ZetaColorBase.linkVisitedLight); + }); + + test('props returns correct list of properties', () { + final zetaColors = ZetaColors(); + + expect( + zetaColors.props, + [ + zetaColors.brightness, + zetaColors.contrast, + zetaColors.primary, + zetaColors.secondary, + zetaColors.error, + zetaColors.cool, + zetaColors.warm, + zetaColors.white, + zetaColors.black, + zetaColors.surfacePrimary, + zetaColors.surfaceSecondary, + zetaColors.surfaceTertiary, + ], + ); + }); + }); + + group('ZetaColorGetters', () { + test('ColorScheme extension getters should return correct colors when scheme is ZetaColorScheme', () { + final zetaColors = ZetaColors(); + final themeData = ThemeData.light().copyWith(colorScheme: zetaColors.toScheme()); + expect(themeData.colorScheme.primarySwatch, zetaColors.primary); + expect(themeData.colorScheme.secondarySwatch, zetaColors.secondary); + expect(themeData.colorScheme.cool, zetaColors.cool); + expect(themeData.colorScheme.warm, zetaColors.warm); + expect(themeData.colorScheme.textDefault, zetaColors.textDefault); + expect(themeData.colorScheme.textSubtle, zetaColors.textSubtle); + expect(themeData.colorScheme.textDisabled, zetaColors.textDisabled); + expect(themeData.colorScheme.textInverse, zetaColors.textInverse); + expect(themeData.colorScheme.surfacePrimary, zetaColors.surfacePrimary); + expect(themeData.colorScheme.surfaceSecondary, zetaColors.surfaceSecondary); + expect(themeData.colorScheme.surfaceTertiary, zetaColors.surfaceTertiary); + expect(themeData.colorScheme.surfaceDisabled, zetaColors.surfaceDisabled); + expect(themeData.colorScheme.surfaceHover, zetaColors.surfaceHover); + expect(themeData.colorScheme.surfaceSelected, zetaColors.surfaceSelected); + expect(themeData.colorScheme.surfaceSelectedHover, zetaColors.surfaceSelectedHover); + expect(themeData.colorScheme.borderDefault, zetaColors.borderDefault); + expect(themeData.colorScheme.borderSubtle, zetaColors.borderSubtle); + expect(themeData.colorScheme.borderDisabled, zetaColors.borderDisabled); + expect(themeData.colorScheme.borderSelected, zetaColors.borderSelected); + expect(themeData.colorScheme.blue, zetaColors.blue); + expect(themeData.colorScheme.green, zetaColors.green); + expect(themeData.colorScheme.red, zetaColors.red); + expect(themeData.colorScheme.orange, zetaColors.orange); + expect(themeData.colorScheme.purple, zetaColors.purple); + expect(themeData.colorScheme.yellow, zetaColors.yellow); + expect(themeData.colorScheme.teal, zetaColors.teal); + expect(themeData.colorScheme.pink, zetaColors.pink); + expect(themeData.colorScheme.positive, zetaColors.green); + expect(themeData.colorScheme.negative, zetaColors.red); + expect(themeData.colorScheme.warning, zetaColors.orange); + expect(themeData.colorScheme.info, zetaColors.purple); + }); + + test('ColorScheme extension getters should return default colors when ZetaColorScheme scheme is not injected', () { + final themeData = ThemeData.light(); + expect(themeData.colorScheme.primarySwatch, ZetaColorBase.blue); + expect(themeData.colorScheme.secondarySwatch, ZetaColorBase.yellow); + expect(themeData.colorScheme.cool, ZetaColorBase.cool); + expect(themeData.colorScheme.warm, ZetaColorBase.warm); + expect(themeData.colorScheme.textDefault, ZetaColorBase.cool.shade90); + expect(themeData.colorScheme.textSubtle, ZetaColorBase.cool.shade70); + expect(themeData.colorScheme.textDisabled, ZetaColorBase.cool.shade50); + expect(themeData.colorScheme.textInverse, ZetaColorBase.cool.shade20); + expect(themeData.colorScheme.surfacePrimary, ZetaColorBase.white); + expect(themeData.colorScheme.surfaceSecondary, ZetaColorBase.cool.shade10); + expect(themeData.colorScheme.surfaceTertiary, ZetaColorBase.warm.shade10); + expect(themeData.colorScheme.surfaceDisabled, ZetaColorBase.cool.shade30); + expect(themeData.colorScheme.surfaceHover, ZetaColorBase.cool.shade20); + expect(themeData.colorScheme.surfaceSelected, ZetaColorBase.blue.shade10); + expect(themeData.colorScheme.surfaceSelectedHover, ZetaColorBase.blue.shade20); + expect(themeData.colorScheme.borderDefault, ZetaColorBase.cool.shade50); + expect(themeData.colorScheme.borderSubtle, ZetaColorBase.cool.shade40); + expect(themeData.colorScheme.borderDisabled, ZetaColorBase.cool.shade30); + expect(themeData.colorScheme.borderSelected, ZetaColorBase.cool.shade90); + expect(themeData.colorScheme.blue, ZetaColorBase.blue); + expect(themeData.colorScheme.green, ZetaColorBase.green); + expect(themeData.colorScheme.red, ZetaColorBase.red); + expect(themeData.colorScheme.orange, ZetaColorBase.orange); + expect(themeData.colorScheme.purple, ZetaColorBase.purple); + expect(themeData.colorScheme.yellow, ZetaColorBase.yellow); + expect(themeData.colorScheme.teal, ZetaColorBase.teal); + expect(themeData.colorScheme.pink, ZetaColorBase.pink); + expect(themeData.colorScheme.positive, ZetaColorBase.green); + expect(themeData.colorScheme.negative, ZetaColorBase.red); + expect(themeData.colorScheme.warning, ZetaColorBase.orange); + expect(themeData.colorScheme.info, ZetaColorBase.purple); + }); + }); +} diff --git a/test/src/theme/contrast_test.dart b/test/src/theme/contrast_test.dart new file mode 100644 index 00000000..fbb0ddc1 --- /dev/null +++ b/test/src/theme/contrast_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:zeta_flutter/src/theme/contrast.dart'; + +void main() { + group('ZetaContrast Accessibility Indices', () { + test('AA contrast indices', () { + expect(ZetaContrast.aa.primary, 60); + expect(ZetaContrast.aa.text, 60); + expect(ZetaContrast.aa.icon, 60); + expect(ZetaContrast.aa.hover, 70); + expect(ZetaContrast.aa.selected, 80); + expect(ZetaContrast.aa.focus, 80); + expect(ZetaContrast.aa.border, 60); + expect(ZetaContrast.aa.subtle, 40); + expect(ZetaContrast.aa.surface, 10); + expect(ZetaContrast.aa.targetContrast, 4.53); + }); + + test('AAA contrast indices', () { + expect(ZetaContrast.aaa.primary, 80); + expect(ZetaContrast.aaa.text, 80); + expect(ZetaContrast.aaa.icon, 80); + expect(ZetaContrast.aaa.hover, 90); + expect(ZetaContrast.aaa.selected, 100); + expect(ZetaContrast.aaa.focus, 100); + expect(ZetaContrast.aaa.border, 80); + expect(ZetaContrast.aaa.subtle, 60); + expect(ZetaContrast.aaa.surface, 10); + expect(ZetaContrast.aaa.targetContrast, 8.33); + }); + }); +} diff --git a/test/src/theme/theme_data_test.dart b/test/src/theme/theme_data_test.dart new file mode 100644 index 00000000..4ddeb3cb --- /dev/null +++ b/test/src/theme/theme_data_test.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zeta_flutter/src/theme/color_extensions.dart'; +import 'package:zeta_flutter/src/theme/contrast.dart'; +import 'package:zeta_flutter/src/theme/theme_data.dart'; + +void main() { + group('ZetaThemeData', () { + test('Default constructor should initialize with correct defaults', () { + final themeData = ZetaThemeData(); + + expect(themeData.fontFamily, kZetaFontFamily); + expect(themeData.identifier, 'default'); + expect(themeData.colorsLight.brightness, Brightness.light); + expect(themeData.colorsDark.brightness, Brightness.dark); + }); + + test('Constructor should initialize with custom values', () { + const customFontFamily = 'CustomFont'; + const customIdentifier = 'custom_theme'; + const customPrimary = Colors.blue; + const customSecondary = Colors.green; + + final themeData = ZetaThemeData( + fontFamily: customFontFamily, + identifier: customIdentifier, + primary: customPrimary, + secondary: customSecondary, + ); + + expect(themeData.fontFamily, customFontFamily); + expect(themeData.identifier, customIdentifier); + expect(themeData.colorsLight.primary, customPrimary.zetaColorSwatch); + expect(themeData.colorsLight.secondary, customSecondary.zetaColorSwatch); + expect(themeData.colorsDark.primary, customPrimary.zetaColorSwatch.apply(brightness: Brightness.dark)); + expect(themeData.colorsDark.secondary, customSecondary.zetaColorSwatch.apply(brightness: Brightness.dark)); + }); + + test('Apply method should return a new instance with updated contrast', () { + final themeData = ZetaThemeData(); + const newContrast = ZetaContrast.aaa; + final newThemeData = themeData.apply(contrast: newContrast); + + expect(newThemeData.colorsLight.contrast, newContrast); + expect(newThemeData.colorsDark.contrast, newContrast); + expect(newThemeData.fontFamily, themeData.fontFamily); + expect(newThemeData.identifier, themeData.identifier); + }); + + test('Equality and hashCode should work correctly', () { + final themeData1 = ZetaThemeData(); + final themeData2 = ZetaThemeData(); + + expect(themeData1, themeData2); + expect(themeData1.hashCode, themeData2.hashCode); + + final themeData3 = ZetaThemeData(identifier: 'different'); + expect(themeData1, isNot(themeData3)); + expect(themeData1.hashCode, isNot(themeData3.hashCode)); + }); + }); +} diff --git a/test/src/utils/debounce_test.dart b/test/src/utils/debounce_test.dart new file mode 100644 index 00000000..46a3e7fc --- /dev/null +++ b/test/src/utils/debounce_test.dart @@ -0,0 +1,73 @@ +// ignore_for_file: inference_failure_on_instance_creation + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class MockCallback extends Mock { + void call(); +} + +void main() { + group('ZetaDebounce', () { + late MockCallback mockCallback; + + setUp(() { + mockCallback = MockCallback(); + }); + + test('debounce calls the callback after the specified duration', () async { + const debounceDuration = Duration(milliseconds: 100); + ZetaDebounce(mockCallback.call, duration: debounceDuration); + + await Future.delayed(debounceDuration + const Duration(milliseconds: 50)); + + verify(mockCallback()).called(1); + }); + + test('debounce does not call the callback if cancelled', () async { + const debounceDuration = Duration(milliseconds: 100); + ZetaDebounce(mockCallback.call, duration: debounceDuration).cancel(); + + await Future.delayed(debounceDuration + const Duration(milliseconds: 50)); + + verifyNever(mockCallback()); + }); + + test('debounce restarts the timer if called again before duration ends', () async { + const debounceDuration = Duration(milliseconds: 100); + final debouncer = ZetaDebounce(mockCallback.call, duration: debounceDuration); + + await Future.delayed(const Duration(milliseconds: 50)); + debouncer.debounce(); + + await Future.delayed(const Duration(milliseconds: 75)); + verifyNever(mockCallback()); + + await Future.delayed(const Duration(milliseconds: 50)); + verify(mockCallback()).called(1); + }); + + test('ZetaDebounce.stopped does not start the timer automatically', () async { + const debounceDuration = Duration(milliseconds: 100); + ZetaDebounce.stopped(mockCallback.call, duration: debounceDuration); + + await Future.delayed(debounceDuration + const Duration(milliseconds: 50)); + + verifyNever(mockCallback()); + }); + + test('debounce can be called with a new callback', () async { + const debounceDuration = Duration(milliseconds: 100); + final debouncer = ZetaDebounce(mockCallback.call, duration: debounceDuration); + + final newCallback = MockCallback(); + debouncer.debounce(newCallback: newCallback.call); + + await Future.delayed(debounceDuration + const Duration(milliseconds: 50)); + + verifyNever(mockCallback()); + verify(newCallback()).called(1); + }); + }); +} diff --git a/test/src/utils/extensions_test.dart b/test/src/utils/extensions_test.dart new file mode 100644 index 00000000..e71bcfb6 --- /dev/null +++ b/test/src/utils/extensions_test.dart @@ -0,0 +1,244 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../test_utils/test_app.dart'; +import 'extensions_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), + MockSpec(), +]) +void main() { + group('ListDivider extension', () { + testWidgets('divide adds separator between widgets', (WidgetTester tester) async { + final widgets = [ + const Text('Widget1'), + const Text('Widget2'), + const Text('Widget3'), + ]; + + final dividedWidgets = widgets.divide(const Divider()).toList(); + + await tester.pumpWidget( + TestApp( + home: Column( + children: dividedWidgets, + ), + ), + ); + + expect(find.byType(Text), findsNWidgets(3)); + expect(find.byType(Divider), findsNWidgets(2)); + }); + + testWidgets('gap adds SizedBox of fixed width between widgets', (WidgetTester tester) async { + final widgets = [ + const Text('Widget1'), + const Text('Widget2'), + const Text('Widget3'), + ]; + + final gappedWidgets = widgets.gap(10); + + await tester.pumpWidget( + TestApp( + home: Column( + children: gappedWidgets, + ), + ), + ); + + expect(find.byType(Text), findsNWidgets(3)); + expect(find.byType(SizedBox), findsNWidgets(2)); + expect(tester.widget(find.byType(SizedBox).first).width, 10.0); + }); + }); + + group('SpacingWidget extension', () { + testWidgets('paddingAll adds padding to all sides', (WidgetTester tester) async { + final widget = const Text('Test').paddingAll(10); + + await tester.pumpWidget( + TestApp( + home: widget, + ), + ); + + final padding = tester.widget(find.byType(Padding)).padding as EdgeInsets; + expect(padding, const EdgeInsets.all(10)); + }); + + testWidgets('paddingStart adds padding to the start', (WidgetTester tester) async { + final widget = const Text('Test').paddingStart(10); + + await tester.pumpWidget( + TestApp( + home: Directionality( + textDirection: TextDirection.ltr, + child: widget, + ), + ), + ); + + final padding = tester.widget(find.byType(Padding)).padding as EdgeInsetsDirectional; + expect(padding.start, 10.0); + expect(padding.end, 0.0); + expect(padding.top, 0.0); + expect(padding.bottom, 0.0); + }); + + testWidgets('paddingEnd adds padding to the end', (WidgetTester tester) async { + final widget = const Text('Test').paddingEnd(10); + + await tester.pumpWidget( + TestApp( + home: Directionality( + textDirection: TextDirection.ltr, + child: widget, + ), + ), + ); + + final padding = tester.widget(find.byType(Padding)).padding as EdgeInsetsDirectional; + expect(padding.end, 10.0); + expect(padding.start, 0.0); + expect(padding.top, 0.0); + expect(padding.bottom, 0.0); + }); + + testWidgets('paddingTop adds padding to the top', (WidgetTester tester) async { + final widget = const Text('Test').paddingTop(10); + + await tester.pumpWidget( + TestApp( + home: Directionality( + textDirection: TextDirection.ltr, + child: widget, + ), + ), + ); + + final padding = tester.widget(find.byType(Padding)).padding as EdgeInsets; + expect(padding.top, 10.0); + expect(padding.left, 0.0); + expect(padding.right, 0.0); + expect(padding.bottom, 0.0); + }); + + testWidgets('paddingBottom adds padding to the bottom', (WidgetTester tester) async { + final widget = const Text('Test').paddingBottom(10); + + await tester.pumpWidget( + TestApp( + home: Directionality( + textDirection: TextDirection.ltr, + child: widget, + ), + ), + ); + + final padding = tester.widget(find.byType(Padding)).padding as EdgeInsets; + expect(padding.bottom, 10.0); + expect(padding.left, 0.0); + expect(padding.right, 0.0); + expect(padding.top, 0.0); + }); + + testWidgets('paddingVertical adds padding to the top & bottom', (WidgetTester tester) async { + final widget = const Text('Test').paddingVertical(10); + + await tester.pumpWidget( + TestApp( + home: Directionality( + textDirection: TextDirection.ltr, + child: widget, + ), + ), + ); + + final padding = tester.widget(find.byType(Padding)).padding as EdgeInsets; + expect(padding.bottom, 10.0); + expect(padding.top, 10.0); + expect(padding.left, 0.0); + expect(padding.right, 0.0); + }); + + testWidgets('paddingHorizontal adds padding to the left & right', (WidgetTester tester) async { + final widget = const Text('Test').paddingHorizontal(10); + + await tester.pumpWidget( + TestApp( + home: Directionality( + textDirection: TextDirection.ltr, + child: widget, + ), + ), + ); + + final padding = tester.widget(find.byType(Padding)).padding as EdgeInsets; + expect(padding.left, 10.0); + expect(padding.right, 10.0); + expect(padding.bottom, 0.0); + expect(padding.top, 0.0); + }); + }); + + group('NumExtensions extension', () { + test('formatMaxChars formats numbers correctly', () { + expect(123.formatMaxChars(2), '99+'); + expect(9.formatMaxChars(2), '9'); + expect(null.formatMaxChars(2), ''); + }); + }); + + group('ColorSwatches extension', () { + late MockBuildContext mockContext; + late MockZetaColors mockZetaColors; + late MockZeta mockZeta; + + setUp(() { + mockZeta = MockZeta(); + mockContext = MockBuildContext(); + mockZetaColors = MockZetaColors(); + when(mockContext.dependOnInheritedWidgetOfExactType()).thenReturn(mockZeta); + when(mockZeta.colors).thenReturn(mockZetaColors); + when(mockZetaColors.surfaceInfo).thenReturn(ZetaColorBase.purple); + when(mockZetaColors.surfacePositive).thenReturn(ZetaColorBase.green); + when(mockZetaColors.surfaceWarning).thenReturn(ZetaColorBase.orange); + when(mockZetaColors.surfaceNegative).thenReturn(ZetaColorBase.red); + when(mockZetaColors.cool).thenReturn(ZetaColorBase.cool); + }); + + test('colorSwatch returns correct color swatch for status', () { + expect(ZetaWidgetStatus.info.colorSwatch(mockContext), ZetaColorBase.purple); + expect(ZetaWidgetStatus.positive.colorSwatch(mockContext), ZetaColorBase.green); + expect(ZetaWidgetStatus.warning.colorSwatch(mockContext), ZetaColorBase.orange); + expect(ZetaWidgetStatus.negative.colorSwatch(mockContext), ZetaColorBase.red); + expect(ZetaWidgetStatus.neutral.colorSwatch(mockContext), ZetaColorBase.cool); + }); + }); + + group('StringExtensions extension', () { + test('initials returns correct initials', () { + expect('John Doe'.initials, 'JD'); + expect('A B C'.initials, 'AB'); + expect('Single'.initials, 'SI'); + expect('A'.initials, 'A'); + expect('a'.initials, 'A'); + expect(''.initials, ''); + expect(null.initials, ''); + }); + + test('capitalize capitalizes first letter', () { + expect('hello'.capitalize(), 'Hello'); + expect('HELLO'.capitalize(), 'Hello'); + expect('a'.capitalize(), 'A'); + expect(''.capitalize(), ''); + expect(null.capitalize(), ''); + }); + }); +} diff --git a/test/src/utils/extensions_test.mocks.dart b/test/src/utils/extensions_test.mocks.dart new file mode 100644 index 00000000..15ccab50 --- /dev/null +++ b/test/src/utils/extensions_test.mocks.dart @@ -0,0 +1,1724 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in zeta_flutter/test/src/utils/extensions_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:ui' as _i5; + +import 'package:flutter/foundation.dart' as _i3; +import 'package:flutter/material.dart' as _i2; +import 'package:flutter/src/widgets/notification_listener.dart' as _i8; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i11; +import 'package:zeta_flutter/src/theme/color_scheme.dart' as _i7; +import 'package:zeta_flutter/src/theme/color_swatch.dart' as _i4; +import 'package:zeta_flutter/src/theme/contrast.dart' as _i9; +import 'package:zeta_flutter/src/theme/theme_data.dart' as _i6; +import 'package:zeta_flutter/src/zeta.dart' as _i10; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeWidget_0 extends _i1.SmartFake implements _i2.Widget { + _FakeWidget_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => super.toString(); +} + +class _FakeInheritedWidget_1 extends _i1.SmartFake implements _i2.InheritedWidget { + _FakeInheritedWidget_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => super.toString(); +} + +class _FakeDiagnosticsNode_2 extends _i1.SmartFake implements _i3.DiagnosticsNode { + _FakeDiagnosticsNode_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({ + _i3.TextTreeConfiguration? parentConfiguration, + _i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info, + }) => + super.toString(); +} + +class _FakeZetaColorSwatch_3 extends _i1.SmartFake implements _i4.ZetaColorSwatch { + _FakeZetaColorSwatch_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeColor_4 extends _i1.SmartFake implements _i5.Color { + _FakeColor_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeZetaColors_5 extends _i1.SmartFake implements _i6.ZetaColors { + _FakeZetaColors_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeZetaColorScheme_6 extends _i1.SmartFake implements _i7.ZetaColorScheme { + _FakeZetaColorScheme_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => super.toString(); +} + +class _FakeZetaThemeData_7 extends _i1.SmartFake implements _i6.ZetaThemeData { + _FakeZetaThemeData_7( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeInheritedElement_8 extends _i1.SmartFake implements _i2.InheritedElement { + _FakeInheritedElement_8( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => super.toString(); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i2.BuildContext { + @override + _i2.Widget get widget => (super.noSuchMethod( + Invocation.getter(#widget), + returnValue: _FakeWidget_0( + this, + Invocation.getter(#widget), + ), + returnValueForMissingStub: _FakeWidget_0( + this, + Invocation.getter(#widget), + ), + ) as _i2.Widget); + + @override + bool get mounted => (super.noSuchMethod( + Invocation.getter(#mounted), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + bool get debugDoingBuild => (super.noSuchMethod( + Invocation.getter(#debugDoingBuild), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + _i2.InheritedWidget dependOnInheritedElement( + _i2.InheritedElement? ancestor, { + Object? aspect, + }) => + (super.noSuchMethod( + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + returnValue: _FakeInheritedWidget_1( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + returnValueForMissingStub: _FakeInheritedWidget_1( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) as _i2.InheritedWidget); + + @override + void visitAncestorElements(_i2.ConditionalElementVisitor? visitor) => super.noSuchMethod( + Invocation.method( + #visitAncestorElements, + [visitor], + ), + returnValueForMissingStub: null, + ); + + @override + void visitChildElements(_i2.ElementVisitor? visitor) => super.noSuchMethod( + Invocation.method( + #visitChildElements, + [visitor], + ), + returnValueForMissingStub: null, + ); + + @override + void dispatchNotification(_i8.Notification? notification) => super.noSuchMethod( + Invocation.method( + #dispatchNotification, + [notification], + ), + returnValueForMissingStub: null, + ); + + @override + _i3.DiagnosticsNode describeElement( + String? name, { + _i3.DiagnosticsTreeStyle? style = _i3.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method( + #describeElement, + [name], + {#style: style}, + ), + returnValue: _FakeDiagnosticsNode_2( + this, + Invocation.method( + #describeElement, + [name], + {#style: style}, + ), + ), + returnValueForMissingStub: _FakeDiagnosticsNode_2( + this, + Invocation.method( + #describeElement, + [name], + {#style: style}, + ), + ), + ) as _i3.DiagnosticsNode); + + @override + _i3.DiagnosticsNode describeWidget( + String? name, { + _i3.DiagnosticsTreeStyle? style = _i3.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method( + #describeWidget, + [name], + {#style: style}, + ), + returnValue: _FakeDiagnosticsNode_2( + this, + Invocation.method( + #describeWidget, + [name], + {#style: style}, + ), + ), + returnValueForMissingStub: _FakeDiagnosticsNode_2( + this, + Invocation.method( + #describeWidget, + [name], + {#style: style}, + ), + ), + ) as _i3.DiagnosticsNode); + + @override + List<_i3.DiagnosticsNode> describeMissingAncestor({required Type? expectedAncestorType}) => (super.noSuchMethod( + Invocation.method( + #describeMissingAncestor, + [], + {#expectedAncestorType: expectedAncestorType}, + ), + returnValue: <_i3.DiagnosticsNode>[], + returnValueForMissingStub: <_i3.DiagnosticsNode>[], + ) as List<_i3.DiagnosticsNode>); + + @override + _i3.DiagnosticsNode describeOwnershipChain(String? name) => (super.noSuchMethod( + Invocation.method( + #describeOwnershipChain, + [name], + ), + returnValue: _FakeDiagnosticsNode_2( + this, + Invocation.method( + #describeOwnershipChain, + [name], + ), + ), + returnValueForMissingStub: _FakeDiagnosticsNode_2( + this, + Invocation.method( + #describeOwnershipChain, + [name], + ), + ), + ) as _i3.DiagnosticsNode); +} + +/// A class which mocks [ZetaColors]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockZetaColors extends _i1.Mock implements _i6.ZetaColors { + @override + _i5.Brightness get brightness => (super.noSuchMethod( + Invocation.getter(#brightness), + returnValue: _i5.Brightness.dark, + returnValueForMissingStub: _i5.Brightness.dark, + ) as _i5.Brightness); + + @override + _i9.ZetaContrast get contrast => (super.noSuchMethod( + Invocation.getter(#contrast), + returnValue: _i9.ZetaContrast.aa, + returnValueForMissingStub: _i9.ZetaContrast.aa, + ) as _i9.ZetaContrast); + + @override + _i4.ZetaColorSwatch get primary => (super.noSuchMethod( + Invocation.getter(#primary), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#primary), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#primary), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i4.ZetaColorSwatch get secondary => (super.noSuchMethod( + Invocation.getter(#secondary), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#secondary), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#secondary), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i4.ZetaColorSwatch get error => (super.noSuchMethod( + Invocation.getter(#error), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#error), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#error), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i4.ZetaColorSwatch get cool => (super.noSuchMethod( + Invocation.getter(#cool), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#cool), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#cool), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i4.ZetaColorSwatch get warm => (super.noSuchMethod( + Invocation.getter(#warm), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#warm), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#warm), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i4.ZetaColorSwatch get pure => (super.noSuchMethod( + Invocation.getter(#pure), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#pure), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#pure), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i5.Color get white => (super.noSuchMethod( + Invocation.getter(#white), + returnValue: _FakeColor_4( + this, + Invocation.getter(#white), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#white), + ), + ) as _i5.Color); + + @override + _i5.Color get black => (super.noSuchMethod( + Invocation.getter(#black), + returnValue: _FakeColor_4( + this, + Invocation.getter(#black), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#black), + ), + ) as _i5.Color); + + @override + _i5.Color get surfacePrimary => (super.noSuchMethod( + Invocation.getter(#surfacePrimary), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfacePrimary), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfacePrimary), + ), + ) as _i5.Color); + + @override + _i5.Color get surfaceSecondary => (super.noSuchMethod( + Invocation.getter(#surfaceSecondary), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfaceSecondary), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfaceSecondary), + ), + ) as _i5.Color); + + @override + _i5.Color get surfaceTertiary => (super.noSuchMethod( + Invocation.getter(#surfaceTertiary), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfaceTertiary), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfaceTertiary), + ), + ) as _i5.Color); + + @override + _i4.ZetaColorSwatch get blue => (super.noSuchMethod( + Invocation.getter(#blue), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#blue), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#blue), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i4.ZetaColorSwatch get green => (super.noSuchMethod( + Invocation.getter(#green), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#green), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#green), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i4.ZetaColorSwatch get red => (super.noSuchMethod( + Invocation.getter(#red), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#red), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#red), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i4.ZetaColorSwatch get orange => (super.noSuchMethod( + Invocation.getter(#orange), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#orange), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#orange), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i4.ZetaColorSwatch get purple => (super.noSuchMethod( + Invocation.getter(#purple), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#purple), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#purple), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i4.ZetaColorSwatch get yellow => (super.noSuchMethod( + Invocation.getter(#yellow), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#yellow), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#yellow), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i4.ZetaColorSwatch get teal => (super.noSuchMethod( + Invocation.getter(#teal), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#teal), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#teal), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i4.ZetaColorSwatch get pink => (super.noSuchMethod( + Invocation.getter(#pink), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#pink), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#pink), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i5.Color get surfaceHovered => (super.noSuchMethod( + Invocation.getter(#surfaceHovered), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfaceHovered), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfaceHovered), + ), + ) as _i5.Color); + + @override + _i5.Color get surfaceSelectedHovered => (super.noSuchMethod( + Invocation.getter(#surfaceSelectedHovered), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfaceSelectedHovered), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfaceSelectedHovered), + ), + ) as _i5.Color); + + @override + _i4.ZetaColorSwatch get positive => (super.noSuchMethod( + Invocation.getter(#positive), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#positive), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#positive), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i4.ZetaColorSwatch get negative => (super.noSuchMethod( + Invocation.getter(#negative), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#negative), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#negative), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i4.ZetaColorSwatch get warning => (super.noSuchMethod( + Invocation.getter(#warning), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#warning), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#warning), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i4.ZetaColorSwatch get info => (super.noSuchMethod( + Invocation.getter(#info), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#info), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#info), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i5.Color get shadow => (super.noSuchMethod( + Invocation.getter(#shadow), + returnValue: _FakeColor_4( + this, + Invocation.getter(#shadow), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#shadow), + ), + ) as _i5.Color); + + @override + _i5.Color get link => (super.noSuchMethod( + Invocation.getter(#link), + returnValue: _FakeColor_4( + this, + Invocation.getter(#link), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#link), + ), + ) as _i5.Color); + + @override + _i5.Color get linkVisited => (super.noSuchMethod( + Invocation.getter(#linkVisited), + returnValue: _FakeColor_4( + this, + Invocation.getter(#linkVisited), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#linkVisited), + ), + ) as _i5.Color); + + @override + _i5.Color get textDefault => (super.noSuchMethod( + Invocation.getter(#textDefault), + returnValue: _FakeColor_4( + this, + Invocation.getter(#textDefault), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#textDefault), + ), + ) as _i5.Color); + + @override + _i5.Color get textSubtle => (super.noSuchMethod( + Invocation.getter(#textSubtle), + returnValue: _FakeColor_4( + this, + Invocation.getter(#textSubtle), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#textSubtle), + ), + ) as _i5.Color); + + @override + _i5.Color get textDisabled => (super.noSuchMethod( + Invocation.getter(#textDisabled), + returnValue: _FakeColor_4( + this, + Invocation.getter(#textDisabled), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#textDisabled), + ), + ) as _i5.Color); + + @override + _i5.Color get textInverse => (super.noSuchMethod( + Invocation.getter(#textInverse), + returnValue: _FakeColor_4( + this, + Invocation.getter(#textInverse), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#textInverse), + ), + ) as _i5.Color); + + @override + _i5.Color get iconDefault => (super.noSuchMethod( + Invocation.getter(#iconDefault), + returnValue: _FakeColor_4( + this, + Invocation.getter(#iconDefault), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#iconDefault), + ), + ) as _i5.Color); + + @override + _i5.Color get iconSubtle => (super.noSuchMethod( + Invocation.getter(#iconSubtle), + returnValue: _FakeColor_4( + this, + Invocation.getter(#iconSubtle), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#iconSubtle), + ), + ) as _i5.Color); + + @override + _i5.Color get iconDisabled => (super.noSuchMethod( + Invocation.getter(#iconDisabled), + returnValue: _FakeColor_4( + this, + Invocation.getter(#iconDisabled), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#iconDisabled), + ), + ) as _i5.Color); + + @override + _i5.Color get iconInverse => (super.noSuchMethod( + Invocation.getter(#iconInverse), + returnValue: _FakeColor_4( + this, + Invocation.getter(#iconInverse), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#iconInverse), + ), + ) as _i5.Color); + + @override + _i5.Color get surfaceDefault => (super.noSuchMethod( + Invocation.getter(#surfaceDefault), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfaceDefault), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfaceDefault), + ), + ) as _i5.Color); + + @override + _i5.Color get surfaceDefaultInverse => (super.noSuchMethod( + Invocation.getter(#surfaceDefaultInverse), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfaceDefaultInverse), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfaceDefaultInverse), + ), + ) as _i5.Color); + + @override + _i5.Color get surfaceHover => (super.noSuchMethod( + Invocation.getter(#surfaceHover), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfaceHover), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfaceHover), + ), + ) as _i5.Color); + + @override + _i5.Color get surfaceSelected => (super.noSuchMethod( + Invocation.getter(#surfaceSelected), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfaceSelected), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfaceSelected), + ), + ) as _i5.Color); + + @override + _i5.Color get surfaceSelectedHover => (super.noSuchMethod( + Invocation.getter(#surfaceSelectedHover), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfaceSelectedHover), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfaceSelectedHover), + ), + ) as _i5.Color); + + @override + _i5.Color get surfaceDisabled => (super.noSuchMethod( + Invocation.getter(#surfaceDisabled), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfaceDisabled), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfaceDisabled), + ), + ) as _i5.Color); + + @override + _i5.Color get surfaceCool => (super.noSuchMethod( + Invocation.getter(#surfaceCool), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfaceCool), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfaceCool), + ), + ) as _i5.Color); + + @override + _i5.Color get surfaceWarm => (super.noSuchMethod( + Invocation.getter(#surfaceWarm), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfaceWarm), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfaceWarm), + ), + ) as _i5.Color); + + @override + _i5.Color get surfacePrimarySubtle => (super.noSuchMethod( + Invocation.getter(#surfacePrimarySubtle), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfacePrimarySubtle), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfacePrimarySubtle), + ), + ) as _i5.Color); + + @override + _i5.Color get surfaceAvatarBlue => (super.noSuchMethod( + Invocation.getter(#surfaceAvatarBlue), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfaceAvatarBlue), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfaceAvatarBlue), + ), + ) as _i5.Color); + + @override + _i4.ZetaColorSwatch get surfaceAvatarGreen => (super.noSuchMethod( + Invocation.getter(#surfaceAvatarGreen), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#surfaceAvatarGreen), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#surfaceAvatarGreen), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i5.Color get surfaceAvatarOrange => (super.noSuchMethod( + Invocation.getter(#surfaceAvatarOrange), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfaceAvatarOrange), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfaceAvatarOrange), + ), + ) as _i5.Color); + + @override + _i5.Color get surfaceAvatarPink => (super.noSuchMethod( + Invocation.getter(#surfaceAvatarPink), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfaceAvatarPink), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfaceAvatarPink), + ), + ) as _i5.Color); + + @override + _i5.Color get surfaceAvatarPurple => (super.noSuchMethod( + Invocation.getter(#surfaceAvatarPurple), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfaceAvatarPurple), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfaceAvatarPurple), + ), + ) as _i5.Color); + + @override + _i5.Color get surfaceAvatarTeal => (super.noSuchMethod( + Invocation.getter(#surfaceAvatarTeal), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfaceAvatarTeal), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfaceAvatarTeal), + ), + ) as _i5.Color); + + @override + _i5.Color get surfaceAvatarYellow => (super.noSuchMethod( + Invocation.getter(#surfaceAvatarYellow), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfaceAvatarYellow), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfaceAvatarYellow), + ), + ) as _i5.Color); + + @override + _i5.Color get surfaceSecondarySubtle => (super.noSuchMethod( + Invocation.getter(#surfaceSecondarySubtle), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfaceSecondarySubtle), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfaceSecondarySubtle), + ), + ) as _i5.Color); + + @override + _i4.ZetaColorSwatch get surfacePositive => (super.noSuchMethod( + Invocation.getter(#surfacePositive), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#surfacePositive), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#surfacePositive), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i5.Color get surfacePositiveSubtle => (super.noSuchMethod( + Invocation.getter(#surfacePositiveSubtle), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfacePositiveSubtle), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfacePositiveSubtle), + ), + ) as _i5.Color); + + @override + _i4.ZetaColorSwatch get surfaceWarning => (super.noSuchMethod( + Invocation.getter(#surfaceWarning), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#surfaceWarning), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#surfaceWarning), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i5.Color get surfaceWarningSubtle => (super.noSuchMethod( + Invocation.getter(#surfaceWarningSubtle), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfaceWarningSubtle), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfaceWarningSubtle), + ), + ) as _i5.Color); + + @override + _i4.ZetaColorSwatch get surfaceNegative => (super.noSuchMethod( + Invocation.getter(#surfaceNegative), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#surfaceNegative), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#surfaceNegative), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i5.Color get surfaceNegativeSubtle => (super.noSuchMethod( + Invocation.getter(#surfaceNegativeSubtle), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfaceNegativeSubtle), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfaceNegativeSubtle), + ), + ) as _i5.Color); + + @override + _i4.ZetaColorSwatch get surfaceInfo => (super.noSuchMethod( + Invocation.getter(#surfaceInfo), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#surfaceInfo), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#surfaceInfo), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i5.Color get surfaceInfoSubtle => (super.noSuchMethod( + Invocation.getter(#surfaceInfoSubtle), + returnValue: _FakeColor_4( + this, + Invocation.getter(#surfaceInfoSubtle), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#surfaceInfoSubtle), + ), + ) as _i5.Color); + + @override + _i5.Color get borderDefault => (super.noSuchMethod( + Invocation.getter(#borderDefault), + returnValue: _FakeColor_4( + this, + Invocation.getter(#borderDefault), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#borderDefault), + ), + ) as _i5.Color); + + @override + _i5.Color get borderSubtle => (super.noSuchMethod( + Invocation.getter(#borderSubtle), + returnValue: _FakeColor_4( + this, + Invocation.getter(#borderSubtle), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#borderSubtle), + ), + ) as _i5.Color); + + @override + _i5.Color get borderHover => (super.noSuchMethod( + Invocation.getter(#borderHover), + returnValue: _FakeColor_4( + this, + Invocation.getter(#borderHover), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#borderHover), + ), + ) as _i5.Color); + + @override + _i5.Color get borderSelected => (super.noSuchMethod( + Invocation.getter(#borderSelected), + returnValue: _FakeColor_4( + this, + Invocation.getter(#borderSelected), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#borderSelected), + ), + ) as _i5.Color); + + @override + _i5.Color get borderDisabled => (super.noSuchMethod( + Invocation.getter(#borderDisabled), + returnValue: _FakeColor_4( + this, + Invocation.getter(#borderDisabled), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#borderDisabled), + ), + ) as _i5.Color); + + @override + _i5.Color get borderPure => (super.noSuchMethod( + Invocation.getter(#borderPure), + returnValue: _FakeColor_4( + this, + Invocation.getter(#borderPure), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#borderPure), + ), + ) as _i5.Color); + + @override + _i4.ZetaColorSwatch get borderPrimaryMain => (super.noSuchMethod( + Invocation.getter(#borderPrimaryMain), + returnValue: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#borderPrimaryMain), + ), + returnValueForMissingStub: _FakeZetaColorSwatch_3( + this, + Invocation.getter(#borderPrimaryMain), + ), + ) as _i4.ZetaColorSwatch); + + @override + _i5.Color get borderPrimary => (super.noSuchMethod( + Invocation.getter(#borderPrimary), + returnValue: _FakeColor_4( + this, + Invocation.getter(#borderPrimary), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#borderPrimary), + ), + ) as _i5.Color); + + @override + _i5.Color get borderSecondary => (super.noSuchMethod( + Invocation.getter(#borderSecondary), + returnValue: _FakeColor_4( + this, + Invocation.getter(#borderSecondary), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#borderSecondary), + ), + ) as _i5.Color); + + @override + _i5.Color get borderPositive => (super.noSuchMethod( + Invocation.getter(#borderPositive), + returnValue: _FakeColor_4( + this, + Invocation.getter(#borderPositive), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#borderPositive), + ), + ) as _i5.Color); + + @override + _i5.Color get borderWarning => (super.noSuchMethod( + Invocation.getter(#borderWarning), + returnValue: _FakeColor_4( + this, + Invocation.getter(#borderWarning), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#borderWarning), + ), + ) as _i5.Color); + + @override + _i5.Color get borderNegative => (super.noSuchMethod( + Invocation.getter(#borderNegative), + returnValue: _FakeColor_4( + this, + Invocation.getter(#borderNegative), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#borderNegative), + ), + ) as _i5.Color); + + @override + _i5.Color get borderInfo => (super.noSuchMethod( + Invocation.getter(#borderInfo), + returnValue: _FakeColor_4( + this, + Invocation.getter(#borderInfo), + ), + returnValueForMissingStub: _FakeColor_4( + this, + Invocation.getter(#borderInfo), + ), + ) as _i5.Color); + + @override + bool get isDarkMode => (super.noSuchMethod( + Invocation.getter(#isDarkMode), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + List<_i4.ZetaColorSwatch> get rainbow => (super.noSuchMethod( + Invocation.getter(#rainbow), + returnValue: <_i4.ZetaColorSwatch>[], + returnValueForMissingStub: <_i4.ZetaColorSwatch>[], + ) as List<_i4.ZetaColorSwatch>); + + @override + Map get rainbowMap => (super.noSuchMethod( + Invocation.getter(#rainbowMap), + returnValue: {}, + returnValueForMissingStub: {}, + ) as Map); + + @override + List get props => (super.noSuchMethod( + Invocation.getter(#props), + returnValue: [], + returnValueForMissingStub: [], + ) as List); + + @override + _i6.ZetaColors copyWith({ + _i5.Brightness? brightness, + _i9.ZetaContrast? contrast, + _i4.ZetaColorSwatch? primary, + _i4.ZetaColorSwatch? secondary, + _i4.ZetaColorSwatch? error, + _i4.ZetaColorSwatch? cool, + _i4.ZetaColorSwatch? warm, + _i5.Color? white, + _i5.Color? black, + _i5.Color? surfacePrimary, + _i5.Color? surfaceSecondary, + _i5.Color? surfaceTertiary, + _i5.Color? link, + _i5.Color? linkVisited, + _i5.Color? shadow, + }) => + (super.noSuchMethod( + Invocation.method( + #copyWith, + [], + { + #brightness: brightness, + #contrast: contrast, + #primary: primary, + #secondary: secondary, + #error: error, + #cool: cool, + #warm: warm, + #white: white, + #black: black, + #surfacePrimary: surfacePrimary, + #surfaceSecondary: surfaceSecondary, + #surfaceTertiary: surfaceTertiary, + #link: link, + #linkVisited: linkVisited, + #shadow: shadow, + }, + ), + returnValue: _FakeZetaColors_5( + this, + Invocation.method( + #copyWith, + [], + { + #brightness: brightness, + #contrast: contrast, + #primary: primary, + #secondary: secondary, + #error: error, + #cool: cool, + #warm: warm, + #white: white, + #black: black, + #surfacePrimary: surfacePrimary, + #surfaceSecondary: surfaceSecondary, + #surfaceTertiary: surfaceTertiary, + #link: link, + #linkVisited: linkVisited, + #shadow: shadow, + }, + ), + ), + returnValueForMissingStub: _FakeZetaColors_5( + this, + Invocation.method( + #copyWith, + [], + { + #brightness: brightness, + #contrast: contrast, + #primary: primary, + #secondary: secondary, + #error: error, + #cool: cool, + #warm: warm, + #white: white, + #black: black, + #surfacePrimary: surfacePrimary, + #surfaceSecondary: surfaceSecondary, + #surfaceTertiary: surfaceTertiary, + #link: link, + #linkVisited: linkVisited, + #shadow: shadow, + }, + ), + ), + ) as _i6.ZetaColors); + + @override + _i6.ZetaColors apply({required _i9.ZetaContrast? contrast}) => (super.noSuchMethod( + Invocation.method( + #apply, + [], + {#contrast: contrast}, + ), + returnValue: _FakeZetaColors_5( + this, + Invocation.method( + #apply, + [], + {#contrast: contrast}, + ), + ), + returnValueForMissingStub: _FakeZetaColors_5( + this, + Invocation.method( + #apply, + [], + {#contrast: contrast}, + ), + ), + ) as _i6.ZetaColors); + + @override + _i7.ZetaColorScheme toScheme() => (super.noSuchMethod( + Invocation.method( + #toScheme, + [], + ), + returnValue: _FakeZetaColorScheme_6( + this, + Invocation.method( + #toScheme, + [], + ), + ), + returnValueForMissingStub: _FakeZetaColorScheme_6( + this, + Invocation.method( + #toScheme, + [], + ), + ), + ) as _i7.ZetaColorScheme); +} + +/// A class which mocks [Zeta]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockZeta extends _i1.Mock implements _i10.Zeta { + @override + _i9.ZetaContrast get contrast => (super.noSuchMethod( + Invocation.getter(#contrast), + returnValue: _i9.ZetaContrast.aa, + returnValueForMissingStub: _i9.ZetaContrast.aa, + ) as _i9.ZetaContrast); + + @override + _i2.ThemeMode get themeMode => (super.noSuchMethod( + Invocation.getter(#themeMode), + returnValue: _i2.ThemeMode.system, + returnValueForMissingStub: _i2.ThemeMode.system, + ) as _i2.ThemeMode); + + @override + _i6.ZetaThemeData get themeData => (super.noSuchMethod( + Invocation.getter(#themeData), + returnValue: _FakeZetaThemeData_7( + this, + Invocation.getter(#themeData), + ), + returnValueForMissingStub: _FakeZetaThemeData_7( + this, + Invocation.getter(#themeData), + ), + ) as _i6.ZetaThemeData); + + @override + _i6.ZetaColors get colors => (super.noSuchMethod( + Invocation.getter(#colors), + returnValue: _FakeZetaColors_5( + this, + Invocation.getter(#colors), + ), + returnValueForMissingStub: _FakeZetaColors_5( + this, + Invocation.getter(#colors), + ), + ) as _i6.ZetaColors); + + @override + _i5.Brightness get brightness => (super.noSuchMethod( + Invocation.getter(#brightness), + returnValue: _i5.Brightness.dark, + returnValueForMissingStub: _i5.Brightness.dark, + ) as _i5.Brightness); + + @override + _i2.Widget get child => (super.noSuchMethod( + Invocation.getter(#child), + returnValue: _FakeWidget_0( + this, + Invocation.getter(#child), + ), + returnValueForMissingStub: _FakeWidget_0( + this, + Invocation.getter(#child), + ), + ) as _i2.Widget); + + @override + bool updateShouldNotify(_i2.InheritedWidget? oldWidget) => (super.noSuchMethod( + Invocation.method( + #updateShouldNotify, + [oldWidget], + ), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + void debugFillProperties(_i3.DiagnosticPropertiesBuilder? properties) => super.noSuchMethod( + Invocation.method( + #debugFillProperties, + [properties], + ), + returnValueForMissingStub: null, + ); + + @override + _i2.InheritedElement createElement() => (super.noSuchMethod( + Invocation.method( + #createElement, + [], + ), + returnValue: _FakeInheritedElement_8( + this, + Invocation.method( + #createElement, + [], + ), + ), + returnValueForMissingStub: _FakeInheritedElement_8( + this, + Invocation.method( + #createElement, + [], + ), + ), + ) as _i2.InheritedElement); + + @override + String toStringShort() => (super.noSuchMethod( + Invocation.method( + #toStringShort, + [], + ), + returnValue: _i11.dummyValue( + this, + Invocation.method( + #toStringShort, + [], + ), + ), + returnValueForMissingStub: _i11.dummyValue( + this, + Invocation.method( + #toStringShort, + [], + ), + ), + ) as String); + + @override + String toStringShallow({ + String? joiner = r', ', + _i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.debug, + }) => + (super.noSuchMethod( + Invocation.method( + #toStringShallow, + [], + { + #joiner: joiner, + #minLevel: minLevel, + }, + ), + returnValue: _i11.dummyValue( + this, + Invocation.method( + #toStringShallow, + [], + { + #joiner: joiner, + #minLevel: minLevel, + }, + ), + ), + returnValueForMissingStub: _i11.dummyValue( + this, + Invocation.method( + #toStringShallow, + [], + { + #joiner: joiner, + #minLevel: minLevel, + }, + ), + ), + ) as String); + + @override + String toStringDeep({ + String? prefixLineOne = r'', + String? prefixOtherLines, + _i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.debug, + }) => + (super.noSuchMethod( + Invocation.method( + #toStringDeep, + [], + { + #prefixLineOne: prefixLineOne, + #prefixOtherLines: prefixOtherLines, + #minLevel: minLevel, + }, + ), + returnValue: _i11.dummyValue( + this, + Invocation.method( + #toStringDeep, + [], + { + #prefixLineOne: prefixLineOne, + #prefixOtherLines: prefixOtherLines, + #minLevel: minLevel, + }, + ), + ), + returnValueForMissingStub: _i11.dummyValue( + this, + Invocation.method( + #toStringDeep, + [], + { + #prefixLineOne: prefixLineOne, + #prefixOtherLines: prefixOtherLines, + #minLevel: minLevel, + }, + ), + ), + ) as String); + + @override + _i3.DiagnosticsNode toDiagnosticsNode({ + String? name, + _i3.DiagnosticsTreeStyle? style, + }) => + (super.noSuchMethod( + Invocation.method( + #toDiagnosticsNode, + [], + { + #name: name, + #style: style, + }, + ), + returnValue: _FakeDiagnosticsNode_2( + this, + Invocation.method( + #toDiagnosticsNode, + [], + { + #name: name, + #style: style, + }, + ), + ), + returnValueForMissingStub: _FakeDiagnosticsNode_2( + this, + Invocation.method( + #toDiagnosticsNode, + [], + { + #name: name, + #style: style, + }, + ), + ), + ) as _i3.DiagnosticsNode); + + @override + List<_i3.DiagnosticsNode> debugDescribeChildren() => (super.noSuchMethod( + Invocation.method( + #debugDescribeChildren, + [], + ), + returnValue: <_i3.DiagnosticsNode>[], + returnValueForMissingStub: <_i3.DiagnosticsNode>[], + ) as List<_i3.DiagnosticsNode>); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => super.toString(); +} diff --git a/test/src/zeta_provider_test.dart b/test/src/zeta_provider_test.dart new file mode 100644 index 00000000..1d39bcf1 --- /dev/null +++ b/test/src/zeta_provider_test.dart @@ -0,0 +1,327 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import 'zeta_provider_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), +]) +void main() { + final mockThemeService = MockZetaThemeService(); + final initialThemeData = ZetaThemeData(); + + group('ZetaProvider', () { + testWidgets('initializes with correct default values', (WidgetTester tester) async { + await tester.pumpWidget( + ZetaProvider( + builder: (context, themeData, themeMode) => Container(), + ), + ); + + final providerState = tester.state(find.byType(ZetaProvider)); + expect(providerState.widget.initialThemeMode, ThemeMode.system); + expect(providerState.widget.initialContrast, ZetaContrast.aa); + expect(providerState.widget.initialThemeData, isNotNull); + }); + + testWidgets('initializes with provided values', (WidgetTester tester) async { + await tester.pumpWidget( + ZetaProvider( + builder: (context, themeData, themeMode) => Container(), + initialThemeMode: ThemeMode.light, + initialContrast: ZetaContrast.aaa, + initialThemeData: initialThemeData, + themeService: mockThemeService, + ), + ); + + final providerState = tester.state(find.byType(ZetaProvider)); + expect(providerState.widget.initialThemeMode, ThemeMode.light); + expect(providerState.widget.initialContrast, ZetaContrast.aaa); + expect(providerState.widget.initialThemeData, initialThemeData); + expect(providerState.widget.themeService, mockThemeService); + }); + + testWidgets('updateThemeMode updates the state correctly', (WidgetTester tester) async { + await tester.pumpWidget( + ZetaProvider( + builder: (context, themeData, themeMode) => Container(), + initialThemeData: initialThemeData, + themeService: mockThemeService, + ), + ); + + tester.state(find.byType(ZetaProvider)).updateThemeMode(ThemeMode.dark); + await tester.pump(); + + // Verifying through the public interface of Zeta widget + final zeta = tester.widget(find.byType(Zeta)); + expect(zeta.themeMode, ThemeMode.dark); + verify( + mockThemeService.saveTheme( + themeData: initialThemeData, + themeMode: ThemeMode.dark, + contrast: ZetaContrast.aa, + ), + ).called(1); + }); + + testWidgets('updateThemeData updates the state correctly', (WidgetTester tester) async { + final newThemeData = ZetaThemeData(); + + await tester.pumpWidget( + ZetaProvider( + builder: (context, themeData, themeMode) => Container(), + initialThemeData: initialThemeData, + themeService: mockThemeService, + ), + ); + + tester.state(find.byType(ZetaProvider)).updateThemeData(newThemeData); + await tester.pump(); + + // Verifying through the public interface of Zeta widget + final zeta = tester.widget(find.byType(Zeta)); + expect(zeta.themeData, newThemeData); + verify( + mockThemeService.saveTheme( + themeData: newThemeData, + themeMode: ThemeMode.system, + contrast: ZetaContrast.aa, + ), + ).called(1); + }); + + testWidgets('updateContrast updates the state correctly', (WidgetTester tester) async { + await tester.pumpWidget( + ZetaProvider( + builder: (context, themeData, themeMode) => Container(), + initialThemeData: initialThemeData, + themeService: mockThemeService, + ), + ); + + tester.state(find.byType(ZetaProvider)).updateContrast(ZetaContrast.aaa); + await tester.pump(); + + // Verifying through the public interface of Zeta widget + final zeta = tester.widget(find.byType(Zeta)); + expect(zeta.contrast, ZetaContrast.aaa); + verify( + mockThemeService.saveTheme( + themeData: initialThemeData.apply(contrast: ZetaContrast.aaa), + themeMode: ThemeMode.system, + contrast: ZetaContrast.aaa, + ), + ).called(1); + }); + + testWidgets('didUpdateWidget in ZetaProviderState works correctly with change in ThemeMode', + (WidgetTester tester) async { + await tester.pumpWidget( + ZetaProvider( + initialThemeMode: ThemeMode.light, + builder: (context, themeData, themeMode) => Builder( + builder: (context) { + return Container(); + }, + ), + ), + ); + + await tester.pumpAndSettle(); + // Verifying through the public interface of Zeta widget + expect(tester.widget(find.byType(Zeta)).themeMode, ThemeMode.light); + + await tester.pumpWidget( + ZetaProvider( + initialThemeMode: ThemeMode.dark, + builder: (context, themeData, themeMode) => Builder( + builder: (context) { + return Container(); + }, + ), + ), + ); + + await tester.pumpAndSettle(const Duration(milliseconds: 250)); + + // Verifying through the public interface of Zeta widget + expect(tester.widget(find.byType(Zeta)).themeMode, ThemeMode.dark); + }); + + testWidgets('didUpdateWidget in ZetaProviderState works correctly with change in contrast', + (WidgetTester tester) async { + await tester.pumpWidget( + ZetaProvider( + builder: (context, themeData, themeMode) => Builder( + builder: (context) { + return Container(); + }, + ), + ), + ); + + await tester.pumpAndSettle(); + + // Verifying through the public interface of Zeta widget + expect(tester.widget(find.byType(Zeta)).contrast, ZetaContrast.aa); + + await tester.pumpWidget( + ZetaProvider( + initialContrast: ZetaContrast.aaa, + builder: (context, themeData, themeMode) => Builder( + builder: (context) { + return Container(); + }, + ), + ), + ); + + await tester.pumpAndSettle(const Duration(milliseconds: 250)); + + // Verifying through the public interface of Zeta widget + expect(tester.widget(find.byType(Zeta)).contrast, ZetaContrast.aaa); + }); + + testWidgets('didUpdateWidget in ZetaProviderState works correctly with change in theme data', + (WidgetTester tester) async { + await tester.pumpWidget( + ZetaProvider( + initialThemeData: initialThemeData, + builder: (context, themeData, themeMode) => Builder( + builder: (context) { + return Container(); + }, + ), + ), + ); + + await tester.pumpAndSettle(); + + // Verifying through the public interface of Zeta widget + expect(tester.widget(find.byType(Zeta)).themeData.identifier, 'default'); + + await tester.pumpWidget( + ZetaProvider( + initialThemeData: ZetaThemeData(identifier: 'different'), + builder: (context, themeData, themeMode) => Builder( + builder: (context) { + return Container(); + }, + ), + ), + ); + + await tester.pumpAndSettle(const Duration(milliseconds: 250)); + + // Verifying through the public interface of Zeta widget + expect(tester.widget(find.byType(Zeta)).themeData.identifier, 'different'); + }); + + testWidgets('retrieves ZetaProviderState from context', (WidgetTester tester) async { + await tester.pumpWidget( + ZetaProvider( + builder: (context, themeData, themeMode) => Builder( + builder: (context) { + final providerState = ZetaProvider.of(context); + expect(providerState, isNotNull); + return Container(); + }, + ), + ), + ); + }); + + testWidgets('throws error if ZetaProvider is not found in widget tree', (WidgetTester tester) async { + await tester.pumpWidget( + Builder( + builder: (context) { + expect(() => ZetaProvider.of(context), throwsA(isA())); + return Container(); + }, + ), + ); + }); + + testWidgets('handles platform brightness changes', (WidgetTester tester) async { + await tester.pumpWidget( + ZetaProvider( + builder: (context, themeData, themeMode) => Container(), + initialThemeData: initialThemeData, + ), + ); + + // Rebuild the widget tree + await tester.pumpAndSettle(); + + // Get test binding + final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); + + // Simulate platform brightness change to dark + binding.platformDispatcher.platformBrightnessTestValue = Brightness.dark; + + await tester.pumpAndSettle(const Duration(milliseconds: 550)); + + // Verifying through the public interface of Zeta widget + final zeta = tester.widget(find.byType(Zeta)); + expect(zeta.brightness, Brightness.dark); + }); + + testWidgets('debugFillProperties works correctly', (WidgetTester tester) async { + final diagnostics = DiagnosticPropertiesBuilder(); + + ZetaProvider( + builder: (context, themeData, themeMode) => Container(), + initialThemeData: initialThemeData, + themeService: mockThemeService, + ).debugFillProperties(diagnostics); + + final description = + diagnostics.properties.where((p) => p.name == 'themeData').map((p) => p.toDescription()).first; + expect(description, contains('ZetaThemeData')); + + final themeMode = + diagnostics.properties.where((p) => p.name == 'initialThemeMode').map((p) => p.toDescription()).first; + expect(themeMode, 'system'); + + final contrast = + diagnostics.properties.where((p) => p.name == 'initialContrast').map((p) => p.toDescription()).first; + expect(contrast, 'aa'); + + final themeService = + diagnostics.properties.where((p) => p.name == 'themeService').map((p) => p.toDescription()).first; + expect(themeService, isNotNull); + }); + + testWidgets('debugFillProperties works correctly', (WidgetTester tester) async { + final diagnostics = DiagnosticPropertiesBuilder(); + + await tester.pumpWidget( + ZetaProvider( + builder: (context, themeData, themeMode) => Container(), + initialThemeData: initialThemeData, + ), + ); + + // Rebuild the widget tree + await tester.pumpAndSettle(); + + tester.state(find.byType(ZetaProvider)).debugFillProperties(diagnostics); + + final description = + diagnostics.properties.where((p) => p.name == 'themeData').map((p) => p.toDescription()).first; + expect(description, contains('ZetaThemeData')); + + final contrast = diagnostics.properties.where((p) => p.name == 'contrast').map((p) => p.toDescription()).first; + expect(contrast, 'aa'); + + final themeMode = diagnostics.properties.where((p) => p.name == 'themeMode').map((p) => p.toDescription()).first; + expect(themeMode, 'system'); + }); + }); +} diff --git a/test/src/zeta_provider_test.mocks.dart b/test/src/zeta_provider_test.mocks.dart new file mode 100644 index 00000000..04681f4b --- /dev/null +++ b/test/src/zeta_provider_test.mocks.dart @@ -0,0 +1,61 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in zeta_flutter/test/src/zeta_provider_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:flutter/material.dart' as _i5; +import 'package:mockito/mockito.dart' as _i1; +import 'package:zeta_flutter/src/theme/contrast.dart' as _i6; +import 'package:zeta_flutter/src/theme/theme_data.dart' as _i4; +import 'package:zeta_flutter/src/theme/theme_service.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [ZetaThemeService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockZetaThemeService extends _i1.Mock implements _i2.ZetaThemeService { + @override + _i3.Future<(_i4.ZetaThemeData?, _i5.ThemeMode?, _i6.ZetaContrast?)> loadTheme() => (super.noSuchMethod( + Invocation.method( + #loadTheme, + [], + ), + returnValue: _i3.Future<(_i4.ZetaThemeData?, _i5.ThemeMode?, _i6.ZetaContrast?)>.value((null, null, null)), + returnValueForMissingStub: + _i3.Future<(_i4.ZetaThemeData?, _i5.ThemeMode?, _i6.ZetaContrast?)>.value((null, null, null)), + ) as _i3.Future<(_i4.ZetaThemeData?, _i5.ThemeMode?, _i6.ZetaContrast?)>); + + @override + _i3.Future saveTheme({ + required _i4.ZetaThemeData? themeData, + required _i5.ThemeMode? themeMode, + required _i6.ZetaContrast? contrast, + }) => + (super.noSuchMethod( + Invocation.method( + #saveTheme, + [], + { + #themeData: themeData, + #themeMode: themeMode, + #contrast: contrast, + }, + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); +} diff --git a/test/src/zeta_test.dart b/test/src/zeta_test.dart new file mode 100644 index 00000000..5c165bec --- /dev/null +++ b/test/src/zeta_test.dart @@ -0,0 +1,174 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zeta_flutter/src/theme/contrast.dart'; +import 'package:zeta_flutter/src/theme/theme_data.dart'; +import 'package:zeta_flutter/src/zeta.dart'; + +void main() { + group('Zeta InheritedWidget', () { + testWidgets('provides correct colors in light mode', (WidgetTester tester) async { + final themeData = ZetaThemeData(); + + await tester.pumpWidget( + Zeta( + mediaBrightness: Brightness.light, + contrast: ZetaContrast.aa, + themeMode: ThemeMode.light, + themeData: themeData, + child: Container(), + ), + ); + + await tester.pumpAndSettle(); + + final zeta = Zeta.of(tester.element(find.byType(Container))); + expect(zeta.colors, themeData.colorsLight); + }); + + testWidgets('provides correct colors in dark mode', (WidgetTester tester) async { + final themeData = ZetaThemeData(); + + await tester.pumpWidget( + Zeta( + mediaBrightness: Brightness.dark, + contrast: ZetaContrast.aa, + themeMode: ThemeMode.dark, + themeData: themeData, + child: Container(), + ), + ); + + await tester.pumpAndSettle(); + + final zeta = Zeta.of(tester.element(find.byType(Container))); + expect(zeta.colors, themeData.colorsDark); + }); + + testWidgets('provides correct colors in system mode with light media brightness', (WidgetTester tester) async { + final themeData = ZetaThemeData(); + + await tester.pumpWidget( + Zeta( + mediaBrightness: Brightness.light, + contrast: ZetaContrast.aa, + themeMode: ThemeMode.system, + themeData: themeData, + child: Container(), + ), + ); + + await tester.pumpAndSettle(); + + final zeta = Zeta.of(tester.element(find.byType(Container))); + expect(zeta.colors, themeData.colorsLight); + }); + + testWidgets('provides correct colors in system mode with dark media brightness', (WidgetTester tester) async { + final themeData = ZetaThemeData(); + + await tester.pumpWidget( + Zeta( + mediaBrightness: Brightness.dark, + contrast: ZetaContrast.aa, + themeMode: ThemeMode.system, + themeData: themeData, + child: Container(), + ), + ); + + await tester.pumpAndSettle(); + + final zeta = Zeta.of(tester.element(find.byType(Container))); + expect(zeta.colors, themeData.colorsDark); + }); + + testWidgets('throws FlutterError if Zeta is not found in widget tree', (WidgetTester tester) async { + await tester.pumpWidget(Container()); + await tester.pumpAndSettle(); + expect(() => Zeta.of(tester.element(find.byType(Container))), throwsA(isA())); + }); + }); + + group('Zeta properties', () { + testWidgets('brightness getter works correctly', (WidgetTester tester) async { + final themeData = ZetaThemeData(); + + await tester.pumpWidget( + Zeta( + mediaBrightness: Brightness.light, + contrast: ZetaContrast.aa, + themeMode: ThemeMode.system, + themeData: themeData, + child: Container(), + ), + ); + + final zeta = Zeta.of(tester.element(find.byType(Container))); + expect(zeta.brightness, Brightness.light); + + await tester.pumpWidget( + Zeta( + mediaBrightness: Brightness.dark, + contrast: ZetaContrast.aa, + themeMode: ThemeMode.dark, + themeData: themeData, + child: Builder( + builder: (context) { + return Container(); + }, + ), + ), + ); + + final zetaDark = Zeta.of(tester.element(find.byType(Container))); + expect(zetaDark.brightness, Brightness.dark); + + await tester.pumpWidget( + Zeta( + mediaBrightness: Brightness.light, + contrast: ZetaContrast.aa, + themeMode: ThemeMode.light, + themeData: themeData, + child: Container(), + ), + ); + + final zetaLight = Zeta.of(tester.element(find.byType(Container))); + expect(zetaLight.brightness, Brightness.light); + }); + + testWidgets('debugFillProperties works correctly', (WidgetTester tester) async { + final themeData = ZetaThemeData(); + + final diagnostics = DiagnosticPropertiesBuilder(); + Zeta( + mediaBrightness: Brightness.light, + contrast: ZetaContrast.aa, + themeMode: ThemeMode.system, + themeData: themeData, + child: Container(), + ).debugFillProperties(diagnostics); + + final description = diagnostics.properties.where((p) => p.name == 'contrast').map((p) => p.toDescription()).first; + expect(description, 'aa'); + + final themeMode = diagnostics.properties.where((p) => p.name == 'themeMode').map((p) => p.toDescription()).first; + expect(themeMode, 'system'); + + final thData = diagnostics.properties.where((p) => p.name == 'themeData').map((p) => p.toDescription()).first; + expect(thData, contains('ZetaThemeData')); + + final colors = diagnostics.properties.where((p) => p.name == 'colors').map((p) => p.toDescription()).first; + expect(colors, contains('ZetaColors')); + + final mediaBrightness = + diagnostics.properties.where((p) => p.name == 'mediaBrightness').map((p) => p.toDescription()).first; + expect(mediaBrightness, 'light'); + + final brightness = + diagnostics.properties.where((p) => p.name == 'brightness').map((p) => p.toDescription()).first; + expect(brightness, 'light'); + }); + }); +} diff --git a/test/test_utils/test_app.dart b/test/test_utils/test_app.dart new file mode 100644 index 00000000..cbb5180b --- /dev/null +++ b/test/test_utils/test_app.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class TestApp extends StatelessWidget { + const TestApp({super.key, required this.home}); + final Widget home; + + @override + Widget build(BuildContext context) { + return ZetaProvider( + builder: (context, themeData, themeMode) { + return MaterialApp( + home: Builder( + builder: (context) { + return home; + }, + ), + ); + }, + ); + } +} diff --git a/test/test_utils/tolerant_comparator.dart b/test/test_utils/tolerant_comparator.dart new file mode 100644 index 00000000..7e5bdda4 --- /dev/null +++ b/test/test_utils/tolerant_comparator.dart @@ -0,0 +1,67 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:image/image.dart' as img; + +class TolerantComparator extends LocalFileComparator { + TolerantComparator(super.testFile, {this.tolerance = 0.0}); + + final double tolerance; + + @override + Future compare(Uint8List imageBytes, Uri golden) async { + final goldenFile = File.fromUri(golden); + if (!goldenFile.existsSync()) { + goldenFile + ..createSync(recursive: true) + ..writeAsBytesSync(imageBytes); + return true; + } + + final goldenBytes = goldenFile.readAsBytesSync(); + final testImage = img.decodeImage(imageBytes); + final goldenImage = img.decodeImage(goldenBytes); + + if (testImage == null || goldenImage == null) { + return false; + } + + return _compareImages(testImage, goldenImage); + } + + bool _compareImages(img.Image testImage, img.Image goldenImage) { + if (testImage.width != goldenImage.width || testImage.height != goldenImage.height) { + return false; + } + + int diffPixels = 0; + for (int y = 0; y < testImage.height; y++) { + for (int x = 0; x < testImage.width; x++) { + final testPixel = testImage.getPixel(x, y); + final goldenPixel = goldenImage.getPixel(x, y); + + if (!_isPixelWithinTolerance(testPixel, goldenPixel)) { + diffPixels++; + } + } + } + + final diffPercentage = diffPixels / (testImage.width * testImage.height); + return diffPercentage <= tolerance; + } + + bool _isPixelWithinTolerance(img.Pixel testPixel, img.Pixel goldenPixel) { + final tr = testPixel.r; + final tg = testPixel.g; + final tb = testPixel.b; + + final gr = goldenPixel.r; + final gg = goldenPixel.g; + final gb = goldenPixel.b; + + return (tr - gr).abs() <= 255 * tolerance && + (tg - gg).abs() <= 255 * tolerance && + (tb - gb).abs() <= 255 * tolerance; + } +}