// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { test('SnackBarThemeData copyWith, ==, hashCode basics', () { expect(const SnackBarThemeData(), const SnackBarThemeData().copyWith()); expect(const SnackBarThemeData().hashCode, const SnackBarThemeData().copyWith().hashCode); }); test('SnackBarThemeData lerp special cases', () { expect(SnackBarThemeData.lerp(null, null, 0), const SnackBarThemeData()); const SnackBarThemeData data = SnackBarThemeData(); expect(identical(SnackBarThemeData.lerp(data, data, 0.5), data), true); }); test('SnackBarThemeData null fields by default', () { const SnackBarThemeData snackBarTheme = SnackBarThemeData(); expect(snackBarTheme.backgroundColor, null); expect(snackBarTheme.actionTextColor, null); expect(snackBarTheme.disabledActionTextColor, null); expect(snackBarTheme.contentTextStyle, null); expect(snackBarTheme.elevation, null); expect(snackBarTheme.shape, null); expect(snackBarTheme.behavior, null); expect(snackBarTheme.width, null); expect(snackBarTheme.insetPadding, null); expect(snackBarTheme.showCloseIcon, null); expect(snackBarTheme.closeIconColor, null); expect(snackBarTheme.actionOverflowThreshold, null); }); test( 'SnackBarTheme throws assertion if width is provided with fixed behaviour', () { expect( () => SnackBarThemeData( behavior: SnackBarBehavior.fixed, width: 300.0, ), throwsAssertionError); }); testWidgets('Default SnackBarThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const SnackBarThemeData().debugFillProperties(builder); final List<String> description = builder.properties .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) .map((DiagnosticsNode node) => node.toString()) .toList(); expect(description, <String>[]); }); testWidgets('SnackBarThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const SnackBarThemeData( backgroundColor: Color(0xFFFFFFFF), actionTextColor: Color(0xFF0000AA), disabledActionTextColor: Color(0xFF00AA00), contentTextStyle: TextStyle(color: Color(0xFF123456)), elevation: 2.0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))), behavior: SnackBarBehavior.floating, width: 400.0, insetPadding: EdgeInsets.all(10.0), showCloseIcon: false, closeIconColor: Color(0xFF0000AA), actionOverflowThreshold: 0.5, ).debugFillProperties(builder); final List<String> description = builder.properties .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) .map((DiagnosticsNode node) => node.toString()) .toList(); expect(description, <String>[ 'backgroundColor: Color(0xffffffff)', 'actionTextColor: Color(0xff0000aa)', 'disabledActionTextColor: Color(0xff00aa00)', 'contentTextStyle: TextStyle(inherit: true, color: Color(0xff123456))', 'elevation: 2.0', 'shape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(2.0))', 'behavior: SnackBarBehavior.floating', 'width: 400.0', 'insetPadding: EdgeInsets.all(10.0)', 'showCloseIcon: false', 'closeIconColor: Color(0xff0000aa)', 'actionOverflowThreshold: 0.5', ]); }); testWidgets('Material2 - Passing no SnackBarThemeData returns defaults', (WidgetTester tester) async { const String text = 'I am a snack bar.'; await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: Scaffold( body: Builder( builder: (BuildContext context) { return GestureDetector( onTap: () { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: const Text(text), duration: const Duration(seconds: 2), action: SnackBarAction(label: 'ACTION', onPressed: () {}), )); }, child: const Text('X'), ); }, ), ), )); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Material material = _getSnackBarMaterial(tester); final RenderParagraph content = _getSnackBarTextRenderObject(tester, text); expect(content.text.style, Typography.material2018().white.titleMedium); expect(material.color, const Color(0xFF333333)); expect(material.elevation, 6.0); expect(material.shape, null); }); testWidgets('Material3 - Passing no SnackBarThemeData returns defaults', (WidgetTester tester) async { const String text = 'I am a snack bar.'; final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget(MaterialApp( theme: theme, home: Scaffold( body: Builder( builder: (BuildContext context) { return GestureDetector( onTap: () { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: const Text(text), duration: const Duration(seconds: 2), action: SnackBarAction(label: 'ACTION', onPressed: () {}), )); }, child: const Text('X'), ); }, ), ), )); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Material material = _getSnackBarMaterial(tester); final RenderParagraph content = _getSnackBarTextRenderObject(tester, text); expect(content.text.style, Typography.material2021().englishLike.bodyMedium?.merge(Typography.material2021().black.bodyMedium).copyWith(color: theme.colorScheme.onInverseSurface, decorationColor: theme.colorScheme.onSurface)); expect(material.color, theme.colorScheme.inverseSurface); expect(material.elevation, 6.0); expect(material.shape, null); }); testWidgets('SnackBar uses values from SnackBarThemeData', (WidgetTester tester) async { const String text = 'I am a snack bar.'; const String action = 'ACTION'; final SnackBarThemeData snackBarTheme = _snackBarTheme(showCloseIcon: true); await tester.pumpWidget(MaterialApp( theme: ThemeData(snackBarTheme: snackBarTheme), home: Scaffold( body: Builder( builder: (BuildContext context) { return GestureDetector( onTap: () { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: const Text(text), duration: const Duration(seconds: 2), action: SnackBarAction(label: action, onPressed: () {}), )); }, child: const Text('X'), ); }, ), ), )); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Material material = _getSnackBarMaterial(tester); final RenderParagraph button = _getSnackBarActionTextRenderObject(tester, action); final RenderParagraph content = _getSnackBarTextRenderObject(tester, text); final Icon icon = _getSnackBarIcon(tester); expect(content.text.style, snackBarTheme.contentTextStyle); expect(material.color, snackBarTheme.backgroundColor); expect(material.elevation, snackBarTheme.elevation); expect(material.shape, snackBarTheme.shape); expect(button.text.style!.color, snackBarTheme.actionTextColor); expect(icon.icon, Icons.close); }); testWidgets('SnackBar widget properties take priority over theme', (WidgetTester tester) async { const Color backgroundColor = Colors.purple; const Color textColor = Colors.pink; const double elevation = 7.0; const String action = 'ACTION'; const ShapeBorder shape = RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(9.0)), ); const double snackBarWidth = 400.0; await tester.pumpWidget(MaterialApp( theme: ThemeData(snackBarTheme: _snackBarTheme(showCloseIcon: true)), home: Scaffold( body: Builder( builder: (BuildContext context) { return GestureDetector( onTap: () { ScaffoldMessenger.of(context).showSnackBar(SnackBar( backgroundColor: backgroundColor, behavior: SnackBarBehavior.floating, width: snackBarWidth, elevation: elevation, shape: shape, content: const Text('I am a snack bar.'), duration: const Duration(seconds: 2), action: SnackBarAction( textColor: textColor, label: action, onPressed: () {}, ), showCloseIcon: false, )); }, child: const Text('X'), ); }, ), ), )); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Finder materialFinder = _getSnackBarMaterialFinder(tester); final Material material = _getSnackBarMaterial(tester); final RenderParagraph button = _getSnackBarActionTextRenderObject(tester, action); expect(material.color, backgroundColor); expect(material.elevation, elevation); expect(material.shape, shape); expect(button.text.style!.color, textColor); expect(_getSnackBarIconFinder(tester), findsNothing); // Assert width. final Offset snackBarBottomLeft = tester.getBottomLeft(materialFinder.first); final Offset snackBarBottomRight = tester.getBottomRight(materialFinder.first); expect(snackBarBottomLeft.dx, (800 - snackBarWidth) / 2); // Device width is 800. expect(snackBarBottomRight.dx, (800 + snackBarWidth) / 2); // Device width is 800. }); testWidgets('SnackBarAction uses actionBackgroundColor', (WidgetTester tester) async { final MaterialStateColor actionBackgroundColor = MaterialStateColor.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return Colors.blue; } return Colors.purple; }); await tester.pumpWidget(MaterialApp( theme: ThemeData(snackBarTheme: _createSnackBarTheme(actionBackgroundColor: actionBackgroundColor)), home: Scaffold( body: Builder( builder: (BuildContext context) { return GestureDetector( onTap: () { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: const Text('I am a snack bar.'), action: SnackBarAction( label: 'ACTION', onPressed: () {}, ), )); }, child: const Text('X'), ); }, ), ), )); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Material materialBeforeDismissed = tester.widget<Material>(find.descendant( of: find.widgetWithText(TextButton, 'ACTION'), matching: find.byType(Material), )); expect(materialBeforeDismissed.color, Colors.purple); await tester.tap(find.text('ACTION')); await tester.pump(); final Material materialAfterDismissed = tester.widget<Material>(find.descendant( of: find.widgetWithText(TextButton, 'ACTION'), matching: find.byType(Material), )); expect(materialAfterDismissed.color, Colors.blue); }); testWidgets('SnackBarAction backgroundColor overrides SnackBarThemeData actionBackgroundColor', (WidgetTester tester) async { final MaterialStateColor snackBarActionBackgroundColor = MaterialStateColor.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return Colors.amber; } return Colors.cyan; }); final MaterialStateColor actionBackgroundColor = MaterialStateColor.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return Colors.blue; } return Colors.purple; }); await tester.pumpWidget(MaterialApp( theme: ThemeData(snackBarTheme: _createSnackBarTheme(actionBackgroundColor: actionBackgroundColor)), home: Scaffold( body: Builder( builder: (BuildContext context) { return GestureDetector( onTap: () { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: const Text('I am a snack bar.'), action: SnackBarAction( label: 'ACTION', backgroundColor: snackBarActionBackgroundColor, onPressed: () {}, ), )); }, child: const Text('X'), ); }, ), ), )); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Material materialBeforeDismissed = tester.widget<Material>(find.descendant( of: find.widgetWithText(TextButton, 'ACTION'), matching: find.byType(Material), )); expect(materialBeforeDismissed.color, Colors.cyan); await tester.tap(find.text('ACTION')); await tester.pump(); final Material materialAfterDismissed = tester.widget<Material>(find.descendant( of: find.widgetWithText(TextButton, 'ACTION'), matching: find.byType(Material), )); expect(materialAfterDismissed.color, Colors.amber); }); testWidgets('SnackBarThemeData asserts when actionBackgroundColor is a MaterialStateColor and disabledActionBackgroundColor is also provided', (WidgetTester tester) async { final MaterialStateColor actionBackgroundColor = MaterialStateColor.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return Colors.blue; } return Colors.purple; }); expect(() => tester.pumpWidget(MaterialApp( theme: ThemeData(snackBarTheme: _createSnackBarTheme(actionBackgroundColor: actionBackgroundColor, disabledActionBackgroundColor: Colors.amber)), home: Scaffold( body: Builder( builder: (BuildContext context) { return GestureDetector( onTap: () { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: const Text('I am a snack bar.'), action: SnackBarAction( label: 'ACTION', onPressed: () {}, ), )); }, child: const Text('X'), ); }, ), ), )), throwsA(isA<AssertionError>().having( (AssertionError e) => e.toString(), 'description', contains('disabledBackgroundColor must not be provided when background color is a MaterialStateColor')) ) ); }); testWidgets('SnackBar theme behavior is correct for floating', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData( snackBarTheme: const SnackBarThemeData(behavior: SnackBarBehavior.floating)), home: Scaffold( floatingActionButton: FloatingActionButton( child: const Icon(Icons.send), onPressed: () {}, ), body: Builder( builder: (BuildContext context) { return GestureDetector( onTap: () { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: const Text('I am a snack bar.'), duration: const Duration(seconds: 2), action: SnackBarAction(label: 'ACTION', onPressed: () {}), )); }, child: const Text('X'), ); }, ), ), )); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final RenderBox snackBarBox = tester.firstRenderObject(find.byType(SnackBar)); final RenderBox floatingActionButtonBox = tester.firstRenderObject(find.byType(FloatingActionButton)); final Offset snackBarBottomCenter = snackBarBox.localToGlobal(snackBarBox.size.bottomCenter(Offset.zero)); final Offset floatingActionButtonTopCenter = floatingActionButtonBox.localToGlobal(floatingActionButtonBox.size.topCenter(Offset.zero)); // Since padding and margin is handled inside snackBarBox, // the bottom offset of snackbar should equal with top offset of FAB expect(snackBarBottomCenter.dy == floatingActionButtonTopCenter.dy, true); }); testWidgets('SnackBar theme behavior is correct for fixed', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData( snackBarTheme: const SnackBarThemeData(behavior: SnackBarBehavior.fixed), ), home: Scaffold( floatingActionButton: FloatingActionButton( child: const Icon(Icons.send), onPressed: () {}, ), body: Builder( builder: (BuildContext context) { return GestureDetector( onTap: () { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: const Text('I am a snack bar.'), duration: const Duration(seconds: 2), action: SnackBarAction(label: 'ACTION', onPressed: () {}), )); }, child: const Text('X'), ); }, ), ), )); final RenderBox floatingActionButtonOriginBox= tester.firstRenderObject(find.byType(FloatingActionButton)); final Offset floatingActionButtonOriginBottomCenter = floatingActionButtonOriginBox.localToGlobal(floatingActionButtonOriginBox.size.bottomCenter(Offset.zero)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final RenderBox snackBarBox = tester.firstRenderObject(find.byType(SnackBar)); final RenderBox floatingActionButtonBox = tester.firstRenderObject(find.byType(FloatingActionButton)); final Offset snackBarTopCenter = snackBarBox.localToGlobal(snackBarBox.size.topCenter(Offset.zero)); final Offset floatingActionButtonBottomCenter = floatingActionButtonBox.localToGlobal(floatingActionButtonBox.size.bottomCenter(Offset.zero)); expect(floatingActionButtonOriginBottomCenter.dy > floatingActionButtonBottomCenter.dy, true); expect(snackBarTopCenter.dy > floatingActionButtonBottomCenter.dy, true); }); Widget buildApp({ required SnackBarBehavior themedBehavior, EdgeInsetsGeometry? margin, double? width, double? themedActionOverflowThreshold, }) { return MaterialApp( theme: ThemeData( snackBarTheme: SnackBarThemeData( behavior: themedBehavior, actionOverflowThreshold: themedActionOverflowThreshold, ), ), home: Scaffold( floatingActionButton: FloatingActionButton( child: const Icon(Icons.send), onPressed: () {}, ), body: Builder( builder: (BuildContext context) { return GestureDetector( onTap: () { ScaffoldMessenger.of(context).showSnackBar(SnackBar( margin: margin, width: width, content: const Text('I am a snack bar.'), duration: const Duration(seconds: 2), action: SnackBarAction(label: 'ACTION', onPressed: () {}), )); }, child: const Text('X'), ); }, ), ), ); } testWidgets('SnackBar theme behavior will assert properly for margin use', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/84935 // SnackBarBehavior.floating set in theme does not assert with margin await tester.pumpWidget(buildApp( themedBehavior: SnackBarBehavior.floating, margin: const EdgeInsets.all(8.0), )); await tester.tap(find.text('X')); await tester.pump(); // start animation await tester.pump(const Duration(milliseconds: 750)); AssertionError? exception = tester.takeException() as AssertionError?; expect(exception, isNull); // SnackBarBehavior.fixed set in theme will still assert with margin await tester.pumpWidget(buildApp( themedBehavior: SnackBarBehavior.fixed, margin: const EdgeInsets.all(8.0), )); await tester.tap(find.text('X')); await tester.pump(); // start animation await tester.pump(const Duration(milliseconds: 750)); exception = tester.takeException() as AssertionError; expect( exception.message, 'Margin can only be used with floating behavior. SnackBarBehavior.fixed ' 'was set by the inherited SnackBarThemeData.', ); }); for (final double overflowThreshold in <double>[-1.0, -.0001, 1.000001, 5]) { test('SnackBar theme will assert for actionOverflowThreshold outside of 0-1 range', () { expect( () => SnackBarThemeData( actionOverflowThreshold: overflowThreshold, ), throwsAssertionError); }); } testWidgets('SnackBar theme behavior will assert properly for width use', (WidgetTester tester) async { // SnackBarBehavior.floating set in theme does not assert with width await tester.pumpWidget(buildApp( themedBehavior: SnackBarBehavior.floating, width: 5.0, )); await tester.tap(find.text('X')); await tester.pump(); // start animation await tester.pump(const Duration(milliseconds: 750)); AssertionError? exception = tester.takeException() as AssertionError?; expect(exception, isNull); // SnackBarBehavior.fixed set in theme will still assert with width await tester.pumpWidget(buildApp( themedBehavior: SnackBarBehavior.fixed, width: 5.0, )); await tester.tap(find.text('X')); await tester.pump(); // start animation await tester.pump(const Duration(milliseconds: 750)); exception = tester.takeException() as AssertionError; expect( exception.message, 'Width can only be used with floating behavior. SnackBarBehavior.fixed ' 'was set by the inherited SnackBarThemeData.', ); }); } SnackBarThemeData _snackBarTheme({bool? showCloseIcon}) { return SnackBarThemeData( backgroundColor: Colors.orange, actionTextColor: Colors.green, contentTextStyle: const TextStyle(color: Colors.blue), elevation: 12.0, showCloseIcon: showCloseIcon, shape: const BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))), ); } SnackBarThemeData _createSnackBarTheme({ Color? backgroundColor, Color? actionTextColor, Color? disabledActionTextColor, TextStyle? contentTextStyle, double? elevation, ShapeBorder? shape, SnackBarBehavior? behavior, Color? actionBackgroundColor, Color? disabledActionBackgroundColor }) { return SnackBarThemeData( backgroundColor: backgroundColor, actionTextColor: actionTextColor, disabledActionTextColor: disabledActionTextColor, contentTextStyle: contentTextStyle, elevation: elevation, shape: shape, behavior: behavior, actionBackgroundColor: actionBackgroundColor, disabledActionBackgroundColor: disabledActionBackgroundColor ); } Material _getSnackBarMaterial(WidgetTester tester) { return tester.widget<Material>( _getSnackBarMaterialFinder(tester).first, ); } Finder _getSnackBarMaterialFinder(WidgetTester tester) { return find.descendant( of: find.byType(SnackBar), matching: find.byType(Material), ); } RenderParagraph _getSnackBarActionTextRenderObject(WidgetTester tester, String text) { return tester.renderObject(find.descendant( of: find.byType(TextButton), matching: find.text(text), )); } Icon _getSnackBarIcon(WidgetTester tester) { return tester.widget<Icon>(_getSnackBarIconFinder(tester)); } Finder _getSnackBarIconFinder(WidgetTester tester) { return find.descendant( of: find.byType(SnackBar), matching: find.byIcon(Icons.close), ); } RenderParagraph _getSnackBarTextRenderObject(WidgetTester tester, String text) { return tester.renderObject(find.descendant( of: find.byType(SnackBar), matching: find.text(text), )); }