From 21027d3fc7ad5745b315dce20576f354b641c2f5 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:18:09 +0000 Subject: [PATCH] CodeRabbit Generated Unit Tests: Add unit tests for PR changes --- .../test/app/gallery_app_test.dart | 150 +++++++ .../test/app/gallery_shell_test.dart | 317 ++++++++++++++ .../test/app/home_test.dart | 301 +++++++++++++ .../config/preview_configuration_test.dart | 259 +++++++++++ .../test/config/theme_configuration_test.dart | 408 ++++++++++++++++++ 5 files changed, 1435 insertions(+) create mode 100644 apps/design_system_gallery/test/app/gallery_app_test.dart create mode 100644 apps/design_system_gallery/test/app/gallery_shell_test.dart create mode 100644 apps/design_system_gallery/test/app/home_test.dart create mode 100644 apps/design_system_gallery/test/config/preview_configuration_test.dart create mode 100644 apps/design_system_gallery/test/config/theme_configuration_test.dart diff --git a/apps/design_system_gallery/test/app/gallery_app_test.dart b/apps/design_system_gallery/test/app/gallery_app_test.dart new file mode 100644 index 0000000..e706d47 --- /dev/null +++ b/apps/design_system_gallery/test/app/gallery_app_test.dart @@ -0,0 +1,150 @@ +import 'package:design_system_gallery/app/gallery_app.dart'; +import 'package:design_system_gallery/config/preview_configuration.dart'; +import 'package:design_system_gallery/config/theme_configuration.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; + +void main() { + group('StreamDesignSystemGallery', () { + testWidgets('renders without errors', (tester) async { + await tester.pumpWidget(const StreamDesignSystemGallery()); + expect(find.byType(StreamDesignSystemGallery), findsOneWidget); + }); + + testWidgets('creates stateful widget', (tester) async { + await tester.pumpWidget(const StreamDesignSystemGallery()); + final state = tester.state>( + find.byType(StreamDesignSystemGallery), + ); + expect(state, isNotNull); + }); + + testWidgets('provides ThemeConfiguration', (tester) async { + await tester.pumpWidget(const StreamDesignSystemGallery()); + await tester.pumpAndSettle(); + + final context = tester.element(find.byType(MaterialApp)); + final themeConfig = Provider.of(context, listen: false); + expect(themeConfig, isNotNull); + }); + + testWidgets('provides PreviewConfiguration', (tester) async { + await tester.pumpWidget(const StreamDesignSystemGallery()); + await tester.pumpAndSettle(); + + final context = tester.element(find.byType(MaterialApp)); + final previewConfig = Provider.of(context, listen: false); + expect(previewConfig, isNotNull); + }); + + testWidgets('creates MaterialApp', (tester) async { + await tester.pumpWidget(const StreamDesignSystemGallery()); + await tester.pumpAndSettle(); + expect(find.byType(MaterialApp), findsOneWidget); + }); + + testWidgets('MaterialApp has correct title', (tester) async { + await tester.pumpWidget(const StreamDesignSystemGallery()); + await tester.pumpAndSettle(); + + final materialApp = tester.widget(find.byType(MaterialApp)); + expect(materialApp.title, 'Stream Design System'); + }); + + testWidgets('MaterialApp has debugShowCheckedModeBanner disabled', (tester) async { + await tester.pumpWidget(const StreamDesignSystemGallery()); + await tester.pumpAndSettle(); + + final materialApp = tester.widget(find.byType(MaterialApp)); + expect(materialApp.debugShowCheckedModeBanner, isFalse); + }); + + testWidgets('disposes configurations on unmount', (tester) async { + await tester.pumpWidget(const StreamDesignSystemGallery()); + await tester.pumpAndSettle(); + + // Pump a different widget to trigger dispose + await tester.pumpWidget(const SizedBox()); + + // If we get here without errors, dispose worked correctly + expect(find.byType(StreamDesignSystemGallery), findsNothing); + }); + + testWidgets('initializes with light theme by default', (tester) async { + await tester.pumpWidget(const StreamDesignSystemGallery()); + await tester.pumpAndSettle(); + + final context = tester.element(find.byType(MaterialApp)); + final themeConfig = Provider.of(context, listen: false); + expect(themeConfig.brightness, Brightness.light); + }); + + testWidgets('can toggle theme panel', (tester) async { + await tester.pumpWidget(const StreamDesignSystemGallery()); + await tester.pumpAndSettle(); + + // Verify initial state (should be shown by default) + final stateBefore = tester.state(find.byType(StreamDesignSystemGallery)) + as dynamic; + expect(stateBefore.showThemePanel, isTrue); + }); + + testWidgets('MultiProvider wraps Consumer', (tester) async { + await tester.pumpWidget(const StreamDesignSystemGallery()); + await tester.pumpAndSettle(); + + expect(find.byType(MultiProvider), findsOneWidget); + expect(find.byType(Consumer), findsOneWidget); + }); + + testWidgets('handles theme changes', (tester) async { + await tester.pumpWidget(const StreamDesignSystemGallery()); + await tester.pumpAndSettle(); + + final context = tester.element(find.byType(MaterialApp)); + final themeConfig = Provider.of(context, listen: false); + + // Change to dark theme + themeConfig.brightness = Brightness.dark; + await tester.pumpAndSettle(); + + expect(themeConfig.brightness, Brightness.dark); + }); + + testWidgets('builds theme correctly', (tester) async { + await tester.pumpWidget(const StreamDesignSystemGallery()); + await tester.pumpAndSettle(); + + final materialApp = tester.widget(find.byType(MaterialApp)); + expect(materialApp.theme, isNotNull); + expect(materialApp.darkTheme, isNotNull); + }); + + testWidgets('sets correct theme mode for light theme', (tester) async { + await tester.pumpWidget(const StreamDesignSystemGallery()); + await tester.pumpAndSettle(); + + final context = tester.element(find.byType(MaterialApp)); + final themeConfig = Provider.of(context, listen: false); + themeConfig.brightness = Brightness.light; + await tester.pumpAndSettle(); + + final materialApp = tester.widget(find.byType(MaterialApp)); + expect(materialApp.themeMode, ThemeMode.light); + }); + + testWidgets('sets correct theme mode for dark theme', (tester) async { + await tester.pumpWidget(const StreamDesignSystemGallery()); + await tester.pumpAndSettle(); + + final context = tester.element(find.byType(MaterialApp)); + final themeConfig = Provider.of(context, listen: false); + themeConfig.brightness = Brightness.dark; + await tester.pumpAndSettle(); + + final materialApp = tester.widget(find.byType(MaterialApp)); + expect(materialApp.themeMode, ThemeMode.dark); + }); + }); +} \ No newline at end of file diff --git a/apps/design_system_gallery/test/app/gallery_shell_test.dart b/apps/design_system_gallery/test/app/gallery_shell_test.dart new file mode 100644 index 0000000..6e22663 --- /dev/null +++ b/apps/design_system_gallery/test/app/gallery_shell_test.dart @@ -0,0 +1,317 @@ +import 'package:design_system_gallery/app/gallery_shell.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; + +void main() { + group('GalleryShell', () { + Widget createTestableWidget({ + bool showThemePanel = true, + VoidCallback? onToggleThemePanel, + }) { + return MaterialApp( + theme: ThemeData(extensions: [StreamTheme()]), + home: GalleryShell( + showThemePanel: showThemePanel, + onToggleThemePanel: onToggleThemePanel ?? () {}, + ), + ); + } + + testWidgets('renders without errors', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.byType(GalleryShell), findsOneWidget); + }); + + testWidgets('creates Scaffold', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.byType(Scaffold), findsOneWidget); + }); + + testWidgets('shows toolbar', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.byType(GalleryToolbar), findsOneWidget); + }); + + testWidgets('toolbar receives showThemePanel prop', (tester) async { + await tester.pumpWidget(createTestableWidget(showThemePanel: true)); + final toolbar = tester.widget(find.byType(GalleryToolbar)); + expect(toolbar.showThemePanel, isTrue); + + await tester.pumpWidget(createTestableWidget(showThemePanel: false)); + final toolbar2 = tester.widget(find.byType(GalleryToolbar)); + expect(toolbar2.showThemePanel, isFalse); + }); + + testWidgets('toolbar receives onToggleThemePanel callback', (tester) async { + var callbackInvoked = false; + await tester.pumpWidget(createTestableWidget( + onToggleThemePanel: () => callbackInvoked = true, + )); + + final toolbar = tester.widget(find.byType(GalleryToolbar)); + toolbar.onToggleThemePanel(); + expect(callbackInvoked, isTrue); + }); + + testWidgets('contains theme customization panel', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.byType(ThemeCustomizationPanel), findsOneWidget); + }); + + testWidgets('theme panel is visible when showThemePanel is true', (tester) async { + await tester.pumpWidget(createTestableWidget(showThemePanel: true)); + await tester.pumpAndSettle(); + + final animatedSlide = tester.widget( + find.ancestor( + of: find.byType(ThemeCustomizationPanel), + matching: find.byType(AnimatedSlide), + ), + ); + expect(animatedSlide.offset, Offset.zero); + }); + + testWidgets('theme panel is hidden when showThemePanel is false', (tester) async { + await tester.pumpWidget(createTestableWidget(showThemePanel: false)); + await tester.pumpAndSettle(); + + final animatedSlide = tester.widget( + find.ancestor( + of: find.byType(ThemeCustomizationPanel), + matching: find.byType(AnimatedSlide), + ), + ); + expect(animatedSlide.offset, const Offset(1, 0)); + }); + + testWidgets('uses correct panel animation duration', (tester) async { + await tester.pumpWidget(createTestableWidget()); + + final animatedSlide = tester.widget( + find.ancestor( + of: find.byType(ThemeCustomizationPanel), + matching: find.byType(AnimatedSlide), + ), + ); + expect(animatedSlide.duration, kPanelAnimationDuration); + }); + + testWidgets('uses correct panel animation curve', (tester) async { + await tester.pumpWidget(createTestableWidget()); + + final animatedSlide = tester.widget( + find.ancestor( + of: find.byType(ThemeCustomizationPanel), + matching: find.byType(AnimatedSlide), + ), + ); + expect(animatedSlide.curve, Curves.easeInOut); + }); + + testWidgets('theme panel has correct width', (tester) async { + await tester.pumpWidget(createTestableWidget()); + + final sizedBox = tester.widget( + find.ancestor( + of: find.byType(ThemeCustomizationPanel), + matching: find.byType(SizedBox), + ), + ); + expect(sizedBox.width, kThemePanelWidth); + }); + + testWidgets('contains Column in Scaffold body', (tester) async { + await tester.pumpWidget(createTestableWidget()); + final scaffold = tester.widget(find.byType(Scaffold)); + expect(scaffold.body, isA()); + }); + + testWidgets('contains Stack for content area', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.byType(Stack), findsOneWidget); + }); + + testWidgets('contains Expanded widget', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.byType(Expanded), findsAtLeastNWidgets(1)); + }); + + testWidgets('applies padding when theme panel is shown on large screen', (tester) async { + // Set large screen size + tester.view.physicalSize = const Size(1920, 1080); + tester.view.devicePixelRatio = 1.0; + + await tester.pumpWidget(createTestableWidget(showThemePanel: true)); + await tester.pumpAndSettle(); + + final animatedPadding = tester.widget( + find.ancestor( + of: find.byType(Widgetbook), + matching: find.byType(AnimatedPadding), + ), + ); + expect(animatedPadding.padding.resolve(TextDirection.ltr).right, kThemePanelWidth); + + addTearDown(tester.view.reset); + }); + + testWidgets('removes padding when theme panel is hidden', (tester) async { + // Set large screen size + tester.view.physicalSize = const Size(1920, 1080); + tester.view.devicePixelRatio = 1.0; + + await tester.pumpWidget(createTestableWidget(showThemePanel: false)); + await tester.pumpAndSettle(); + + final animatedPadding = tester.widget( + find.ancestor( + of: find.byType(Widgetbook), + matching: find.byType(AnimatedPadding), + ), + ); + expect(animatedPadding.padding.resolve(TextDirection.ltr).right, 0); + + addTearDown(tester.view.reset); + }); + + testWidgets('uses overlay on small screens', (tester) async { + // Set small screen size + tester.view.physicalSize = const Size(600, 800); + tester.view.devicePixelRatio = 1.0; + + await tester.pumpWidget(createTestableWidget(showThemePanel: true)); + await tester.pumpAndSettle(); + + // On small screens with panel shown, padding should be 0 (overlay mode) + final animatedPadding = tester.widget( + find.ancestor( + of: find.byType(Widgetbook), + matching: find.byType(AnimatedPadding), + ), + ); + expect(animatedPadding.padding.resolve(TextDirection.ltr).right, 0); + + addTearDown(tester.view.reset); + }); + + test('kThemePanelWidth constant is defined', () { + expect(kThemePanelWidth, 340.0); + }); + + test('kWidgetbookDesktopBreakpoint constant is defined', () { + expect(kWidgetbookDesktopBreakpoint, 840.0); + }); + + test('kPanelAnimationDuration constant is defined', () { + expect(kPanelAnimationDuration, const Duration(milliseconds: 250)); + }); + }); + + group('_collapseDirectories', () { + testWidgets('collapses WidgetbookFolder nodes', (tester) async { + final folder = WidgetbookFolder( + name: 'Test Folder', + isInitiallyExpanded: true, + children: [], + ); + + final collapsed = _collapseNode(folder); + expect(collapsed, isA()); + expect((collapsed as WidgetbookFolder).isInitiallyExpanded, isFalse); + }); + + testWidgets('keeps WidgetbookCategory expanded', (tester) async { + final category = WidgetbookCategory( + name: 'Test Category', + children: [], + ); + + final collapsed = _collapseNode(category); + expect(collapsed, isA()); + expect((collapsed as WidgetbookCategory).name, 'Test Category'); + }); + + testWidgets('collapses WidgetbookComponent nodes', (tester) async { + final component = WidgetbookComponent( + name: 'Test Component', + isInitiallyExpanded: true, + useCases: [ + WidgetbookUseCase( + name: 'Test Use Case', + builder: (context) => const SizedBox(), + ), + ], + ); + + final collapsed = _collapseNode(component); + expect(collapsed, isA()); + expect((collapsed as WidgetbookComponent).isInitiallyExpanded, isFalse); + }); + + testWidgets('preserves use cases when collapsing component', (tester) async { + final component = WidgetbookComponent( + name: 'Test Component', + isInitiallyExpanded: true, + useCases: [ + WidgetbookUseCase( + name: 'Use Case 1', + builder: (context) => const SizedBox(), + ), + WidgetbookUseCase( + name: 'Use Case 2', + builder: (context) => const SizedBox(), + ), + ], + ); + + final collapsed = _collapseNode(component); + expect((collapsed as WidgetbookComponent).useCases.length, 2); + }); + + testWidgets('recursively collapses children', (tester) async { + final category = WidgetbookCategory( + name: 'Test Category', + children: [ + WidgetbookFolder( + name: 'Nested Folder', + isInitiallyExpanded: true, + children: [], + ), + ], + ); + + final collapsed = _collapseNode(category); + final nestedFolder = (collapsed as WidgetbookCategory).children!.first as WidgetbookFolder; + expect(nestedFolder.isInitiallyExpanded, isFalse); + }); + + testWidgets('handles empty children', (tester) async { + final folder = WidgetbookFolder( + name: 'Empty Folder', + isInitiallyExpanded: true, + children: [], + ); + + final collapsed = _collapseNode(folder); + expect((collapsed as WidgetbookFolder).children, isEmpty); + }); + + testWidgets('preserves node names', (tester) async { + final nodes = [ + WidgetbookFolder(name: 'Folder', isInitiallyExpanded: true, children: []), + WidgetbookCategory(name: 'Category', children: []), + WidgetbookComponent( + name: 'Component', + isInitiallyExpanded: true, + useCases: [], + ), + ]; + + for (final node in nodes) { + final collapsed = _collapseNode(node); + expect(collapsed.name, node.name); + } + }); + }); +} \ No newline at end of file diff --git a/apps/design_system_gallery/test/app/home_test.dart b/apps/design_system_gallery/test/app/home_test.dart new file mode 100644 index 0000000..bd6ef7a --- /dev/null +++ b/apps/design_system_gallery/test/app/home_test.dart @@ -0,0 +1,301 @@ +import 'package:design_system_gallery/app/home.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; + +void main() { + group('GalleryHomePage', () { + Widget createTestableWidget() { + return MaterialApp( + theme: ThemeData(extensions: [StreamTheme()]), + home: const GalleryHomePage(), + ); + } + + testWidgets('renders without errors', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.byType(GalleryHomePage), findsOneWidget); + }); + + testWidgets('displays correct title', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.text('Stream Design System'), findsOneWidget); + }); + + testWidgets('displays correct subtitle', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect( + find.text( + 'A comprehensive design system for building beautiful, ' + 'consistent chat and activity feed experiences.', + ), + findsOneWidget, + ); + }); + + testWidgets('displays Stream logo', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.byType(_StreamLogo), findsOneWidget); + }); + + testWidgets('displays feature chips', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.byType(_FeatureChips), findsOneWidget); + }); + + testWidgets('displays getting started hint', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.byType(_GettingStartedHint), findsOneWidget); + }); + + testWidgets('uses ColoredBox as background', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.byType(ColoredBox), findsAtLeastNWidgets(1)); + }); + + testWidgets('uses Center for layout', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.byType(Center), findsOneWidget); + }); + + testWidgets('uses SingleChildScrollView for scrolling', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.byType(SingleChildScrollView), findsOneWidget); + }); + + testWidgets('has constrained width', (tester) async { + await tester.pumpWidget(createTestableWidget()); + final constrainedBox = tester.widget( + find.ancestor( + of: find.byType(Column), + matching: find.byType(ConstrainedBox), + ).first, + ); + expect(constrainedBox.constraints.maxWidth, 560); + }); + + testWidgets('main column has correct alignment', (tester) async { + await tester.pumpWidget(createTestableWidget()); + final column = tester.widget( + find.descendant( + of: find.byType(ConstrainedBox), + matching: find.byType(Column), + ).first, + ); + expect(column.mainAxisAlignment, MainAxisAlignment.center); + }); + }); + + group('_StreamLogo', () { + Widget createTestableWidget() { + return MaterialApp( + theme: ThemeData(extensions: [StreamTheme()]), + home: const Scaffold(body: _StreamLogo()), + ); + } + + testWidgets('renders without errors', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.byType(_StreamLogo), findsOneWidget); + }); + + testWidgets('uses SvgIcon', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.byType(SvgIcon), findsOneWidget); + }); + + testWidgets('has correct size', (tester) async { + await tester.pumpWidget(createTestableWidget()); + final svgIcon = tester.widget(find.byType(SvgIcon)); + expect(svgIcon.size, 80); + }); + }); + + group('_FeatureChips', () { + Widget createTestableWidget() { + return MaterialApp( + theme: ThemeData(extensions: [StreamTheme()]), + home: const Scaffold(body: _FeatureChips()), + ); + } + + testWidgets('renders without errors', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.byType(_FeatureChips), findsOneWidget); + }); + + testWidgets('uses Wrap for layout', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.byType(Wrap), findsOneWidget); + }); + + testWidgets('Wrap has center alignment', (tester) async { + await tester.pumpWidget(createTestableWidget()); + final wrap = tester.widget(find.byType(Wrap)); + expect(wrap.alignment, WrapAlignment.center); + }); + + testWidgets('displays all feature chips', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.byType(_FeatureChip), findsNWidgets(4)); + }); + + testWidgets('displays Themeable chip', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.text('Themeable'), findsOneWidget); + }); + + testWidgets('displays Dark Mode chip', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.text('Dark Mode'), findsOneWidget); + }); + + testWidgets('displays Responsive chip', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.text('Responsive'), findsOneWidget); + }); + + testWidgets('displays Accessible chip', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.text('Accessible'), findsOneWidget); + }); + }); + + group('_FeatureChip', () { + Widget createTestableWidget({ + required IconData icon, + required String label, + }) { + return MaterialApp( + theme: ThemeData(extensions: [StreamTheme()]), + home: Scaffold( + body: _FeatureChip(icon: icon, label: label), + ), + ); + } + + testWidgets('renders without errors', (tester) async { + await tester.pumpWidget(createTestableWidget( + icon: Icons.palette_outlined, + label: 'Test', + )); + expect(find.byType(_FeatureChip), findsOneWidget); + }); + + testWidgets('displays icon', (tester) async { + await tester.pumpWidget(createTestableWidget( + icon: Icons.palette_outlined, + label: 'Test', + )); + expect(find.byIcon(Icons.palette_outlined), findsOneWidget); + }); + + testWidgets('displays label', (tester) async { + await tester.pumpWidget(createTestableWidget( + icon: Icons.palette_outlined, + label: 'Test Label', + )); + expect(find.text('Test Label'), findsOneWidget); + }); + + testWidgets('uses Container with decoration', (tester) async { + await tester.pumpWidget(createTestableWidget( + icon: Icons.palette_outlined, + label: 'Test', + )); + final container = tester.widget( + find.descendant( + of: find.byType(_FeatureChip), + matching: find.byType(Container), + ), + ); + expect(container.decoration, isA()); + }); + + testWidgets('uses Row for layout', (tester) async { + await tester.pumpWidget(createTestableWidget( + icon: Icons.palette_outlined, + label: 'Test', + )); + expect(find.byType(Row), findsOneWidget); + }); + + testWidgets('Row has min size', (tester) async { + await tester.pumpWidget(createTestableWidget( + icon: Icons.palette_outlined, + label: 'Test', + )); + final row = tester.widget(find.byType(Row)); + expect(row.mainAxisSize, MainAxisSize.min); + }); + + testWidgets('icon has correct size', (tester) async { + await tester.pumpWidget(createTestableWidget( + icon: Icons.palette_outlined, + label: 'Test', + )); + final icon = tester.widget(find.byIcon(Icons.palette_outlined)); + expect(icon.size, 16); + }); + }); + + group('_GettingStartedHint', () { + Widget createTestableWidget() { + return MaterialApp( + theme: ThemeData(extensions: [StreamTheme()]), + home: const Scaffold(body: _GettingStartedHint()), + ); + } + + testWidgets('renders without errors', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.byType(_GettingStartedHint), findsOneWidget); + }); + + testWidgets('displays hint text', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect( + find.text('Select a component from the sidebar to get started'), + findsOneWidget, + ); + }); + + testWidgets('displays arrow icon', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.byIcon(Icons.arrow_back), findsOneWidget); + }); + + testWidgets('uses Container with decoration', (tester) async { + await tester.pumpWidget(createTestableWidget()); + final container = tester.widget( + find.descendant( + of: find.byType(_GettingStartedHint), + matching: find.byType(Container), + ), + ); + expect(container.decoration, isA()); + }); + + testWidgets('uses Row for layout', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.byType(Row), findsOneWidget); + }); + + testWidgets('Row has min size', (tester) async { + await tester.pumpWidget(createTestableWidget()); + final row = tester.widget(find.byType(Row)); + expect(row.mainAxisSize, MainAxisSize.min); + }); + + testWidgets('text is flexible', (tester) async { + await tester.pumpWidget(createTestableWidget()); + expect(find.byType(Flexible), findsOneWidget); + }); + + testWidgets('icon has correct size', (tester) async { + await tester.pumpWidget(createTestableWidget()); + final icon = tester.widget(find.byIcon(Icons.arrow_back)); + expect(icon.size, 18); + }); + }); +} \ No newline at end of file diff --git a/apps/design_system_gallery/test/config/preview_configuration_test.dart b/apps/design_system_gallery/test/config/preview_configuration_test.dart new file mode 100644 index 0000000..2bb2b94 --- /dev/null +++ b/apps/design_system_gallery/test/config/preview_configuration_test.dart @@ -0,0 +1,259 @@ +import 'package:design_system_gallery/config/preview_configuration.dart'; +import 'package:device_frame_plus/device_frame_plus.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('PreviewConfiguration', () { + test('creates with default values', () { + final config = PreviewConfiguration(); + expect(config.selectedDevice, isNotNull); + expect(config.textScale, 1.0); + expect(config.showDeviceFrame, isFalse); + expect(config.textDirection, TextDirection.ltr); + expect(config.targetPlatform, isNull); + }); + + test('default device is iPhone13ProMax', () { + final config = PreviewConfiguration(); + expect(config.selectedDevice, Devices.ios.iPhone13ProMax); + }); + + group('setDevice', () { + test('updates device', () { + final config = PreviewConfiguration(); + final newDevice = Devices.ios.iPhone13Mini; + config.setDevice(newDevice); + expect(config.selectedDevice, newDevice); + }); + + test('notifies listeners', () { + final config = PreviewConfiguration(); + var notified = false; + config.addListener(() => notified = true); + config.setDevice(Devices.ios.iPhone13Mini); + expect(notified, isTrue); + }); + + test('does not notify if device is same', () { + final config = PreviewConfiguration(); + var notifyCount = 0; + config.addListener(() => notifyCount++); + config.setDevice(Devices.ios.iPhone13ProMax); + expect(notifyCount, 0); + }); + }); + + group('setTextScale', () { + test('updates text scale', () { + final config = PreviewConfiguration(); + config.setTextScale(1.5); + expect(config.textScale, 1.5); + }); + + test('notifies listeners', () { + final config = PreviewConfiguration(); + var notified = false; + config.addListener(() => notified = true); + config.setTextScale(1.5); + expect(notified, isTrue); + }); + + test('does not notify if scale is same', () { + final config = PreviewConfiguration(); + var notifyCount = 0; + config.addListener(() => notifyCount++); + config.setTextScale(1.0); + expect(notifyCount, 0); + }); + + test('handles extreme values', () { + final config = PreviewConfiguration(); + config.setTextScale(0.5); + expect(config.textScale, 0.5); + config.setTextScale(3.0); + expect(config.textScale, 3.0); + }); + }); + + group('toggleDeviceFrame', () { + test('toggles device frame visibility', () { + final config = PreviewConfiguration(); + expect(config.showDeviceFrame, isFalse); + config.toggleDeviceFrame(); + expect(config.showDeviceFrame, isTrue); + config.toggleDeviceFrame(); + expect(config.showDeviceFrame, isFalse); + }); + + test('notifies listeners', () { + final config = PreviewConfiguration(); + var notifyCount = 0; + config.addListener(() => notifyCount++); + config.toggleDeviceFrame(); + expect(notifyCount, 1); + }); + }); + + group('setTextDirection', () { + test('updates text direction', () { + final config = PreviewConfiguration(); + config.setTextDirection(TextDirection.rtl); + expect(config.textDirection, TextDirection.rtl); + }); + + test('notifies listeners', () { + final config = PreviewConfiguration(); + var notified = false; + config.addListener(() => notified = true); + config.setTextDirection(TextDirection.rtl); + expect(notified, isTrue); + }); + + test('does not notify if direction is same', () { + final config = PreviewConfiguration(); + var notifyCount = 0; + config.addListener(() => notifyCount++); + config.setTextDirection(TextDirection.ltr); + expect(notifyCount, 0); + }); + }); + + group('setTargetPlatform', () { + test('updates target platform', () { + final config = PreviewConfiguration(); + config.setTargetPlatform(TargetPlatform.android); + expect(config.targetPlatform, TargetPlatform.android); + }); + + test('notifies listeners', () { + final config = PreviewConfiguration(); + var notified = false; + config.addListener(() => notified = true); + config.setTargetPlatform(TargetPlatform.iOS); + expect(notified, isTrue); + }); + + test('does not notify if platform is same', () { + final config = PreviewConfiguration(); + var notifyCount = 0; + config.addListener(() => notifyCount++); + config.setTargetPlatform(null); + expect(notifyCount, 0); + }); + + test('can be set to null', () { + final config = PreviewConfiguration(); + config.setTargetPlatform(TargetPlatform.android); + config.setTargetPlatform(null); + expect(config.targetPlatform, isNull); + }); + + test('supports all platforms', () { + final config = PreviewConfiguration(); + for (final platform in [ + TargetPlatform.android, + TargetPlatform.iOS, + TargetPlatform.macOS, + TargetPlatform.windows, + TargetPlatform.linux, + ]) { + config.setTargetPlatform(platform); + expect(config.targetPlatform, platform); + } + }); + }); + + group('static options', () { + test('deviceOptions is not empty', () { + expect(PreviewConfiguration.deviceOptions, isNotEmpty); + }); + + test('deviceOptions contains expected devices', () { + final options = PreviewConfiguration.deviceOptions; + expect(options, contains(Devices.ios.iPhone13ProMax)); + expect(options, contains(Devices.ios.iPhone13Mini)); + expect(options, contains(Devices.ios.iPhoneSE)); + expect(options, contains(Devices.ios.iPad)); + expect(options, contains(Devices.android.samsungGalaxyS20)); + expect(options, contains(Devices.android.samsungGalaxyNote20)); + }); + + test('textScaleOptions is not empty', () { + expect(PreviewConfiguration.textScaleOptions, isNotEmpty); + }); + + test('textScaleOptions contains expected values', () { + expect(PreviewConfiguration.textScaleOptions, [0.85, 1, 1.15, 1.3, 2]); + }); + + test('textDirectionOptions contains both directions', () { + expect(PreviewConfiguration.textDirectionOptions, [ + TextDirection.rtl, + TextDirection.ltr, + ]); + }); + + test('platformOptions contains all platforms and null', () { + final options = PreviewConfiguration.platformOptions; + expect(options, contains(null)); + expect(options, contains(TargetPlatform.android)); + expect(options, contains(TargetPlatform.iOS)); + expect(options, contains(TargetPlatform.macOS)); + expect(options, contains(TargetPlatform.windows)); + expect(options, contains(TargetPlatform.linux)); + }); + }); + + group('edge cases', () { + test('multiple listeners are notified', () { + final config = PreviewConfiguration(); + var count1 = 0; + var count2 = 0; + config.addListener(() => count1++); + config.addListener(() => count2++); + config.setTextScale(1.5); + expect(count1, 1); + expect(count2, 1); + }); + + test('can be disposed', () { + final config = PreviewConfiguration(); + config.dispose(); + // Should not throw + }); + + test('setting same value multiple times', () { + final config = PreviewConfiguration(); + var notifyCount = 0; + config.addListener(() => notifyCount++); + config.setTextScale(1.0); + config.setTextScale(1.0); + config.setTextScale(1.0); + expect(notifyCount, 0); + }); + + test('rapid toggles', () { + final config = PreviewConfiguration(); + config.toggleDeviceFrame(); + config.toggleDeviceFrame(); + config.toggleDeviceFrame(); + expect(config.showDeviceFrame, isTrue); + }); + + test('chain method calls', () { + final config = PreviewConfiguration(); + config + ..setDevice(Devices.ios.iPad) + ..setTextScale(1.5) + ..setTextDirection(TextDirection.rtl) + ..setTargetPlatform(TargetPlatform.android); + + expect(config.selectedDevice, Devices.ios.iPad); + expect(config.textScale, 1.5); + expect(config.textDirection, TextDirection.rtl); + expect(config.targetPlatform, TargetPlatform.android); + }); + }); + }); +} \ No newline at end of file diff --git a/apps/design_system_gallery/test/config/theme_configuration_test.dart b/apps/design_system_gallery/test/config/theme_configuration_test.dart new file mode 100644 index 0000000..f7b2ade --- /dev/null +++ b/apps/design_system_gallery/test/config/theme_configuration_test.dart @@ -0,0 +1,408 @@ +import 'package:design_system_gallery/config/theme_configuration.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('ThemeConfiguration', () { + test('creates with default light brightness', () { + final config = ThemeConfiguration(); + expect(config.brightness, Brightness.light); + }); + + test('creates with dark brightness', () { + final config = ThemeConfiguration(brightness: Brightness.dark); + expect(config.brightness, Brightness.dark); + }); + + test('light factory creates light theme', () { + final config = ThemeConfiguration.light(); + expect(config.brightness, Brightness.light); + }); + + test('dark factory creates dark theme', () { + final config = ThemeConfiguration.dark(); + expect(config.brightness, Brightness.dark); + }); + + test('themeData is initialized', () { + final config = ThemeConfiguration(); + expect(config.themeData, isNotNull); + }); + + test('setBrightness changes brightness', () { + final config = ThemeConfiguration(); + config.setBrightness(Brightness.dark); + expect(config.brightness, Brightness.dark); + }); + + test('setBrightness notifies listeners', () { + final config = ThemeConfiguration(); + var notified = false; + config.addListener(() => notified = true); + config.setBrightness(Brightness.dark); + expect(notified, isTrue); + }); + + test('setBrightness with same value does not notify', () { + final config = ThemeConfiguration(); + var notifyCount = 0; + config.addListener(() => notifyCount++); + config.setBrightness(Brightness.light); + expect(notifyCount, 0); + }); + + group('accent colors', () { + test('setAccentPrimary updates and notifies', () { + final config = ThemeConfiguration(); + var notified = false; + config.addListener(() => notified = true); + config.setAccentPrimary(Colors.blue); + expect(config.accentPrimary, Colors.blue); + expect(notified, isTrue); + }); + + test('setAccentSuccess updates and notifies', () { + final config = ThemeConfiguration(); + var notified = false; + config.addListener(() => notified = true); + config.setAccentSuccess(Colors.green); + expect(config.accentSuccess, Colors.green); + expect(notified, isTrue); + }); + + test('setAccentWarning updates and notifies', () { + final config = ThemeConfiguration(); + config.setAccentWarning(Colors.orange); + expect(config.accentWarning, Colors.orange); + }); + + test('setAccentError updates and notifies', () { + final config = ThemeConfiguration(); + config.setAccentError(Colors.red); + expect(config.accentError, Colors.red); + }); + + test('setAccentNeutral updates and notifies', () { + final config = ThemeConfiguration(); + config.setAccentNeutral(Colors.grey); + expect(config.accentNeutral, Colors.grey); + }); + }); + + group('text colors', () { + test('setTextPrimary updates', () { + final config = ThemeConfiguration(); + config.setTextPrimary(Colors.black); + expect(config.textPrimary, Colors.black); + }); + + test('setTextSecondary updates', () { + final config = ThemeConfiguration(); + config.setTextSecondary(Colors.grey); + expect(config.textSecondary, Colors.grey); + }); + + test('setTextTertiary updates', () { + final config = ThemeConfiguration(); + config.setTextTertiary(Colors.grey); + expect(config.textTertiary, Colors.grey); + }); + + test('setTextDisabled updates', () { + final config = ThemeConfiguration(); + config.setTextDisabled(Colors.grey); + expect(config.textDisabled, Colors.grey); + }); + + test('setTextInverse updates', () { + final config = ThemeConfiguration(); + config.setTextInverse(Colors.white); + expect(config.textInverse, Colors.white); + }); + + test('setTextLink updates', () { + final config = ThemeConfiguration(); + config.setTextLink(Colors.blue); + expect(config.textLink, Colors.blue); + }); + + test('setTextOnAccent updates', () { + final config = ThemeConfiguration(); + config.setTextOnAccent(Colors.white); + expect(config.textOnAccent, Colors.white); + }); + }); + + group('background colors', () { + test('setBackgroundApp updates', () { + final config = ThemeConfiguration(); + config.setBackgroundApp(Colors.white); + expect(config.backgroundApp, Colors.white); + }); + + test('setBackgroundSurface updates', () { + final config = ThemeConfiguration(); + config.setBackgroundSurface(Colors.white); + expect(config.backgroundSurface, Colors.white); + }); + + test('setBackgroundSurfaceSubtle updates', () { + final config = ThemeConfiguration(); + config.setBackgroundSurfaceSubtle(Colors.grey.shade100); + expect(config.backgroundSurfaceSubtle, Colors.grey.shade100); + }); + + test('setBackgroundSurfaceStrong updates', () { + final config = ThemeConfiguration(); + config.setBackgroundSurfaceStrong(Colors.grey.shade200); + expect(config.backgroundSurfaceStrong, Colors.grey.shade200); + }); + + test('setBackgroundOverlay updates', () { + final config = ThemeConfiguration(); + config.setBackgroundOverlay(Colors.black.withAlpha(128)); + expect(config.backgroundOverlay.alpha, 128); + }); + + test('setBackgroundDisabled updates', () { + final config = ThemeConfiguration(); + config.setBackgroundDisabled(Colors.grey); + expect(config.backgroundDisabled, Colors.grey); + }); + }); + + group('border colors', () { + test('setBorderDefault updates', () { + final config = ThemeConfiguration(); + config.setBorderDefault(Colors.grey); + expect(config.borderDefault, Colors.grey); + }); + + test('setBorderSubtle updates', () { + final config = ThemeConfiguration(); + config.setBorderSubtle(Colors.grey.shade200); + expect(config.borderSubtle, Colors.grey.shade200); + }); + + test('setBorderStrong updates', () { + final config = ThemeConfiguration(); + config.setBorderStrong(Colors.grey.shade800); + expect(config.borderStrong, Colors.grey.shade800); + }); + + test('setBorderFocus updates', () { + final config = ThemeConfiguration(); + config.setBorderFocus(Colors.blue); + expect(config.borderFocus, Colors.blue); + }); + + test('setBorderError updates', () { + final config = ThemeConfiguration(); + config.setBorderError(Colors.red); + expect(config.borderError, Colors.red); + }); + }); + + group('state colors', () { + test('setStateHover updates', () { + final config = ThemeConfiguration(); + config.setStateHover(Colors.grey.shade100); + expect(config.stateHover.value, Colors.grey.shade100.value); + }); + + test('setStatePressed updates', () { + final config = ThemeConfiguration(); + config.setStatePressed(Colors.grey.shade200); + expect(config.statePressed.value, Colors.grey.shade200.value); + }); + + test('setStateSelected updates', () { + final config = ThemeConfiguration(); + config.setStateSelected(Colors.blue.shade100); + expect(config.stateSelected.value, Colors.blue.shade100.value); + }); + + test('setStateFocused updates', () { + final config = ThemeConfiguration(); + config.setStateFocused(Colors.blue.shade50); + expect(config.stateFocused.value, Colors.blue.shade50.value); + }); + + test('setStateDisabled updates', () { + final config = ThemeConfiguration(); + config.setStateDisabled(Colors.grey.shade300); + expect(config.stateDisabled.value, Colors.grey.shade300.value); + }); + }); + + group('system colors', () { + test('setSystemText updates', () { + final config = ThemeConfiguration(); + config.setSystemText(Colors.black); + expect(config.systemText, Colors.black); + }); + + test('setSystemScrollbar updates', () { + final config = ThemeConfiguration(); + config.setSystemScrollbar(Colors.grey); + expect(config.systemScrollbar, Colors.grey); + }); + }); + + group('brand color', () { + test('setBrandPrimaryColor updates', () { + final config = ThemeConfiguration(); + config.setBrandPrimaryColor(Colors.purple); + expect(config.brandPrimaryColor, Colors.purple); + }); + + test('brand color affects accent primary', () { + final config = ThemeConfiguration(); + config.setBrandPrimaryColor(Colors.purple); + expect(config.accentPrimary, Colors.purple); + }); + }); + + group('resetToDefaults', () { + test('resets all customizations', () { + final config = ThemeConfiguration(); + config.setAccentPrimary(Colors.blue); + config.setTextPrimary(Colors.black); + config.setBorderDefault(Colors.grey); + + config.resetToDefaults(); + + // Values should be reset to defaults + expect(config.themeData, isNotNull); + }); + + test('notifies listeners on reset', () { + final config = ThemeConfiguration(); + var notified = false; + config.addListener(() => notified = true); + config.resetToDefaults(); + expect(notified, isTrue); + }); + }); + + group('buildMaterialTheme', () { + test('creates ThemeData', () { + final config = ThemeConfiguration(); + final themeData = config.buildMaterialTheme(); + expect(themeData, isNotNull); + }); + + test('has correct brightness', () { + final lightConfig = ThemeConfiguration.light(); + final darkConfig = ThemeConfiguration.dark(); + + expect(lightConfig.buildMaterialTheme().brightness, Brightness.light); + expect(darkConfig.buildMaterialTheme().brightness, Brightness.dark); + }); + + test('enables Material 3', () { + final config = ThemeConfiguration(); + final themeData = config.buildMaterialTheme(); + expect(themeData.useMaterial3, isTrue); + }); + + test('includes StreamTheme extension', () { + final config = ThemeConfiguration(); + final themeData = config.buildMaterialTheme(); + expect(themeData.extensions.isNotEmpty, isTrue); + }); + + test('sets scaffold background color', () { + final config = ThemeConfiguration(); + final themeData = config.buildMaterialTheme(); + expect(themeData.scaffoldBackgroundColor, isNotNull); + }); + + test('configures dialog theme', () { + final config = ThemeConfiguration(); + final themeData = config.buildMaterialTheme(); + expect(themeData.dialogTheme, isNotNull); + expect(themeData.dialogTheme.backgroundColor, isNotNull); + }); + + test('configures appBar theme', () { + final config = ThemeConfiguration(); + final themeData = config.buildMaterialTheme(); + expect(themeData.appBarTheme, isNotNull); + expect(themeData.appBarTheme.elevation, 0); + }); + + test('configures button themes', () { + final config = ThemeConfiguration(); + final themeData = config.buildMaterialTheme(); + expect(themeData.filledButtonTheme, isNotNull); + expect(themeData.outlinedButtonTheme, isNotNull); + expect(themeData.textButtonTheme, isNotNull); + expect(themeData.elevatedButtonTheme, isNotNull); + }); + + test('configures input decoration theme', () { + final config = ThemeConfiguration(); + final themeData = config.buildMaterialTheme(); + expect(themeData.inputDecorationTheme, isNotNull); + expect(themeData.inputDecorationTheme.filled, isTrue); + }); + + test('configures scrollbar theme', () { + final config = ThemeConfiguration(); + final themeData = config.buildMaterialTheme(); + expect(themeData.scrollbarTheme, isNotNull); + }); + + test('configures text theme', () { + final config = ThemeConfiguration(); + final themeData = config.buildMaterialTheme(); + expect(themeData.textTheme, isNotNull); + expect(themeData.textTheme.bodyMedium, isNotNull); + }); + + test('custom colors are applied', () { + final config = ThemeConfiguration(); + config.setAccentPrimary(Colors.purple); + final themeData = config.buildMaterialTheme(); + expect(themeData.colorScheme.primary, Colors.purple); + }); + }); + + group('edge cases', () { + test('setting same brightness twice', () { + final config = ThemeConfiguration(); + var notifyCount = 0; + config.addListener(() => notifyCount++); + config.setBrightness(Brightness.light); + config.setBrightness(Brightness.light); + expect(notifyCount, 0); + }); + + test('multiple listeners are notified', () { + final config = ThemeConfiguration(); + var count1 = 0; + var count2 = 0; + config.addListener(() => count1++); + config.addListener(() => count2++); + config.setAccentPrimary(Colors.blue); + expect(count1, 1); + expect(count2, 1); + }); + + test('can be disposed', () { + final config = ThemeConfiguration(); + config.dispose(); + // Should not throw + }); + + test('multiple color updates trigger single rebuild', () { + final config = ThemeConfiguration(); + var notifyCount = 0; + config.addListener(() => notifyCount++); + config.setAccentPrimary(Colors.blue); + expect(notifyCount, 1); + }); + }); + }); +} \ No newline at end of file