import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:material_3_expressive/material_3_expressive.dart'; import 'package:material_ui/material_ui.dart' as material_ui; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/theme/app_theme.dart'; import 'package:spotiflac_android/theme/material_expressive.dart'; import 'package:spotiflac_android/theme/mornye_theme.dart'; import 'package:spotiflac_android/widgets/app_action_button.dart'; import 'package:spotiflac_android/widgets/app_choice_chip.dart'; import 'package:spotiflac_android/widgets/app_content_card.dart'; import 'package:spotiflac_android/widgets/app_switch.dart'; import 'package:spotiflac_android/widgets/expressive_button.dart'; import 'package:spotiflac_android/widgets/expressive_icon_button.dart'; import 'package:spotiflac_android/widgets/expressive_navigation_bar.dart'; import 'package:spotiflac_android/widgets/selection_action_button.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; Widget _host( Widget child, { ThemeData? theme, bool reduceMotion = false, double textScale = 1, Locale locale = const Locale('en'), }) => ProviderScope( child: MaterialApp( theme: theme ?? AppTheme.light(), locale: locale, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, builder: (context, child) => MediaQuery( data: MediaQuery.of(context).copyWith( disableAnimations: reduceMotion, textScaler: TextScaler.linear(textScale), ), child: child!, ), home: Scaffold(body: Center(child: child)), ), ); void main() { testWidgets('bridge retains dynamic colors, AMOLED, font and locale', ( tester, ) async { for (final systemFont in [false, true]) { final scheme = ColorScheme.fromSeed( seedColor: Colors.green, brightness: Brightness.dark, ).copyWith(primary: const Color(0xffc6f192)); final theme = AppTheme.dark( dynamicScheme: scheme, isAmoled: true, useSystemFont: systemFont, ); late material_ui.ThemeData adapted; late M3EThemeData expressive; late ThemeData host; late String adaptedCancel; late String hostCancel; await tester.pumpWidget( _host( MaterialExpressiveScope( child: Builder( builder: (context) { adapted = material_ui.Theme.of(context); expressive = M3ETheme.of(context); host = Theme.of(context); adaptedCancel = material_ui.MaterialLocalizations.of( context, ).cancelButtonLabel; hostCancel = MaterialLocalizations.of( context, ).cancelButtonLabel; return const SizedBox(); }, ), ), theme: theme, locale: const Locale('id'), ), ); await tester.pumpAndSettle(); expect(adapted.colorScheme.primary, scheme.primary); expect(adapted.colorScheme.primaryFixed, scheme.primaryFixed); expect(adapted.colorScheme.surface, scheme.surface); expect(adapted.scaffoldBackgroundColor, Colors.black); expect( expressive.typeScale.bodyMedium.fontFamily, theme.textTheme.bodyMedium!.fontFamily, ); expect(host.colorScheme, scheme); expect(adaptedCancel, hostCancel); expect(tester.takeException(), isNull); } }); testWidgets('switch row and thumb toggle once and support keyboard', ( tester, ) async { var value = false; var calls = 0; await tester.pumpWidget( _host( StatefulBuilder( builder: (context, setState) { return AppSwitchListTile( title: const Text('Autoplay'), value: value, onChanged: (next) => setState(() { value = next; calls++; }), ); }, ), ), ); await tester.pumpAndSettle(); await tester.tap(find.text('Autoplay')); await tester.pumpAndSettle(); expect(value, isTrue); expect(calls, 1); await tester.tap(find.byType(M3ESwitch)); await tester.pumpAndSettle(); expect(value, isFalse); expect(calls, 2); await tester.sendKeyEvent(LogicalKeyboardKey.tab); await tester.sendKeyEvent(LogicalKeyboardKey.space); await tester.pumpAndSettle(); expect(calls, 3); expect(tester.takeException(), isNull); }); testWidgets('chips preserve select and deselect callbacks', (tester) async { var selected = false; await tester.pumpWidget( _host( StatefulBuilder( builder: (context, setState) => AppChoiceChip( label: const Text('Albums'), selected: selected, singleChoice: true, onSelected: (next) => setState(() => selected = next), ), ), ), ); await tester.pumpAndSettle(); await tester.tap(find.text('Albums')); await tester.pumpAndSettle(); expect(selected, isTrue); await tester.tap(find.text('Albums')); await tester.pumpAndSettle(); expect(selected, isFalse); }); testWidgets('actions respect disabled state, custom colors and touch size', ( tester, ) async { var enabled = false; var calls = 0; late StateSetter update; await tester.pumpWidget( _host( StatefulBuilder( builder: (context, setState) { update = setState; return ExpressiveButton( onPressed: enabled ? () => calls++ : null, style: FilledButton.styleFrom( backgroundColor: Colors.teal, foregroundColor: Colors.white, ), child: const Text('Play'), ); }, ), ), ); await tester.pumpAndSettle(); await tester.tap(find.text('Play')); expect(calls, 0); update(() => enabled = true); await tester.pumpAndSettle(); final button = tester.widget(find.byType(M3EButton)); expect(button.decoration!.backgroundColor!.resolve({}), Colors.teal); expect( tester.getSize(find.byType(M3EButton)).height, greaterThanOrEqualTo(48), ); await tester.tap(find.text('Play')); expect(calls, 1); }); testWidgets('large labels fit narrow settings and selection actions', ( tester, ) async { tester.view.physicalSize = const Size(320, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget( _host( Column( mainAxisSize: MainAxisSize.min, children: [ SettingsGroup( children: [ AppSwitchListTile( value: true, onChanged: (_) {}, title: const Text('Continue playing music from your Library'), ), ], ), Row( children: [ for (final label in [ 'Remove ReplayGain (10)', 'Convert 10 tracks', ]) Expanded( child: SelectionActionButton( icon: Icons.music_note, label: label, onPressed: () {}, colorScheme: AppTheme.light().colorScheme, ), ), ], ), ], ), textScale: 2, ), ); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); }); testWidgets('navigation preserves badges, RTL order and safe area', ( tester, ) async { final semantics = tester.ensureSemantics(); var selected = 0; await tester.pumpWidget( _host( StatefulBuilder( builder: (context, setState) => Directionality( textDirection: TextDirection.rtl, child: MediaQuery( data: MediaQuery.of( context, ).copyWith(padding: const EdgeInsets.only(bottom: 24)), child: ExpressiveNavigationBar( selectedIndex: selected, onDestinationSelected: (index) => setState(() => selected = index), backgroundColor: Colors.black, destinations: const [ NavigationDestination(icon: Icon(Icons.home), label: 'Home'), NavigationDestination( icon: Badge(label: Text('3'), child: Icon(Icons.download)), label: 'Downloads', ), NavigationDestination( icon: Icon(Icons.settings), label: 'Settings', ), ], ), ), ), ), ), ); await tester.pumpAndSettle(); expect(tester.getSize(find.byType(ExpressiveNavigationBar)).height, 88); expect(find.text('3'), findsOneWidget); expect(find.bySemanticsLabel('Home'), findsOneWidget); expect(find.bySemanticsLabel('Home\nHome'), findsNothing); expect( tester.getCenter(find.text('Home')).dx, greaterThan(tester.getCenter(find.text('Settings')).dx), ); await tester.tap(find.text('Downloads')); await tester.pumpAndSettle(); expect(selected, 1); semantics.dispose(); expect(tester.takeException(), isNull); }); testWidgets('reduced motion keeps interactive Material fallback', ( tester, ) async { var calls = 0; await tester.pumpWidget( _host( Column( mainAxisSize: MainAxisSize.min, children: [ ExpressiveButton( onPressed: () => calls++, child: const Text('Play'), ), ExpressiveIconButton( icon: const Icon(Icons.skip_next), tooltip: 'Next', onPressed: () => calls++, ), AppSwitch(value: false, onChanged: (_) => calls++), ], ), reduceMotion: true, ), ); expect(find.byType(M3EButton), findsNothing); expect(find.byType(M3EIconButton), findsNothing); expect(find.byType(M3ESwitch), findsNothing); await tester.tap(find.text('Play')); await tester.tap(find.byTooltip('Next')); await tester.tap(find.byType(Switch)); expect(calls, 3); }); testWidgets('Mornye keeps Cupertino actions and its own surfaces', ( tester, ) async { await tester.pumpWidget( _host( Column( mainAxisSize: MainAxisSize.min, children: [ AppActionButton( onPressed: () {}, icon: const Icon(Icons.play_arrow), label: const Text('Play'), ), AppChoiceChip( label: const Text('Albums'), selected: true, onSelected: (_) {}, ), const AppContentCard(child: Text('Details')), AppSwitch(value: true, onChanged: (_) {}), ], ), theme: MornyeTheme.build(Brightness.dark), reduceMotion: true, ), ); await tester.pumpAndSettle(); expect(find.byType(MaterialExpressiveScope), findsNothing); expect(find.byType(CupertinoButton), findsAtLeast(2)); expect(tester.takeException(), isNull); }); }