1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/foundation.dart';
6import 'package:flutter/gestures.dart';
7import 'package:flutter/material.dart';
8import 'package:flutter/rendering.dart';
9import 'package:flutter_test/flutter_test.dart';
10import '../widgets/semantics_tester.dart';
11
12void main() {
13 TextStyle iconStyle(WidgetTester tester, IconData icon) {
14 final RichText iconRichText = tester.widget<RichText>(
15 find.descendant(of: find.byIcon(icon), matching: find.byType(RichText)),
16 );
17 return iconRichText.text.style!;
18 }
19
20 Color textColor(WidgetTester tester, String text) {
21 return tester.renderObject<RenderParagraph>(find.text(text)).text.style!.color!;
22 }
23
24 testWidgets('ElevatedButton, ElevatedButton.icon defaults', (WidgetTester tester) async {
25 const ColorScheme colorScheme = ColorScheme.light();
26 final ThemeData theme = ThemeData.from(colorScheme: colorScheme);
27 final bool material3 = theme.useMaterial3;
28
29 // Enabled ElevatedButton
30 await tester.pumpWidget(
31 MaterialApp(
32 theme: theme,
33 home: Center(
34 child: ElevatedButton(onPressed: () {}, child: const Text('button')),
35 ),
36 ),
37 );
38
39 final Finder buttonMaterial = find.descendant(
40 of: find.byType(ElevatedButton),
41 matching: find.byType(Material),
42 );
43
44 Material material = tester.widget<Material>(buttonMaterial);
45 expect(material.animationDuration, const Duration(milliseconds: 200));
46 expect(material.borderOnForeground, false);
47 expect(material.borderRadius, null);
48 expect(material.clipBehavior, Clip.none);
49 expect(material.color, material3 ? colorScheme.onPrimary : colorScheme.primary);
50 expect(material.elevation, material3 ? 1 : 2);
51 expect(material.shadowColor, const Color(0xff000000));
52 expect(
53 material.shape,
54 material3
55 ? const StadiumBorder()
56 : const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4))),
57 );
58 expect(material.textStyle!.color, material3 ? colorScheme.primary : colorScheme.onPrimary);
59 expect(material.textStyle!.fontFamily, 'Roboto');
60 expect(material.textStyle!.fontSize, 14);
61 expect(material.textStyle!.fontWeight, FontWeight.w500);
62 expect(material.type, MaterialType.button);
63
64 final Align align = tester.firstWidget<Align>(
65 find.ancestor(of: find.text('button'), matching: find.byType(Align)),
66 );
67 expect(align.alignment, Alignment.center);
68
69 final Offset center = tester.getCenter(find.byType(ElevatedButton));
70 final TestGesture gesture = await tester.startGesture(center);
71 await tester.pump(); // start the splash animation
72 await tester.pump(const Duration(milliseconds: 100)); // splash is underway
73
74 // Material 3 uses the InkSparkle which uses a shader, so we can't capture
75 // the effect with paint methods.
76 if (!material3) {
77 final RenderObject inkFeatures = tester.allRenderObjects.firstWhere(
78 (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
79 );
80 expect(inkFeatures, paints..circle(color: colorScheme.onPrimary.withOpacity(0.24)));
81 }
82
83 // Only elevation changes when enabled and pressed.
84 material = tester.widget<Material>(buttonMaterial);
85 expect(material.animationDuration, const Duration(milliseconds: 200));
86 expect(material.borderOnForeground, false);
87 expect(material.borderRadius, null);
88 expect(material.clipBehavior, Clip.none);
89 expect(material.color, material3 ? colorScheme.onPrimary : colorScheme.primary);
90 expect(material.elevation, material3 ? 1 : 8);
91 expect(material.shadowColor, const Color(0xff000000));
92 expect(
93 material.shape,
94 material3
95 ? const StadiumBorder()
96 : const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4))),
97 );
98 expect(material.textStyle!.color, material3 ? colorScheme.primary : colorScheme.onPrimary);
99 expect(material.textStyle!.fontFamily, 'Roboto');
100 expect(material.textStyle!.fontSize, 14);
101 expect(material.textStyle!.fontWeight, FontWeight.w500);
102 expect(material.type, MaterialType.button);
103
104 await gesture.up();
105 await tester.pumpAndSettle();
106
107 // Enabled ElevatedButton.icon
108 final Key iconButtonKey = UniqueKey();
109 await tester.pumpWidget(
110 MaterialApp(
111 theme: theme,
112 home: Center(
113 child: ElevatedButton.icon(
114 key: iconButtonKey,
115 onPressed: () {},
116 icon: const Icon(Icons.add),
117 label: const Text('label'),
118 ),
119 ),
120 ),
121 );
122
123 final Finder iconButtonMaterial = find.descendant(
124 of: find.byKey(iconButtonKey),
125 matching: find.byType(Material),
126 );
127
128 material = tester.widget<Material>(iconButtonMaterial);
129 expect(material.animationDuration, const Duration(milliseconds: 200));
130 expect(material.borderOnForeground, false);
131 expect(material.borderRadius, null);
132 expect(material.clipBehavior, Clip.none);
133 expect(material.color, material3 ? colorScheme.onPrimary : colorScheme.primary);
134 expect(material.elevation, material3 ? 1 : 2);
135 expect(material.shadowColor, const Color(0xff000000));
136 expect(
137 material.shape,
138 material3
139 ? const StadiumBorder()
140 : const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4))),
141 );
142 expect(material.textStyle!.color, material3 ? colorScheme.primary : colorScheme.onPrimary);
143 expect(material.textStyle!.fontFamily, 'Roboto');
144 expect(material.textStyle!.fontSize, 14);
145 expect(material.textStyle!.fontWeight, FontWeight.w500);
146 expect(material.type, MaterialType.button);
147
148 // Disabled ElevatedButton
149 await tester.pumpWidget(
150 MaterialApp(
151 theme: theme,
152 home: const Center(child: ElevatedButton(onPressed: null, child: Text('button'))),
153 ),
154 );
155
156 // Finish the elevation animation, final background color change.
157 await tester.pumpAndSettle();
158
159 material = tester.widget<Material>(buttonMaterial);
160 expect(material.animationDuration, const Duration(milliseconds: 200));
161 expect(material.borderOnForeground, false);
162 expect(material.borderRadius, null);
163 expect(material.clipBehavior, Clip.none);
164 expect(material.color, colorScheme.onSurface.withOpacity(0.12));
165 expect(material.elevation, 0.0);
166 expect(material.shadowColor, const Color(0xff000000));
167 expect(
168 material.shape,
169 material3
170 ? const StadiumBorder()
171 : const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4))),
172 );
173 expect(material.textStyle!.color, colorScheme.onSurface.withOpacity(0.38));
174 expect(material.textStyle!.fontFamily, 'Roboto');
175 expect(material.textStyle!.fontSize, 14);
176 expect(material.textStyle!.fontWeight, FontWeight.w500);
177 expect(material.type, MaterialType.button);
178 });
179
180 testWidgets(
181 'ElevatedButton.defaultStyle produces a ButtonStyle with appropriate non-null values',
182 (WidgetTester tester) async {
183 const ColorScheme colorScheme = ColorScheme.light();
184 final ThemeData theme = ThemeData.from(colorScheme: colorScheme);
185
186 final ElevatedButton button = ElevatedButton(onPressed: () {}, child: const Text('button'));
187 BuildContext? capturedContext;
188 // Enabled ElevatedButton
189 await tester.pumpWidget(
190 MaterialApp(
191 theme: theme,
192 home: Center(
193 child: Builder(
194 builder: (BuildContext context) {
195 capturedContext = context;
196 return button;
197 },
198 ),
199 ),
200 ),
201 );
202 final ButtonStyle style = button.defaultStyleOf(capturedContext!);
203
204 // Properties that must be non-null.
205 expect(style.textStyle, isNotNull, reason: 'textStyle style');
206 expect(style.backgroundColor, isNotNull, reason: 'backgroundColor style');
207 expect(style.foregroundColor, isNotNull, reason: 'foregroundColor style');
208 expect(style.overlayColor, isNotNull, reason: 'overlayColor style');
209 expect(style.shadowColor, isNotNull, reason: 'shadowColor style');
210 expect(style.surfaceTintColor, isNotNull, reason: 'surfaceTintColor style');
211 expect(style.elevation, isNotNull, reason: 'elevation style');
212 expect(style.padding, isNotNull, reason: 'padding style');
213 expect(style.minimumSize, isNotNull, reason: 'minimumSize style');
214 expect(style.maximumSize, isNotNull, reason: 'maximumSize style');
215 expect(style.iconColor, isNotNull, reason: 'iconColor style');
216 expect(style.iconSize, isNotNull, reason: 'iconSize style');
217 expect(style.shape, isNotNull, reason: 'shape style');
218 expect(style.mouseCursor, isNotNull, reason: 'mouseCursor style');
219 expect(style.visualDensity, isNotNull, reason: 'visualDensity style');
220 expect(style.tapTargetSize, isNotNull, reason: 'tapTargetSize style');
221 expect(style.animationDuration, isNotNull, reason: 'animationDuration style');
222 expect(style.enableFeedback, isNotNull, reason: 'enableFeedback style');
223 expect(style.alignment, isNotNull, reason: 'alignment style');
224 expect(style.splashFactory, isNotNull, reason: 'splashFactory style');
225
226 // Properties that are expected to be null.
227 expect(style.fixedSize, isNull, reason: 'fixedSize style');
228 expect(style.side, isNull, reason: 'side style');
229 expect(style.backgroundBuilder, isNull, reason: 'backgroundBuilder style');
230 expect(style.foregroundBuilder, isNull, reason: 'foregroundBuilder style');
231 },
232 );
233
234 testWidgets(
235 'ElevatedButton.defaultStyle with an icon produces a ButtonStyle with appropriate non-null values',
236 (WidgetTester tester) async {
237 const ColorScheme colorScheme = ColorScheme.light();
238 final ThemeData theme = ThemeData.from(colorScheme: colorScheme);
239
240 final ElevatedButton button = ElevatedButton.icon(
241 onPressed: () {},
242 icon: const SizedBox(),
243 label: const Text('button'),
244 );
245 BuildContext? capturedContext;
246 await tester.pumpWidget(
247 MaterialApp(
248 theme: theme,
249 home: Center(
250 child: Builder(
251 builder: (BuildContext context) {
252 capturedContext = context;
253 return button;
254 },
255 ),
256 ),
257 ),
258 );
259 final ButtonStyle style = button.defaultStyleOf(capturedContext!);
260
261 // Properties that must be non-null.
262 expect(style.textStyle, isNotNull, reason: 'textStyle style');
263 expect(style.backgroundColor, isNotNull, reason: 'backgroundColor style');
264 expect(style.foregroundColor, isNotNull, reason: 'foregroundColor style');
265 expect(style.overlayColor, isNotNull, reason: 'overlayColor style');
266 expect(style.shadowColor, isNotNull, reason: 'shadowColor style');
267 expect(style.surfaceTintColor, isNotNull, reason: 'surfaceTintColor style');
268 expect(style.elevation, isNotNull, reason: 'elevation style');
269 expect(style.padding, isNotNull, reason: 'padding style');
270 expect(style.minimumSize, isNotNull, reason: 'minimumSize style');
271 expect(style.maximumSize, isNotNull, reason: 'maximumSize style');
272 expect(style.iconColor, isNotNull, reason: 'iconColor style');
273 expect(style.iconSize, isNotNull, reason: 'iconSize style');
274 expect(style.shape, isNotNull, reason: 'shape style');
275 expect(style.mouseCursor, isNotNull, reason: 'mouseCursor style');
276 expect(style.visualDensity, isNotNull, reason: 'visualDensity style');
277 expect(style.tapTargetSize, isNotNull, reason: 'tapTargetSize style');
278 expect(style.animationDuration, isNotNull, reason: 'animationDuration style');
279 expect(style.enableFeedback, isNotNull, reason: 'enableFeedback style');
280 expect(style.alignment, isNotNull, reason: 'alignment style');
281 expect(style.splashFactory, isNotNull, reason: 'splashFactory style');
282
283 // Properties that are expected to be null.
284 expect(style.fixedSize, isNull, reason: 'fixedSize style');
285 expect(style.side, isNull, reason: 'side style');
286 expect(style.backgroundBuilder, isNull, reason: 'backgroundBuilder style');
287 expect(style.foregroundBuilder, isNull, reason: 'foregroundBuilder style');
288 },
289 );
290
291 testWidgets('ElevatedButton.icon produces the correct widgets if icon is null', (
292 WidgetTester tester,
293 ) async {
294 const ColorScheme colorScheme = ColorScheme.light();
295 final ThemeData theme = ThemeData.from(colorScheme: colorScheme);
296 final Key iconButtonKey = UniqueKey();
297 await tester.pumpWidget(
298 MaterialApp(
299 theme: theme,
300 home: Center(
301 child: ElevatedButton.icon(
302 key: iconButtonKey,
303 onPressed: () {},
304 icon: const Icon(Icons.add),
305 label: const Text('label'),
306 ),
307 ),
308 ),
309 );
310
311 expect(find.byIcon(Icons.add), findsOneWidget);
312 expect(find.text('label'), findsOneWidget);
313
314 await tester.pumpWidget(
315 MaterialApp(
316 theme: theme,
317 home: Center(
318 child: ElevatedButton.icon(
319 key: iconButtonKey,
320 onPressed: () {},
321 // No icon specified.
322 label: const Text('label'),
323 ),
324 ),
325 ),
326 );
327
328 expect(find.byIcon(Icons.add), findsNothing);
329 expect(find.text('label'), findsOneWidget);
330 });
331
332 testWidgets(
333 'Default ElevatedButton meets a11y contrast guidelines',
334 (WidgetTester tester) async {
335 final FocusNode focusNode = FocusNode();
336
337 await tester.pumpWidget(
338 MaterialApp(
339 theme: ThemeData.from(colorScheme: const ColorScheme.light()),
340 home: Scaffold(
341 body: Center(
342 child: ElevatedButton(
343 onPressed: () {},
344 focusNode: focusNode,
345 child: const Text('ElevatedButton'),
346 ),
347 ),
348 ),
349 ),
350 );
351
352 // Default, not disabled.
353 await expectLater(tester, meetsGuideline(textContrastGuideline));
354
355 // Focused.
356 focusNode.requestFocus();
357 await tester.pumpAndSettle();
358 await expectLater(tester, meetsGuideline(textContrastGuideline));
359
360 // Hovered.
361 final Offset center = tester.getCenter(find.byType(ElevatedButton));
362 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
363 await gesture.addPointer();
364 await gesture.moveTo(center);
365 await tester.pumpAndSettle();
366 await expectLater(tester, meetsGuideline(textContrastGuideline));
367 focusNode.dispose();
368 },
369 skip: isBrowser, // https://github.com/flutter/flutter/issues/44115
370 );
371
372 testWidgets('ElevatedButton default overlayColor and elevation resolve pressed state', (
373 WidgetTester tester,
374 ) async {
375 final FocusNode focusNode = FocusNode();
376 final ThemeData theme = ThemeData();
377
378 await tester.pumpWidget(
379 MaterialApp(
380 theme: theme,
381 home: Scaffold(
382 body: Center(
383 child: ElevatedButton(
384 onPressed: () {},
385 focusNode: focusNode,
386 child: const Text('ElevatedButton'),
387 ),
388 ),
389 ),
390 ),
391 );
392
393 RenderObject overlayColor() {
394 return tester.allRenderObjects.firstWhere(
395 (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
396 );
397 }
398
399 double elevation() {
400 return tester
401 .widget<PhysicalShape>(
402 find.descendant(of: find.byType(ElevatedButton), matching: find.byType(PhysicalShape)),
403 )
404 .elevation;
405 }
406
407 // Hovered.
408 final Offset center = tester.getCenter(find.byType(ElevatedButton));
409 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
410 await gesture.addPointer();
411 await gesture.moveTo(center);
412 await tester.pumpAndSettle();
413 expect(elevation(), 3.0);
414 expect(overlayColor(), paints..rect(color: theme.colorScheme.primary.withOpacity(0.08)));
415
416 // Highlighted (pressed).
417 await gesture.down(center);
418 await tester.pumpAndSettle();
419 expect(elevation(), 1.0);
420 expect(
421 overlayColor(),
422 paints
423 ..rect()
424 ..rect(color: theme.colorScheme.primary.withOpacity(0.1)),
425 );
426 // Remove pressed and hovered states
427 await gesture.up();
428 await tester.pumpAndSettle();
429 await gesture.moveTo(const Offset(0, 50));
430 await tester.pumpAndSettle();
431
432 // Focused.
433 focusNode.requestFocus();
434 await tester.pumpAndSettle();
435 expect(elevation(), 1.0);
436 expect(overlayColor(), paints..rect(color: theme.colorScheme.primary.withOpacity(0.1)));
437
438 focusNode.dispose();
439 });
440
441 testWidgets('ElevatedButton uses stateful color for text color in different states', (
442 WidgetTester tester,
443 ) async {
444 const String buttonText = 'ElevatedButton';
445 final FocusNode focusNode = FocusNode();
446 const Color pressedColor = Color(0x00000001);
447 const Color hoverColor = Color(0x00000002);
448 const Color focusedColor = Color(0x00000003);
449 const Color defaultColor = Color(0x00000004);
450
451 Color getTextColor(Set<MaterialState> states) {
452 if (states.contains(MaterialState.pressed)) {
453 return pressedColor;
454 }
455 if (states.contains(MaterialState.hovered)) {
456 return hoverColor;
457 }
458 if (states.contains(MaterialState.focused)) {
459 return focusedColor;
460 }
461 return defaultColor;
462 }
463
464 await tester.pumpWidget(
465 MaterialApp(
466 home: Scaffold(
467 body: Center(
468 child: ElevatedButtonTheme(
469 data: ElevatedButtonThemeData(
470 style: ButtonStyle(
471 foregroundColor: MaterialStateProperty.resolveWith<Color>(getTextColor),
472 ),
473 ),
474 child: Builder(
475 builder: (BuildContext context) {
476 return ElevatedButton(
477 onPressed: () {},
478 focusNode: focusNode,
479 child: const Text(buttonText),
480 );
481 },
482 ),
483 ),
484 ),
485 ),
486 ),
487 );
488
489 // Default, not disabled.
490 expect(textColor(tester, buttonText), equals(defaultColor));
491
492 // Focused.
493 focusNode.requestFocus();
494 await tester.pumpAndSettle();
495 expect(textColor(tester, buttonText), focusedColor);
496
497 // Hovered.
498 final Offset center = tester.getCenter(find.byType(ElevatedButton));
499 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
500 await gesture.addPointer();
501 await gesture.moveTo(center);
502 await tester.pumpAndSettle();
503 expect(textColor(tester, buttonText), hoverColor);
504
505 // Highlighted (pressed).
506 await gesture.down(center);
507 await tester.pump(); // Start the splash and highlight animations.
508 await tester.pump(
509 const Duration(milliseconds: 800),
510 ); // Wait for splash and highlight to be well under way.
511 expect(textColor(tester, buttonText), pressedColor);
512
513 focusNode.dispose();
514 });
515
516 testWidgets('ElevatedButton uses stateful color for icon color in different states', (
517 WidgetTester tester,
518 ) async {
519 final FocusNode focusNode = FocusNode();
520 final Key buttonKey = UniqueKey();
521
522 const Color pressedColor = Color(0x00000001);
523 const Color hoverColor = Color(0x00000002);
524 const Color focusedColor = Color(0x00000003);
525 const Color defaultColor = Color(0x00000004);
526
527 Color getTextColor(Set<MaterialState> states) {
528 if (states.contains(MaterialState.pressed)) {
529 return pressedColor;
530 }
531 if (states.contains(MaterialState.hovered)) {
532 return hoverColor;
533 }
534 if (states.contains(MaterialState.focused)) {
535 return focusedColor;
536 }
537 return defaultColor;
538 }
539
540 await tester.pumpWidget(
541 MaterialApp(
542 home: Scaffold(
543 body: Center(
544 child: ElevatedButtonTheme(
545 data: ElevatedButtonThemeData(
546 style: ButtonStyle(
547 foregroundColor: MaterialStateProperty.resolveWith<Color>(getTextColor),
548 iconColor: MaterialStateProperty.resolveWith<Color>(getTextColor),
549 ),
550 ),
551 child: Builder(
552 builder: (BuildContext context) {
553 return ElevatedButton.icon(
554 key: buttonKey,
555 icon: const Icon(Icons.add),
556 label: const Text('ElevatedButton'),
557 onPressed: () {},
558 focusNode: focusNode,
559 );
560 },
561 ),
562 ),
563 ),
564 ),
565 ),
566 );
567
568 // Default, not disabled.
569 expect(iconStyle(tester, Icons.add).color, equals(defaultColor));
570
571 // Focused.
572 focusNode.requestFocus();
573 await tester.pumpAndSettle();
574 expect(iconStyle(tester, Icons.add).color, focusedColor);
575
576 // Hovered.
577 final Offset center = tester.getCenter(find.byKey(buttonKey));
578 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
579 await gesture.addPointer();
580 await gesture.moveTo(center);
581 await tester.pumpAndSettle();
582 expect(iconStyle(tester, Icons.add).color, hoverColor);
583
584 // Highlighted (pressed).
585 await gesture.down(center);
586 await tester.pump(); // Start the splash and highlight animations.
587 await tester.pump(
588 const Duration(milliseconds: 800),
589 ); // Wait for splash and highlight to be well under way.
590 expect(iconStyle(tester, Icons.add).color, pressedColor);
591
592 focusNode.dispose();
593 });
594
595 testWidgets(
596 'ElevatedButton onPressed and onLongPress callbacks are correctly called when non-null',
597 (WidgetTester tester) async {
598 bool wasPressed;
599 Finder elevatedButton;
600
601 Widget buildFrame({VoidCallback? onPressed, VoidCallback? onLongPress}) {
602 return Directionality(
603 textDirection: TextDirection.ltr,
604 child: ElevatedButton(
605 onPressed: onPressed,
606 onLongPress: onLongPress,
607 child: const Text('button'),
608 ),
609 );
610 }
611
612 // onPressed not null, onLongPress null.
613 wasPressed = false;
614 await tester.pumpWidget(
615 buildFrame(
616 onPressed: () {
617 wasPressed = true;
618 },
619 ),
620 );
621 elevatedButton = find.byType(ElevatedButton);
622 expect(tester.widget<ElevatedButton>(elevatedButton).enabled, true);
623 await tester.tap(elevatedButton);
624 expect(wasPressed, true);
625
626 // onPressed null, onLongPress not null.
627 wasPressed = false;
628 await tester.pumpWidget(
629 buildFrame(
630 onLongPress: () {
631 wasPressed = true;
632 },
633 ),
634 );
635 elevatedButton = find.byType(ElevatedButton);
636 expect(tester.widget<ElevatedButton>(elevatedButton).enabled, true);
637 await tester.longPress(elevatedButton);
638 expect(wasPressed, true);
639
640 // onPressed null, onLongPress null.
641 await tester.pumpWidget(buildFrame());
642 elevatedButton = find.byType(ElevatedButton);
643 expect(tester.widget<ElevatedButton>(elevatedButton).enabled, false);
644 },
645 );
646
647 testWidgets('ElevatedButton onPressed and onLongPress callbacks are distinctly recognized', (
648 WidgetTester tester,
649 ) async {
650 bool didPressButton = false;
651 bool didLongPressButton = false;
652
653 await tester.pumpWidget(
654 Directionality(
655 textDirection: TextDirection.ltr,
656 child: ElevatedButton(
657 onPressed: () {
658 didPressButton = true;
659 },
660 onLongPress: () {
661 didLongPressButton = true;
662 },
663 child: const Text('button'),
664 ),
665 ),
666 );
667
668 final Finder elevatedButton = find.byType(ElevatedButton);
669 expect(tester.widget<ElevatedButton>(elevatedButton).enabled, true);
670
671 expect(didPressButton, isFalse);
672 await tester.tap(elevatedButton);
673 expect(didPressButton, isTrue);
674
675 expect(didLongPressButton, isFalse);
676 await tester.longPress(elevatedButton);
677 expect(didLongPressButton, isTrue);
678 });
679
680 testWidgets("ElevatedButton response doesn't hover when disabled", (WidgetTester tester) async {
681 FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch;
682 final FocusNode focusNode = FocusNode(debugLabel: 'ElevatedButton Focus');
683 final GlobalKey childKey = GlobalKey();
684 bool hovering = false;
685 await tester.pumpWidget(
686 Directionality(
687 textDirection: TextDirection.ltr,
688 child: SizedBox(
689 width: 100,
690 height: 100,
691 child: ElevatedButton(
692 autofocus: true,
693 onPressed: () {},
694 onLongPress: () {},
695 onHover: (bool value) {
696 hovering = value;
697 },
698 focusNode: focusNode,
699 child: SizedBox(key: childKey),
700 ),
701 ),
702 ),
703 );
704 await tester.pumpAndSettle();
705 expect(focusNode.hasPrimaryFocus, isTrue);
706 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
707 await gesture.addPointer();
708 await gesture.moveTo(tester.getCenter(find.byKey(childKey)));
709 await tester.pumpAndSettle();
710 expect(hovering, isTrue);
711
712 await tester.pumpWidget(
713 Directionality(
714 textDirection: TextDirection.ltr,
715 child: SizedBox(
716 width: 100,
717 height: 100,
718 child: ElevatedButton(
719 focusNode: focusNode,
720 onHover: (bool value) {
721 hovering = value;
722 },
723 onPressed: null,
724 child: SizedBox(key: childKey),
725 ),
726 ),
727 ),
728 );
729
730 await tester.pumpAndSettle();
731 expect(focusNode.hasPrimaryFocus, isFalse);
732
733 focusNode.dispose();
734 });
735
736 testWidgets('disabled and hovered ElevatedButton responds to mouse-exit', (
737 WidgetTester tester,
738 ) async {
739 int onHoverCount = 0;
740 late bool hover;
741
742 Widget buildFrame({required bool enabled}) {
743 return Directionality(
744 textDirection: TextDirection.ltr,
745 child: Center(
746 child: SizedBox(
747 width: 100,
748 height: 100,
749 child: ElevatedButton(
750 onPressed: enabled ? () {} : null,
751 onHover: (bool value) {
752 onHoverCount += 1;
753 hover = value;
754 },
755 child: const Text('ElevatedButton'),
756 ),
757 ),
758 ),
759 );
760 }
761
762 await tester.pumpWidget(buildFrame(enabled: true));
763 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
764 await gesture.addPointer();
765
766 await gesture.moveTo(tester.getCenter(find.byType(ElevatedButton)));
767 await tester.pumpAndSettle();
768 expect(onHoverCount, 1);
769 expect(hover, true);
770
771 await tester.pumpWidget(buildFrame(enabled: false));
772 await tester.pumpAndSettle();
773 await gesture.moveTo(Offset.zero);
774 // Even though the ElevatedButton has been disabled, the mouse-exit still
775 // causes onHover(false) to be called.
776 expect(onHoverCount, 2);
777 expect(hover, false);
778
779 await gesture.moveTo(tester.getCenter(find.byType(ElevatedButton)));
780 await tester.pumpAndSettle();
781 // We no longer see hover events because the ElevatedButton is disabled
782 // and it's no longer in the "hovering" state.
783 expect(onHoverCount, 2);
784 expect(hover, false);
785
786 await tester.pumpWidget(buildFrame(enabled: true));
787 await tester.pumpAndSettle();
788 // The ElevatedButton was enabled while it contained the mouse, however
789 // we do not call onHover() because it may call setState().
790 expect(onHoverCount, 2);
791 expect(hover, false);
792
793 await gesture.moveTo(tester.getCenter(find.byType(ElevatedButton)) - const Offset(1, 1));
794 await tester.pumpAndSettle();
795 // Moving the mouse a little within the ElevatedButton doesn't change anything.
796 expect(onHoverCount, 2);
797 expect(hover, false);
798 });
799
800 testWidgets('Can set ElevatedButton focus and Can set unFocus.', (WidgetTester tester) async {
801 final FocusNode node = FocusNode(debugLabel: 'ElevatedButton Focus');
802 bool gotFocus = false;
803 await tester.pumpWidget(
804 Directionality(
805 textDirection: TextDirection.ltr,
806 child: ElevatedButton(
807 focusNode: node,
808 onFocusChange: (bool focused) => gotFocus = focused,
809 onPressed: () {},
810 child: const SizedBox(),
811 ),
812 ),
813 );
814
815 node.requestFocus();
816
817 await tester.pump();
818
819 expect(gotFocus, isTrue);
820 expect(node.hasFocus, isTrue);
821
822 node.unfocus();
823 await tester.pump();
824
825 expect(gotFocus, isFalse);
826 expect(node.hasFocus, isFalse);
827
828 node.dispose();
829 });
830
831 testWidgets('When ElevatedButton disable, Can not set ElevatedButton focus.', (
832 WidgetTester tester,
833 ) async {
834 final FocusNode node = FocusNode(debugLabel: 'ElevatedButton Focus');
835 bool gotFocus = false;
836 await tester.pumpWidget(
837 Directionality(
838 textDirection: TextDirection.ltr,
839 child: ElevatedButton(
840 focusNode: node,
841 onFocusChange: (bool focused) => gotFocus = focused,
842 onPressed: null,
843 child: const SizedBox(),
844 ),
845 ),
846 );
847
848 node.requestFocus();
849
850 await tester.pump();
851
852 expect(gotFocus, isFalse);
853 expect(node.hasFocus, isFalse);
854
855 node.dispose();
856 });
857
858 testWidgets('Does ElevatedButton work with hover', (WidgetTester tester) async {
859 const Color hoverColor = Color(0xff001122);
860
861 await tester.pumpWidget(
862 Directionality(
863 textDirection: TextDirection.ltr,
864 child: ElevatedButton(
865 style: ButtonStyle(
866 overlayColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
867 return states.contains(MaterialState.hovered) ? hoverColor : null;
868 }),
869 ),
870 onPressed: () {},
871 child: const Text('button'),
872 ),
873 ),
874 );
875
876 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
877 await gesture.addPointer();
878 await gesture.moveTo(tester.getCenter(find.byType(ElevatedButton)));
879 await tester.pumpAndSettle();
880
881 final RenderObject inkFeatures = tester.allRenderObjects.firstWhere(
882 (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
883 );
884 expect(inkFeatures, paints..rect(color: hoverColor));
885 });
886
887 testWidgets('Does ElevatedButton work with focus', (WidgetTester tester) async {
888 const Color focusColor = Color(0xff001122);
889
890 final FocusNode focusNode = FocusNode(debugLabel: 'ElevatedButton Node');
891 await tester.pumpWidget(
892 Directionality(
893 textDirection: TextDirection.ltr,
894 child: ElevatedButton(
895 style: ButtonStyle(
896 overlayColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
897 return states.contains(MaterialState.focused) ? focusColor : null;
898 }),
899 ),
900 focusNode: focusNode,
901 onPressed: () {},
902 child: const Text('button'),
903 ),
904 ),
905 );
906
907 FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
908 focusNode.requestFocus();
909 await tester.pumpAndSettle();
910
911 final RenderObject inkFeatures = tester.allRenderObjects.firstWhere(
912 (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
913 );
914 expect(inkFeatures, paints..rect(color: focusColor));
915
916 focusNode.dispose();
917 });
918
919 testWidgets('Does ElevatedButton work with autofocus', (WidgetTester tester) async {
920 const Color focusColor = Color(0xff001122);
921
922 Color? getOverlayColor(Set<MaterialState> states) {
923 return states.contains(MaterialState.focused) ? focusColor : null;
924 }
925
926 final FocusNode focusNode = FocusNode(debugLabel: 'ElevatedButton Node');
927 await tester.pumpWidget(
928 Directionality(
929 textDirection: TextDirection.ltr,
930 child: ElevatedButton(
931 autofocus: true,
932 style: ButtonStyle(
933 overlayColor: MaterialStateProperty.resolveWith<Color?>(getOverlayColor),
934 ),
935 focusNode: focusNode,
936 onPressed: () {},
937 child: const Text('button'),
938 ),
939 ),
940 );
941
942 FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
943 await tester.pumpAndSettle();
944
945 final RenderObject inkFeatures = tester.allRenderObjects.firstWhere(
946 (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
947 );
948 expect(inkFeatures, paints..rect(color: focusColor));
949
950 focusNode.dispose();
951 });
952
953 testWidgets('Does ElevatedButton contribute semantics', (WidgetTester tester) async {
954 final SemanticsTester semantics = SemanticsTester(tester);
955 await tester.pumpWidget(
956 Theme(
957 data: ThemeData(useMaterial3: false),
958 child: Directionality(
959 textDirection: TextDirection.ltr,
960 child: Center(
961 child: ElevatedButton(
962 style: const ButtonStyle(
963 // Specifying minimumSize to mimic the original minimumSize for
964 // RaisedButton so that the semantics tree's rect and transform
965 // match the original version of this test.
966 minimumSize: MaterialStatePropertyAll<Size>(Size(88, 36)),
967 ),
968 onPressed: () {},
969 child: const Text('ABC'),
970 ),
971 ),
972 ),
973 ),
974 );
975
976 expect(
977 semantics,
978 hasSemantics(
979 TestSemantics.root(
980 children: <TestSemantics>[
981 TestSemantics.rootChild(
982 actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
983 label: 'ABC',
984 rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
985 transform: Matrix4.translationValues(356.0, 276.0, 0.0),
986 flags: <SemanticsFlag>[
987 SemanticsFlag.hasEnabledState,
988 SemanticsFlag.isButton,
989 SemanticsFlag.isEnabled,
990 SemanticsFlag.isFocusable,
991 ],
992 ),
993 ],
994 ),
995 ignoreId: true,
996 ),
997 );
998
999 semantics.dispose();
1000 });
1001
1002 testWidgets('ElevatedButton size is configurable by ThemeData.materialTapTargetSize', (
1003 WidgetTester tester,
1004 ) async {
1005 const ButtonStyle style = ButtonStyle(
1006 // Specifying minimumSize to mimic the original minimumSize for
1007 // RaisedButton so that the corresponding button size matches
1008 // the original version of this test.
1009 minimumSize: MaterialStatePropertyAll<Size>(Size(88, 36)),
1010 );
1011
1012 Widget buildFrame(MaterialTapTargetSize tapTargetSize, Key key) {
1013 return Theme(
1014 data: ThemeData(useMaterial3: false, materialTapTargetSize: tapTargetSize),
1015 child: Directionality(
1016 textDirection: TextDirection.ltr,
1017 child: Center(
1018 child: ElevatedButton(
1019 key: key,
1020 style: style,
1021 child: const SizedBox(width: 50.0, height: 8.0),
1022 onPressed: () {},
1023 ),
1024 ),
1025 ),
1026 );
1027 }
1028
1029 final Key key1 = UniqueKey();
1030 await tester.pumpWidget(buildFrame(MaterialTapTargetSize.padded, key1));
1031 expect(tester.getSize(find.byKey(key1)), const Size(88.0, 48.0));
1032
1033 final Key key2 = UniqueKey();
1034 await tester.pumpWidget(buildFrame(MaterialTapTargetSize.shrinkWrap, key2));
1035 expect(tester.getSize(find.byKey(key2)), const Size(88.0, 36.0));
1036 });
1037
1038 testWidgets('ElevatedButton has no clip by default', (WidgetTester tester) async {
1039 await tester.pumpWidget(
1040 Directionality(
1041 textDirection: TextDirection.ltr,
1042 child: ElevatedButton(
1043 onPressed: () {
1044 /* to make sure the button is enabled */
1045 },
1046 child: const Text('button'),
1047 ),
1048 ),
1049 );
1050
1051 expect(tester.renderObject(find.byType(ElevatedButton)), paintsExactlyCountTimes(#clipPath, 0));
1052 });
1053
1054 testWidgets('ElevatedButton responds to density changes.', (WidgetTester tester) async {
1055 const Key key = Key('test');
1056 const Key childKey = Key('test child');
1057
1058 Future<void> buildTest(VisualDensity visualDensity, {bool useText = false}) async {
1059 return tester.pumpWidget(
1060 MaterialApp(
1061 theme: ThemeData(useMaterial3: false),
1062 home: Directionality(
1063 textDirection: TextDirection.rtl,
1064 child: Center(
1065 child: ElevatedButton(
1066 style: ButtonStyle(
1067 visualDensity: visualDensity,
1068 // Specifying minimumSize to mimic the original minimumSize for
1069 // RaisedButton so that the corresponding button size matches
1070 // the original version of this test.
1071 minimumSize: const MaterialStatePropertyAll<Size>(Size(88, 36)),
1072 ),
1073 key: key,
1074 onPressed: () {},
1075 child: useText
1076 ? const Text('Text', key: childKey)
1077 : Container(
1078 key: childKey,
1079 width: 100,
1080 height: 100,
1081 color: const Color(0xffff0000),
1082 ),
1083 ),
1084 ),
1085 ),
1086 ),
1087 );
1088 }
1089
1090 await buildTest(VisualDensity.standard);
1091 final RenderBox box = tester.renderObject(find.byKey(key));
1092 Rect childRect = tester.getRect(find.byKey(childKey));
1093 await tester.pumpAndSettle();
1094 expect(box.size, equals(const Size(132, 100)));
1095 expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350)));
1096
1097 await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0));
1098 await tester.pumpAndSettle();
1099 childRect = tester.getRect(find.byKey(childKey));
1100 expect(box.size, equals(const Size(156, 124)));
1101 expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350)));
1102
1103 await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0));
1104 await tester.pumpAndSettle();
1105 childRect = tester.getRect(find.byKey(childKey));
1106 expect(box.size, equals(const Size(132, 100)));
1107 expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350)));
1108
1109 await buildTest(VisualDensity.standard, useText: true);
1110 await tester.pumpAndSettle();
1111 childRect = tester.getRect(find.byKey(childKey));
1112 expect(box.size, equals(const Size(88, 48)));
1113 expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0)));
1114
1115 await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0), useText: true);
1116 await tester.pumpAndSettle();
1117 childRect = tester.getRect(find.byKey(childKey));
1118 expect(box.size, equals(const Size(112, 60)));
1119 expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0)));
1120
1121 await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0), useText: true);
1122 await tester.pumpAndSettle();
1123 childRect = tester.getRect(find.byKey(childKey));
1124 expect(box.size, equals(const Size(88, 36)));
1125 expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0)));
1126 });
1127
1128 testWidgets('ElevatedButton.icon responds to applied padding', (WidgetTester tester) async {
1129 const Key buttonKey = Key('test');
1130 const Key labelKey = Key('label');
1131 await tester.pumpWidget(
1132 // When textDirection is set to TextDirection.ltr, the label appears on the
1133 // right side of the icon. This is important in determining whether the
1134 // horizontal padding is applied correctly later on
1135 Directionality(
1136 textDirection: TextDirection.ltr,
1137 child: Center(
1138 child: ElevatedButton.icon(
1139 key: buttonKey,
1140 style: const ButtonStyle(
1141 padding: MaterialStatePropertyAll<EdgeInsets>(EdgeInsets.fromLTRB(16, 5, 10, 12)),
1142 ),
1143 onPressed: () {},
1144 icon: const Icon(Icons.add),
1145 label: const Text('Hello', key: labelKey),
1146 ),
1147 ),
1148 ),
1149 );
1150
1151 final Rect paddingRect = tester.getRect(find.byType(Padding));
1152 final Rect labelRect = tester.getRect(find.byKey(labelKey));
1153 final Rect iconRect = tester.getRect(find.byType(Icon));
1154
1155 // The right padding should be applied on the right of the label, whereas the
1156 // left padding should be applied on the left side of the icon.
1157 expect(paddingRect.right, labelRect.right + 10);
1158 expect(paddingRect.left, iconRect.left - 16);
1159 // Use the taller widget to check the top and bottom padding.
1160 final Rect tallerWidget = iconRect.height > labelRect.height ? iconRect : labelRect;
1161 expect(paddingRect.top, closeTo(tallerWidget.top - 6.5, .01));
1162 expect(paddingRect.bottom, closeTo(tallerWidget.bottom + 13.5, .01));
1163 });
1164
1165 group('Default ElevatedButton padding for textScaleFactor, textDirection', () {
1166 const ValueKey<String> buttonKey = ValueKey<String>('button');
1167 const ValueKey<String> labelKey = ValueKey<String>('label');
1168 const ValueKey<String> iconKey = ValueKey<String>('icon');
1169
1170 const List<double> textScaleFactorOptions = <double>[0.5, 1.0, 1.25, 1.5, 2.0, 2.5, 3.0, 4.0];
1171 const List<TextDirection> textDirectionOptions = <TextDirection>[
1172 TextDirection.ltr,
1173 TextDirection.rtl,
1174 ];
1175 const List<Widget?> iconOptions = <Widget?>[null, Icon(Icons.add, size: 18, key: iconKey)];
1176
1177 // Expected values for each textScaleFactor.
1178 final Map<double, double> paddingWithoutIconStart = <double, double>{
1179 0.5: 16,
1180 1: 16,
1181 1.25: 14,
1182 1.5: 12,
1183 2: 8,
1184 2.5: 6,
1185 3: 4,
1186 4: 4,
1187 };
1188 final Map<double, double> paddingWithoutIconEnd = <double, double>{
1189 0.5: 16,
1190 1: 16,
1191 1.25: 14,
1192 1.5: 12,
1193 2: 8,
1194 2.5: 6,
1195 3: 4,
1196 4: 4,
1197 };
1198 final Map<double, double> paddingWithIconStart = <double, double>{
1199 0.5: 12,
1200 1: 12,
1201 1.25: 11,
1202 1.5: 10,
1203 2: 8,
1204 2.5: 8,
1205 3: 8,
1206 4: 8,
1207 };
1208 final Map<double, double> paddingWithIconEnd = <double, double>{
1209 0.5: 16,
1210 1: 16,
1211 1.25: 14,
1212 1.5: 12,
1213 2: 8,
1214 2.5: 6,
1215 3: 4,
1216 4: 4,
1217 };
1218 final Map<double, double> paddingWithIconGap = <double, double>{
1219 0.5: 8,
1220 1: 8,
1221 1.25: 7,
1222 1.5: 6,
1223 2: 4,
1224 2.5: 4,
1225 3: 4,
1226 4: 4,
1227 };
1228
1229 Rect globalBounds(RenderBox renderBox) {
1230 final Offset topLeft = renderBox.localToGlobal(Offset.zero);
1231 return topLeft & renderBox.size;
1232 }
1233
1234 /// Computes the padding between two [Rect]s, one inside the other.
1235 EdgeInsets paddingBetween({required Rect parent, required Rect child}) {
1236 assert(parent.intersect(child) == child);
1237 return EdgeInsets.fromLTRB(
1238 child.left - parent.left,
1239 child.top - parent.top,
1240 parent.right - child.right,
1241 parent.bottom - child.bottom,
1242 );
1243 }
1244
1245 for (final double textScaleFactor in textScaleFactorOptions) {
1246 for (final TextDirection textDirection in textDirectionOptions) {
1247 for (final Widget? icon in iconOptions) {
1248 final String testName = <String>[
1249 'ElevatedButton, text scale $textScaleFactor',
1250 if (icon != null) 'with icon',
1251 if (textDirection == TextDirection.rtl) 'RTL',
1252 ].join(', ');
1253 testWidgets(testName, (WidgetTester tester) async {
1254 await tester.pumpWidget(
1255 MaterialApp(
1256 theme: ThemeData(
1257 useMaterial3: false,
1258 colorScheme: const ColorScheme.light(),
1259 elevatedButtonTheme: ElevatedButtonThemeData(
1260 style: ElevatedButton.styleFrom(minimumSize: const Size(64, 36)),
1261 ),
1262 ),
1263 home: Builder(
1264 builder: (BuildContext context) {
1265 return MediaQuery.withClampedTextScaling(
1266 minScaleFactor: textScaleFactor,
1267 maxScaleFactor: textScaleFactor,
1268 child: Directionality(
1269 textDirection: textDirection,
1270 child: Scaffold(
1271 body: Center(
1272 child: icon == null
1273 ? ElevatedButton(
1274 key: buttonKey,
1275 onPressed: () {},
1276 child: const Text('button', key: labelKey),
1277 )
1278 : ElevatedButton.icon(
1279 key: buttonKey,
1280 onPressed: () {},
1281 icon: icon,
1282 label: const Text('button', key: labelKey),
1283 ),
1284 ),
1285 ),
1286 ),
1287 );
1288 },
1289 ),
1290 ),
1291 );
1292
1293 final Element paddingElement = tester.element(
1294 find.descendant(of: find.byKey(buttonKey), matching: find.byType(Padding)),
1295 );
1296 expect(Directionality.of(paddingElement), textDirection);
1297 final Padding paddingWidget = paddingElement.widget as Padding;
1298
1299 // Compute expected padding, and check.
1300
1301 final double expectedStart = icon != null
1302 ? paddingWithIconStart[textScaleFactor]!
1303 : paddingWithoutIconStart[textScaleFactor]!;
1304 final double expectedEnd = icon != null
1305 ? paddingWithIconEnd[textScaleFactor]!
1306 : paddingWithoutIconEnd[textScaleFactor]!;
1307 final EdgeInsets expectedPadding = EdgeInsetsDirectional.fromSTEB(
1308 expectedStart,
1309 0,
1310 expectedEnd,
1311 0,
1312 ).resolve(textDirection);
1313
1314 expect(paddingWidget.padding.resolve(textDirection), expectedPadding);
1315
1316 // Measure padding in terms of the difference between the button and its label child
1317 // and check that.
1318
1319 final RenderBox labelRenderBox = tester.renderObject<RenderBox>(find.byKey(labelKey));
1320 final Rect labelBounds = globalBounds(labelRenderBox);
1321 final RenderBox? iconRenderBox = icon == null
1322 ? null
1323 : tester.renderObject<RenderBox>(find.byKey(iconKey));
1324 final Rect? iconBounds = icon == null ? null : globalBounds(iconRenderBox!);
1325 final Rect childBounds = icon == null
1326 ? labelBounds
1327 : labelBounds.expandToInclude(iconBounds!);
1328
1329 // We measure the `InkResponse` descendant of the button
1330 // element, because the button has a larger `RenderBox`
1331 // which accommodates the minimum tap target with a height
1332 // of 48.
1333 final RenderBox buttonRenderBox = tester.renderObject<RenderBox>(
1334 find.descendant(
1335 of: find.byKey(buttonKey),
1336 matching: find.byWidgetPredicate((Widget widget) => widget is InkResponse),
1337 ),
1338 );
1339 final Rect buttonBounds = globalBounds(buttonRenderBox);
1340 final EdgeInsets visuallyMeasuredPadding = paddingBetween(
1341 parent: buttonBounds,
1342 child: childBounds,
1343 );
1344
1345 // Since there is a requirement of a minimum width of 64
1346 // and a minimum height of 36 on material buttons, the visual
1347 // padding of smaller buttons may not match their settings.
1348 // Therefore, we only test buttons that are large enough.
1349 if (buttonBounds.width > 64) {
1350 expect(visuallyMeasuredPadding.left, expectedPadding.left);
1351 expect(visuallyMeasuredPadding.right, expectedPadding.right);
1352 }
1353
1354 if (buttonBounds.height > 36) {
1355 expect(visuallyMeasuredPadding.top, expectedPadding.top);
1356 expect(visuallyMeasuredPadding.bottom, expectedPadding.bottom);
1357 }
1358
1359 // Check the gap between the icon and the label
1360 if (icon != null) {
1361 final double gapWidth = textDirection == TextDirection.ltr
1362 ? labelBounds.left - iconBounds!.right
1363 : iconBounds!.left - labelBounds.right;
1364 expect(gapWidth, paddingWithIconGap[textScaleFactor]);
1365 }
1366
1367 // Check the text's height - should be consistent with the textScaleFactor.
1368 final RenderBox textRenderObject = tester.renderObject<RenderBox>(
1369 find.descendant(
1370 of: find.byKey(labelKey),
1371 matching: find.byElementPredicate((Element element) => element.widget is RichText),
1372 ),
1373 );
1374 final double textHeight = textRenderObject.paintBounds.size.height;
1375 final double expectedTextHeight = 14 * textScaleFactor;
1376 expect(textHeight, moreOrLessEquals(expectedTextHeight, epsilon: 0.5));
1377 });
1378 }
1379 }
1380 }
1381 });
1382
1383 testWidgets('Override theme fontSize changes padding', (WidgetTester tester) async {
1384 await tester.pumpWidget(
1385 MaterialApp(
1386 theme: ThemeData.from(
1387 colorScheme: const ColorScheme.light(),
1388 textTheme: const TextTheme(labelLarge: TextStyle(fontSize: 28.0)),
1389 ),
1390 home: Builder(
1391 builder: (BuildContext context) {
1392 return Scaffold(
1393 body: Center(
1394 child: ElevatedButton(onPressed: () {}, child: const Text('text')),
1395 ),
1396 );
1397 },
1398 ),
1399 ),
1400 );
1401
1402 final Padding paddingWidget = tester.widget<Padding>(
1403 find.descendant(of: find.byType(ElevatedButton), matching: find.byType(Padding)),
1404 );
1405 expect(paddingWidget.padding, const EdgeInsets.symmetric(horizontal: 12));
1406 });
1407
1408 testWidgets('Override ElevatedButton default padding', (WidgetTester tester) async {
1409 await tester.pumpWidget(
1410 MaterialApp(
1411 theme: ThemeData.from(colorScheme: const ColorScheme.light()),
1412 home: Builder(
1413 builder: (BuildContext context) {
1414 return MediaQuery.withClampedTextScaling(
1415 minScaleFactor: 2,
1416 maxScaleFactor: 2,
1417 child: Scaffold(
1418 body: Center(
1419 child: ElevatedButton(
1420 style: ElevatedButton.styleFrom(padding: const EdgeInsets.all(22)),
1421 onPressed: () {},
1422 child: const Text('ElevatedButton'),
1423 ),
1424 ),
1425 ),
1426 );
1427 },
1428 ),
1429 ),
1430 );
1431
1432 final Padding paddingWidget = tester.widget<Padding>(
1433 find.descendant(of: find.byType(ElevatedButton), matching: find.byType(Padding)),
1434 );
1435 expect(paddingWidget.padding, const EdgeInsets.all(22));
1436 });
1437
1438 testWidgets('M3 ElevatedButton has correct padding', (WidgetTester tester) async {
1439 final Key key = UniqueKey();
1440 await tester.pumpWidget(
1441 MaterialApp(
1442 theme: ThemeData.from(colorScheme: const ColorScheme.light()),
1443 home: Scaffold(
1444 body: Center(
1445 child: ElevatedButton(key: key, onPressed: () {}, child: const Text('ElevatedButton')),
1446 ),
1447 ),
1448 ),
1449 );
1450
1451 final Padding paddingWidget = tester.widget<Padding>(
1452 find.descendant(of: find.byKey(key), matching: find.byType(Padding)),
1453 );
1454 expect(paddingWidget.padding, const EdgeInsets.symmetric(horizontal: 24));
1455 });
1456
1457 testWidgets('M3 ElevatedButton.icon has correct padding', (WidgetTester tester) async {
1458 final Key key = UniqueKey();
1459 await tester.pumpWidget(
1460 MaterialApp(
1461 theme: ThemeData.from(colorScheme: const ColorScheme.light()),
1462 home: Scaffold(
1463 body: Center(
1464 child: ElevatedButton.icon(
1465 key: key,
1466 icon: const Icon(Icons.favorite),
1467 onPressed: () {},
1468 label: const Text('ElevatedButton'),
1469 ),
1470 ),
1471 ),
1472 ),
1473 );
1474
1475 final Padding paddingWidget = tester.widget<Padding>(
1476 find.descendant(of: find.byKey(key), matching: find.byType(Padding)),
1477 );
1478 expect(paddingWidget.padding, const EdgeInsetsDirectional.fromSTEB(16.0, 0.0, 24.0, 0.0));
1479 });
1480
1481 testWidgets('Elevated buttons animate elevation before color on disable', (
1482 WidgetTester tester,
1483 ) async {
1484 // This is a regression test for https://github.com/flutter/flutter/issues/387
1485
1486 const ColorScheme colorScheme = ColorScheme.light();
1487 final Color backgroundColor = colorScheme.primary;
1488 final Color disabledBackgroundColor = colorScheme.onSurface.withOpacity(0.12);
1489
1490 Widget buildFrame({required bool enabled}) {
1491 return MaterialApp(
1492 theme: ThemeData.from(colorScheme: colorScheme, useMaterial3: false),
1493 home: Center(
1494 child: ElevatedButton(onPressed: enabled ? () {} : null, child: const Text('button')),
1495 ),
1496 );
1497 }
1498
1499 PhysicalShape physicalShape() {
1500 return tester.widget<PhysicalShape>(
1501 find.descendant(of: find.byType(ElevatedButton), matching: find.byType(PhysicalShape)),
1502 );
1503 }
1504
1505 // Default elevation is 2, background color is primary.
1506 await tester.pumpWidget(buildFrame(enabled: true));
1507 expect(physicalShape().elevation, 2);
1508 expect(physicalShape().color, backgroundColor);
1509
1510 // Disabled elevation animates to 0 over 200ms, THEN the background
1511 // color changes to onSurface.withOpacity(0.12)
1512 await tester.pumpWidget(buildFrame(enabled: false));
1513 await tester.pump(const Duration(milliseconds: 50));
1514 expect(physicalShape().elevation, lessThan(2));
1515 expect(physicalShape().color, backgroundColor);
1516 await tester.pump(const Duration(milliseconds: 150));
1517 expect(physicalShape().elevation, 0);
1518 expect(physicalShape().color, backgroundColor);
1519 await tester.pumpAndSettle();
1520 expect(physicalShape().elevation, 0);
1521 expect(physicalShape().color, disabledBackgroundColor);
1522 });
1523
1524 testWidgets('By default, ElevatedButton shape outline is defined by shape.side', (
1525 WidgetTester tester,
1526 ) async {
1527 // This is a regression test for https://github.com/flutter/flutter/issues/69544
1528
1529 const Color borderColor = Color(0xff4caf50);
1530 await tester.pumpWidget(
1531 MaterialApp(
1532 theme: ThemeData(
1533 colorScheme: const ColorScheme.light(),
1534 textTheme: Typography.englishLike2014,
1535 useMaterial3: false,
1536 ),
1537 home: Center(
1538 child: ElevatedButton(
1539 style: ElevatedButton.styleFrom(
1540 shape: const RoundedRectangleBorder(
1541 borderRadius: BorderRadius.all(Radius.circular(16)),
1542 side: BorderSide(width: 10, color: borderColor),
1543 ),
1544 minimumSize: const Size(64, 36),
1545 ),
1546 onPressed: () {},
1547 child: const Text('button'),
1548 ),
1549 ),
1550 ),
1551 );
1552
1553 expect(
1554 find.byType(ElevatedButton),
1555 paints..drrect(
1556 // Outer and inner rect that give the outline a width of 10.
1557 outer: RRect.fromLTRBR(0.0, 0.0, 116.0, 36.0, const Radius.circular(16)),
1558 inner: RRect.fromLTRBR(10.0, 10.0, 106.0, 26.0, const Radius.circular(16 - 10)),
1559 color: borderColor,
1560 ),
1561 );
1562 });
1563
1564 testWidgets('Fixed size ElevatedButtons', (WidgetTester tester) async {
1565 await tester.pumpWidget(
1566 MaterialApp(
1567 home: Scaffold(
1568 body: Column(
1569 mainAxisSize: MainAxisSize.min,
1570 children: <Widget>[
1571 ElevatedButton(
1572 style: ElevatedButton.styleFrom(fixedSize: const Size(100, 100)),
1573 onPressed: () {},
1574 child: const Text('100x100'),
1575 ),
1576 ElevatedButton(
1577 style: ElevatedButton.styleFrom(fixedSize: const Size.fromWidth(200)),
1578 onPressed: () {},
1579 child: const Text('200xh'),
1580 ),
1581 ElevatedButton(
1582 style: ElevatedButton.styleFrom(fixedSize: const Size.fromHeight(200)),
1583 onPressed: () {},
1584 child: const Text('wx200'),
1585 ),
1586 ],
1587 ),
1588 ),
1589 ),
1590 );
1591
1592 expect(tester.getSize(find.widgetWithText(ElevatedButton, '100x100')), const Size(100, 100));
1593 expect(tester.getSize(find.widgetWithText(ElevatedButton, '200xh')).width, 200);
1594 expect(tester.getSize(find.widgetWithText(ElevatedButton, 'wx200')).height, 200);
1595 });
1596
1597 testWidgets('ElevatedButton with NoSplash splashFactory paints nothing', (
1598 WidgetTester tester,
1599 ) async {
1600 Widget buildFrame({InteractiveInkFeatureFactory? splashFactory}) {
1601 return MaterialApp(
1602 home: Scaffold(
1603 body: Center(
1604 child: ElevatedButton(
1605 style: ElevatedButton.styleFrom(splashFactory: splashFactory),
1606 onPressed: () {},
1607 child: const Text('test'),
1608 ),
1609 ),
1610 ),
1611 );
1612 }
1613
1614 // NoSplash.splashFactory, no splash circles drawn
1615 await tester.pumpWidget(buildFrame(splashFactory: NoSplash.splashFactory));
1616 {
1617 final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('test')));
1618 final MaterialInkController material = Material.of(tester.element(find.text('test')));
1619 await tester.pump(const Duration(milliseconds: 200));
1620 expect(material, paintsExactlyCountTimes(#drawCircle, 0));
1621 await gesture.up();
1622 await tester.pumpAndSettle();
1623 }
1624
1625 // InkRipple.splashFactory, one splash circle drawn.
1626 await tester.pumpWidget(buildFrame(splashFactory: InkRipple.splashFactory));
1627 {
1628 final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('test')));
1629 final MaterialInkController material = Material.of(tester.element(find.text('test')));
1630 await tester.pump(const Duration(milliseconds: 200));
1631 expect(material, paintsExactlyCountTimes(#drawCircle, 1));
1632 await gesture.up();
1633 await tester.pumpAndSettle();
1634 }
1635 });
1636
1637 testWidgets(
1638 'ElevatedButton uses InkSparkle only for Android non-web when useMaterial3 is true',
1639 (WidgetTester tester) async {
1640 final ThemeData theme = ThemeData();
1641
1642 await tester.pumpWidget(
1643 MaterialApp(
1644 theme: theme,
1645 home: Center(
1646 child: ElevatedButton(onPressed: () {}, child: const Text('button')),
1647 ),
1648 ),
1649 );
1650
1651 final InkWell buttonInkWell = tester.widget<InkWell>(
1652 find.descendant(of: find.byType(ElevatedButton), matching: find.byType(InkWell)),
1653 );
1654
1655 if (debugDefaultTargetPlatformOverride! == TargetPlatform.android && !kIsWeb) {
1656 expect(buttonInkWell.splashFactory, equals(InkSparkle.splashFactory));
1657 } else {
1658 expect(buttonInkWell.splashFactory, equals(InkRipple.splashFactory));
1659 }
1660 },
1661 variant: TargetPlatformVariant.all(),
1662 );
1663
1664 testWidgets('ElevatedButton uses InkRipple when useMaterial3 is false', (
1665 WidgetTester tester,
1666 ) async {
1667 final ThemeData theme = ThemeData(useMaterial3: false);
1668
1669 await tester.pumpWidget(
1670 MaterialApp(
1671 theme: theme,
1672 home: Center(
1673 child: ElevatedButton(onPressed: () {}, child: const Text('button')),
1674 ),
1675 ),
1676 );
1677
1678 final InkWell buttonInkWell = tester.widget<InkWell>(
1679 find.descendant(of: find.byType(ElevatedButton), matching: find.byType(InkWell)),
1680 );
1681 expect(buttonInkWell.splashFactory, equals(InkRipple.splashFactory));
1682 }, variant: TargetPlatformVariant.all());
1683
1684 testWidgets('ElevatedButton.icon does not overflow', (WidgetTester tester) async {
1685 // Regression test for https://github.com/flutter/flutter/issues/77815
1686 await tester.pumpWidget(
1687 MaterialApp(
1688 home: Scaffold(
1689 body: SizedBox(
1690 width: 200,
1691 child: ElevatedButton.icon(
1692 onPressed: () {},
1693 icon: const Icon(Icons.add),
1694 label: const Text(
1695 // Much wider than 200
1696 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut a euismod nibh. Morbi laoreet purus.',
1697 ),
1698 ),
1699 ),
1700 ),
1701 ),
1702 );
1703 expect(tester.takeException(), null);
1704 });
1705
1706 testWidgets('ElevatedButton.icon icon,label layout', (WidgetTester tester) async {
1707 final Key buttonKey = UniqueKey();
1708 final Key iconKey = UniqueKey();
1709 final Key labelKey = UniqueKey();
1710 final ButtonStyle style = ElevatedButton.styleFrom(
1711 padding: EdgeInsets.zero,
1712 visualDensity: VisualDensity.standard, // dx=0, dy=0
1713 );
1714
1715 await tester.pumpWidget(
1716 MaterialApp(
1717 home: Scaffold(
1718 body: SizedBox(
1719 width: 200,
1720 child: ElevatedButton.icon(
1721 key: buttonKey,
1722 style: style,
1723 onPressed: () {},
1724 icon: SizedBox(key: iconKey, width: 50, height: 100),
1725 label: SizedBox(key: labelKey, width: 50, height: 100),
1726 ),
1727 ),
1728 ),
1729 ),
1730 );
1731
1732 // The button's label and icon are separated by a gap of 8:
1733 // 46 [icon 50] 8 [label 50] 46
1734 // The overall button width is 200. So:
1735 // icon.x = 46
1736 // label.x = 46 + 50 + 8 = 104
1737
1738 expect(tester.getRect(find.byKey(buttonKey)), const Rect.fromLTRB(0.0, 0.0, 200.0, 100.0));
1739 expect(tester.getRect(find.byKey(iconKey)), const Rect.fromLTRB(46.0, 0.0, 96.0, 100.0));
1740 expect(tester.getRect(find.byKey(labelKey)), const Rect.fromLTRB(104.0, 0.0, 154.0, 100.0));
1741 });
1742
1743 testWidgets('ElevatedButton maximumSize', (WidgetTester tester) async {
1744 final Key key0 = UniqueKey();
1745 final Key key1 = UniqueKey();
1746
1747 await tester.pumpWidget(
1748 MaterialApp(
1749 theme: ThemeData(textTheme: Typography.englishLike2014),
1750 home: Scaffold(
1751 body: Center(
1752 child: Column(
1753 mainAxisSize: MainAxisSize.min,
1754 children: <Widget>[
1755 ElevatedButton(
1756 key: key0,
1757 style: ElevatedButton.styleFrom(
1758 minimumSize: const Size(24, 36),
1759 maximumSize: const Size.fromWidth(64),
1760 ),
1761 onPressed: () {},
1762 child: const Text('A B C D E F G H I J K L M N O P'),
1763 ),
1764 ElevatedButton.icon(
1765 key: key1,
1766 style: ElevatedButton.styleFrom(
1767 minimumSize: const Size(24, 36),
1768 maximumSize: const Size.fromWidth(104),
1769 ),
1770 onPressed: () {},
1771 icon: Container(color: Colors.red, width: 32, height: 32),
1772 label: const Text('A B C D E F G H I J K L M N O P'),
1773 ),
1774 ],
1775 ),
1776 ),
1777 ),
1778 ),
1779 );
1780
1781 expect(tester.getSize(find.byKey(key0)), const Size(64.0, 224.0));
1782 expect(tester.getSize(find.byKey(key1)), const Size(104.0, 224.0));
1783 });
1784
1785 testWidgets('Fixed size ElevatedButton, same as minimumSize == maximumSize', (
1786 WidgetTester tester,
1787 ) async {
1788 await tester.pumpWidget(
1789 MaterialApp(
1790 home: Scaffold(
1791 body: Column(
1792 mainAxisSize: MainAxisSize.min,
1793 children: <Widget>[
1794 ElevatedButton(
1795 style: ElevatedButton.styleFrom(fixedSize: const Size(200, 200)),
1796 onPressed: () {},
1797 child: const Text('200x200'),
1798 ),
1799 ElevatedButton(
1800 style: ElevatedButton.styleFrom(
1801 minimumSize: const Size(200, 200),
1802 maximumSize: const Size(200, 200),
1803 ),
1804 onPressed: () {},
1805 child: const Text('200,200'),
1806 ),
1807 ],
1808 ),
1809 ),
1810 ),
1811 );
1812
1813 expect(tester.getSize(find.widgetWithText(ElevatedButton, '200x200')), const Size(200, 200));
1814 expect(tester.getSize(find.widgetWithText(ElevatedButton, '200,200')), const Size(200, 200));
1815 });
1816
1817 testWidgets('ElevatedButton changes mouse cursor when hovered', (WidgetTester tester) async {
1818 await tester.pumpWidget(
1819 Directionality(
1820 textDirection: TextDirection.ltr,
1821 child: MouseRegion(
1822 cursor: SystemMouseCursors.forbidden,
1823 child: ElevatedButton(
1824 style: ElevatedButton.styleFrom(
1825 enabledMouseCursor: SystemMouseCursors.text,
1826 disabledMouseCursor: SystemMouseCursors.grab,
1827 ),
1828 onPressed: () {},
1829 child: const Text('button'),
1830 ),
1831 ),
1832 ),
1833 );
1834
1835 final TestGesture gesture = await tester.createGesture(
1836 kind: PointerDeviceKind.mouse,
1837 pointer: 1,
1838 );
1839 await gesture.addPointer(location: Offset.zero);
1840
1841 await tester.pump();
1842
1843 expect(
1844 RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
1845 SystemMouseCursors.text,
1846 );
1847
1848 // Test cursor when disabled
1849 await tester.pumpWidget(
1850 Directionality(
1851 textDirection: TextDirection.ltr,
1852 child: MouseRegion(
1853 cursor: SystemMouseCursors.forbidden,
1854 child: ElevatedButton(
1855 style: ElevatedButton.styleFrom(
1856 enabledMouseCursor: SystemMouseCursors.text,
1857 disabledMouseCursor: SystemMouseCursors.grab,
1858 ),
1859 onPressed: null,
1860 child: const Text('button'),
1861 ),
1862 ),
1863 ),
1864 );
1865
1866 expect(
1867 RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
1868 SystemMouseCursors.grab,
1869 );
1870
1871 // Test default cursor
1872 await tester.pumpWidget(
1873 Directionality(
1874 textDirection: TextDirection.ltr,
1875 child: MouseRegion(
1876 cursor: SystemMouseCursors.forbidden,
1877 child: ElevatedButton(onPressed: () {}, child: const Text('button')),
1878 ),
1879 ),
1880 );
1881
1882 expect(
1883 RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
1884 SystemMouseCursors.click,
1885 );
1886
1887 // Test default cursor when disabled
1888 await tester.pumpWidget(
1889 const Directionality(
1890 textDirection: TextDirection.ltr,
1891 child: MouseRegion(
1892 cursor: SystemMouseCursors.forbidden,
1893 child: ElevatedButton(onPressed: null, child: Text('button')),
1894 ),
1895 ),
1896 );
1897
1898 expect(
1899 RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
1900 SystemMouseCursors.basic,
1901 );
1902 });
1903
1904 testWidgets('ElevatedButton in SelectionArea changes mouse cursor when hovered', (
1905 WidgetTester tester,
1906 ) async {
1907 // Regression test for https://github.com/flutter/flutter/issues/104595.
1908 await tester.pumpWidget(
1909 MaterialApp(
1910 home: SelectionArea(
1911 child: ElevatedButton(
1912 style: ElevatedButton.styleFrom(
1913 enabledMouseCursor: SystemMouseCursors.click,
1914 disabledMouseCursor: SystemMouseCursors.grab,
1915 ),
1916 onPressed: () {},
1917 child: const Text('button'),
1918 ),
1919 ),
1920 ),
1921 );
1922
1923 final TestGesture gesture = await tester.createGesture(
1924 kind: PointerDeviceKind.mouse,
1925 pointer: 1,
1926 );
1927 await gesture.addPointer(location: tester.getCenter(find.byType(Text)));
1928
1929 await tester.pump();
1930
1931 expect(
1932 RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
1933 SystemMouseCursors.click,
1934 );
1935 });
1936
1937 testWidgets('Ink Response shape matches Material shape', (WidgetTester tester) async {
1938 // This is a regression test for https://github.com/flutter/flutter/issues/91844
1939
1940 Widget buildFrame({BorderSide? side}) {
1941 return MaterialApp(
1942 home: Scaffold(
1943 body: Center(
1944 child: ElevatedButton(
1945 style: ElevatedButton.styleFrom(
1946 side: side,
1947 shape: const RoundedRectangleBorder(
1948 side: BorderSide(color: Color(0xff0000ff), width: 0),
1949 ),
1950 ),
1951 onPressed: () {},
1952 child: const Text('ElevatedButton'),
1953 ),
1954 ),
1955 ),
1956 );
1957 }
1958
1959 const BorderSide borderSide = BorderSide(width: 10, color: Color(0xff00ff00));
1960 await tester.pumpWidget(buildFrame(side: borderSide));
1961 expect(
1962 tester.widget<InkWell>(find.byType(InkWell)).customBorder,
1963 const RoundedRectangleBorder(side: borderSide),
1964 );
1965
1966 await tester.pumpWidget(buildFrame());
1967 await tester.pumpAndSettle();
1968 expect(
1969 tester.widget<InkWell>(find.byType(InkWell)).customBorder,
1970 const RoundedRectangleBorder(side: BorderSide(color: Color(0xff0000ff), width: 0.0)),
1971 );
1972 });
1973
1974 testWidgets('ElevatedButton.styleFrom can be used to set foreground and background colors', (
1975 WidgetTester tester,
1976 ) async {
1977 await tester.pumpWidget(
1978 MaterialApp(
1979 home: Scaffold(
1980 body: ElevatedButton(
1981 style: ElevatedButton.styleFrom(
1982 foregroundColor: Colors.white,
1983 backgroundColor: Colors.purple,
1984 ),
1985 onPressed: () {},
1986 child: const Text('button'),
1987 ),
1988 ),
1989 ),
1990 );
1991
1992 final Material material = tester.widget<Material>(
1993 find.descendant(of: find.byType(ElevatedButton), matching: find.byType(Material)),
1994 );
1995 expect(material.color, Colors.purple);
1996 expect(material.textStyle!.color, Colors.white);
1997 });
1998
1999 Future<void> testStatesController(Widget? icon, WidgetTester tester) async {
2000 int count = 0;
2001 void valueChanged() {
2002 count += 1;
2003 }
2004
2005 final MaterialStatesController controller = MaterialStatesController();
2006 addTearDown(controller.dispose);
2007 controller.addListener(valueChanged);
2008
2009 await tester.pumpWidget(
2010 MaterialApp(
2011 home: Center(
2012 child: icon == null
2013 ? ElevatedButton(
2014 statesController: controller,
2015 onPressed: () {},
2016 child: const Text('button'),
2017 )
2018 : ElevatedButton.icon(
2019 statesController: controller,
2020 onPressed: () {},
2021 icon: icon,
2022 label: const Text('button'),
2023 ),
2024 ),
2025 ),
2026 );
2027
2028 expect(controller.value, <MaterialState>{});
2029 expect(count, 0);
2030
2031 final Offset center = tester.getCenter(find.byType(Text));
2032 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
2033 await gesture.addPointer();
2034 await gesture.moveTo(center);
2035 await tester.pumpAndSettle();
2036
2037 expect(controller.value, <MaterialState>{MaterialState.hovered});
2038 expect(count, 1);
2039
2040 await gesture.moveTo(Offset.zero);
2041 await tester.pumpAndSettle();
2042
2043 expect(controller.value, <MaterialState>{});
2044 expect(count, 2);
2045
2046 await gesture.moveTo(center);
2047 await tester.pumpAndSettle();
2048
2049 expect(controller.value, <MaterialState>{MaterialState.hovered});
2050 expect(count, 3);
2051
2052 await gesture.down(center);
2053 await tester.pumpAndSettle();
2054
2055 expect(controller.value, <MaterialState>{MaterialState.hovered, MaterialState.pressed});
2056 expect(count, 4);
2057
2058 await gesture.up();
2059 await tester.pumpAndSettle();
2060
2061 expect(controller.value, <MaterialState>{MaterialState.hovered});
2062 expect(count, 5);
2063
2064 await gesture.moveTo(Offset.zero);
2065 await tester.pumpAndSettle();
2066
2067 expect(controller.value, <MaterialState>{});
2068 expect(count, 6);
2069
2070 await gesture.down(center);
2071 await tester.pumpAndSettle();
2072 expect(controller.value, <MaterialState>{MaterialState.hovered, MaterialState.pressed});
2073 expect(count, 8); // adds hovered and pressed - two changes
2074
2075 // If the button is rebuilt disabled, then the pressed state is
2076 // removed.
2077 await tester.pumpWidget(
2078 MaterialApp(
2079 home: Center(
2080 child: icon == null
2081 ? ElevatedButton(
2082 statesController: controller,
2083 onPressed: null,
2084 child: const Text('button'),
2085 )
2086 : ElevatedButton.icon(
2087 statesController: controller,
2088 onPressed: null,
2089 icon: icon,
2090 label: const Text('button'),
2091 ),
2092 ),
2093 ),
2094 );
2095 await tester.pumpAndSettle();
2096 expect(controller.value, <MaterialState>{MaterialState.hovered, MaterialState.disabled});
2097 expect(count, 10); // removes pressed and adds disabled - two changes
2098 await gesture.moveTo(Offset.zero);
2099 await tester.pumpAndSettle();
2100 expect(controller.value, <MaterialState>{MaterialState.disabled});
2101 expect(count, 11);
2102 await gesture.removePointer();
2103 }
2104
2105 testWidgets('ElevatedButton statesController', (WidgetTester tester) async {
2106 testStatesController(null, tester);
2107 });
2108
2109 testWidgets('ElevatedButton.icon statesController', (WidgetTester tester) async {
2110 testStatesController(const Icon(Icons.add), tester);
2111 });
2112
2113 testWidgets('Disabled ElevatedButton statesController', (WidgetTester tester) async {
2114 int count = 0;
2115 void valueChanged() {
2116 count += 1;
2117 }
2118
2119 final MaterialStatesController controller = MaterialStatesController();
2120 addTearDown(controller.dispose);
2121 controller.addListener(valueChanged);
2122
2123 await tester.pumpWidget(
2124 MaterialApp(
2125 home: Center(
2126 child: ElevatedButton(
2127 statesController: controller,
2128 onPressed: null,
2129 child: const Text('button'),
2130 ),
2131 ),
2132 ),
2133 );
2134 expect(controller.value, <MaterialState>{MaterialState.disabled});
2135 expect(count, 1);
2136 });
2137
2138 testWidgets('ElevatedButton backgroundBuilder and foregroundBuilder', (
2139 WidgetTester tester,
2140 ) async {
2141 const Color backgroundColor = Color(0xFF000011);
2142 const Color foregroundColor = Color(0xFF000022);
2143
2144 await tester.pumpWidget(
2145 Directionality(
2146 textDirection: TextDirection.ltr,
2147 child: ElevatedButton(
2148 style: ElevatedButton.styleFrom(
2149 backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
2150 return DecoratedBox(
2151 decoration: const BoxDecoration(color: backgroundColor),
2152 child: child,
2153 );
2154 },
2155 foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
2156 return DecoratedBox(
2157 decoration: const BoxDecoration(color: foregroundColor),
2158 child: child,
2159 );
2160 },
2161 ),
2162 onPressed: () {},
2163 child: const Text('button'),
2164 ),
2165 ),
2166 );
2167
2168 BoxDecoration boxDecorationOf(Finder finder) {
2169 return tester.widget<DecoratedBox>(finder).decoration as BoxDecoration;
2170 }
2171
2172 final Finder decorations = find.descendant(
2173 of: find.byType(ElevatedButton),
2174 matching: find.byType(DecoratedBox),
2175 );
2176
2177 expect(boxDecorationOf(decorations.at(0)).color, backgroundColor);
2178 expect(boxDecorationOf(decorations.at(1)).color, foregroundColor);
2179
2180 Text textChildOf(Finder finder) {
2181 return tester.widget<Text>(find.descendant(of: finder, matching: find.byType(Text)));
2182 }
2183
2184 expect(textChildOf(decorations.at(0)).data, 'button');
2185 expect(textChildOf(decorations.at(1)).data, 'button');
2186 });
2187
2188 testWidgets(
2189 'ElevatedButton backgroundBuilder drops button child and foregroundBuilder return value',
2190 (WidgetTester tester) async {
2191 const Color backgroundColor = Color(0xFF000011);
2192 const Color foregroundColor = Color(0xFF000022);
2193
2194 await tester.pumpWidget(
2195 Directionality(
2196 textDirection: TextDirection.ltr,
2197 child: ElevatedButton(
2198 style: ElevatedButton.styleFrom(
2199 backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
2200 return const DecoratedBox(decoration: BoxDecoration(color: backgroundColor));
2201 },
2202 foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
2203 return const DecoratedBox(decoration: BoxDecoration(color: foregroundColor));
2204 },
2205 ),
2206 onPressed: () {},
2207 child: const Text('button'),
2208 ),
2209 ),
2210 );
2211
2212 final Finder background = find.descendant(
2213 of: find.byType(ElevatedButton),
2214 matching: find.byType(DecoratedBox),
2215 );
2216
2217 expect(background, findsOneWidget);
2218 expect(find.text('button'), findsNothing);
2219 },
2220 );
2221
2222 testWidgets('ElevatedButton foregroundBuilder drops button child', (WidgetTester tester) async {
2223 const Color foregroundColor = Color(0xFF000022);
2224
2225 await tester.pumpWidget(
2226 Directionality(
2227 textDirection: TextDirection.ltr,
2228 child: ElevatedButton(
2229 style: ElevatedButton.styleFrom(
2230 foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
2231 return const DecoratedBox(decoration: BoxDecoration(color: foregroundColor));
2232 },
2233 ),
2234 onPressed: () {},
2235 child: const Text('button'),
2236 ),
2237 ),
2238 );
2239
2240 final Finder foreground = find.descendant(
2241 of: find.byType(ElevatedButton),
2242 matching: find.byType(DecoratedBox),
2243 );
2244
2245 expect(foreground, findsOneWidget);
2246 expect(find.text('button'), findsNothing);
2247 });
2248
2249 testWidgets(
2250 'ElevatedButton foreground and background builders are applied to the correct states',
2251 (WidgetTester tester) async {
2252 Set<MaterialState> foregroundStates = <MaterialState>{};
2253 Set<MaterialState> backgroundStates = <MaterialState>{};
2254 final FocusNode focusNode = FocusNode();
2255
2256 await tester.pumpWidget(
2257 MaterialApp(
2258 home: Scaffold(
2259 body: Center(
2260 child: ElevatedButton(
2261 style: ButtonStyle(
2262 backgroundBuilder:
2263 (BuildContext context, Set<MaterialState> states, Widget? child) {
2264 backgroundStates = states;
2265 return child!;
2266 },
2267 foregroundBuilder:
2268 (BuildContext context, Set<MaterialState> states, Widget? child) {
2269 foregroundStates = states;
2270 return child!;
2271 },
2272 ),
2273 onPressed: () {},
2274 focusNode: focusNode,
2275 child: const Text('button'),
2276 ),
2277 ),
2278 ),
2279 ),
2280 );
2281
2282 // Default.
2283 expect(backgroundStates.isEmpty, isTrue);
2284 expect(foregroundStates.isEmpty, isTrue);
2285
2286 const Set<MaterialState> focusedStates = <MaterialState>{MaterialState.focused};
2287 const Set<MaterialState> focusedHoveredStates = <MaterialState>{
2288 MaterialState.focused,
2289 MaterialState.hovered,
2290 };
2291 const Set<MaterialState> focusedHoveredPressedStates = <MaterialState>{
2292 MaterialState.focused,
2293 MaterialState.hovered,
2294 MaterialState.pressed,
2295 };
2296
2297 bool sameStates(Set<MaterialState> expectedValue, Set<MaterialState> actualValue) {
2298 return expectedValue.difference(actualValue).isEmpty &&
2299 actualValue.difference(expectedValue).isEmpty;
2300 }
2301
2302 // Focused.
2303 focusNode.requestFocus();
2304 await tester.pumpAndSettle();
2305 expect(sameStates(focusedStates, backgroundStates), isTrue);
2306 expect(sameStates(focusedStates, foregroundStates), isTrue);
2307
2308 // Hovered.
2309 final Offset center = tester.getCenter(find.byType(ElevatedButton));
2310 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
2311 await gesture.addPointer();
2312 await gesture.moveTo(center);
2313 await tester.pumpAndSettle();
2314 expect(sameStates(focusedHoveredStates, backgroundStates), isTrue);
2315 expect(sameStates(focusedHoveredStates, foregroundStates), isTrue);
2316
2317 // Highlighted (pressed).
2318 await gesture.down(center);
2319 await tester.pump(); // Start the splash and highlight animations.
2320 await tester.pump(
2321 const Duration(milliseconds: 800),
2322 ); // Wait for splash and highlight to be well under way.
2323 expect(sameStates(focusedHoveredPressedStates, backgroundStates), isTrue);
2324 expect(sameStates(focusedHoveredPressedStates, foregroundStates), isTrue);
2325
2326 focusNode.dispose();
2327 },
2328 );
2329
2330 testWidgets('Default ElevatedButton icon alignment', (WidgetTester tester) async {
2331 Widget buildWidget({required TextDirection textDirection}) {
2332 return MaterialApp(
2333 home: Directionality(
2334 textDirection: textDirection,
2335 child: Center(
2336 child: ElevatedButton.icon(
2337 onPressed: () {},
2338 icon: const Icon(Icons.add),
2339 label: const Text('button'),
2340 ),
2341 ),
2342 ),
2343 );
2344 }
2345
2346 // Test default iconAlignment when textDirection is ltr.
2347 await tester.pumpWidget(buildWidget(textDirection: TextDirection.ltr));
2348
2349 final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last);
2350 final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add));
2351
2352 // The icon is aligned to the left of the button.
2353 expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); // 16.0 - padding between icon and button edge.
2354
2355 // Test default iconAlignment when textDirection is rtl.
2356 await tester.pumpWidget(buildWidget(textDirection: TextDirection.rtl));
2357
2358 final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
2359 final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
2360
2361 // The icon is aligned to the right of the button.
2362 expect(
2363 buttonTopRight.dx,
2364 iconTopRight.dx + 16.0,
2365 ); // 16.0 - padding between icon and button edge.
2366 });
2367
2368 testWidgets('ElevatedButton icon alignment can be customized', (WidgetTester tester) async {
2369 Widget buildWidget({
2370 required TextDirection textDirection,
2371 required IconAlignment iconAlignment,
2372 }) {
2373 return MaterialApp(
2374 home: Directionality(
2375 textDirection: textDirection,
2376 child: Center(
2377 child: ElevatedButton.icon(
2378 onPressed: () {},
2379 icon: const Icon(Icons.add),
2380 label: const Text('button'),
2381 iconAlignment: iconAlignment,
2382 ),
2383 ),
2384 ),
2385 );
2386 }
2387
2388 // Test iconAlignment when textDirection is ltr.
2389 await tester.pumpWidget(
2390 buildWidget(textDirection: TextDirection.ltr, iconAlignment: IconAlignment.start),
2391 );
2392
2393 Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last);
2394 Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add));
2395
2396 // The icon is aligned to the left of the button.
2397 expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); // 16.0 - padding between icon and button edge.
2398
2399 // Test iconAlignment when textDirection is ltr.
2400 await tester.pumpWidget(
2401 buildWidget(textDirection: TextDirection.ltr, iconAlignment: IconAlignment.end),
2402 );
2403
2404 Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
2405 Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
2406
2407 // The icon is aligned to the right of the button.
2408 expect(
2409 buttonTopRight.dx,
2410 iconTopRight.dx + 24.0,
2411 ); // 24.0 - padding between icon and button edge.
2412
2413 // Test iconAlignment when textDirection is rtl.
2414 await tester.pumpWidget(
2415 buildWidget(textDirection: TextDirection.rtl, iconAlignment: IconAlignment.start),
2416 );
2417
2418 buttonTopRight = tester.getTopRight(find.byType(Material).last);
2419 iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
2420
2421 // The icon is aligned to the right of the button.
2422 expect(
2423 buttonTopRight.dx,
2424 iconTopRight.dx + 16.0,
2425 ); // 16.0 - padding between icon and button edge.
2426
2427 // Test iconAlignment when textDirection is rtl.
2428 await tester.pumpWidget(
2429 buildWidget(textDirection: TextDirection.rtl, iconAlignment: IconAlignment.end),
2430 );
2431
2432 buttonTopLeft = tester.getTopLeft(find.byType(Material).last);
2433 iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add));
2434
2435 // The icon is aligned to the left of the button.
2436 expect(buttonTopLeft.dx, iconTopLeft.dx - 24.0); // 24.0 - padding between icon and button edge.
2437 });
2438
2439 testWidgets('ElevatedButton icon alignment respects ButtonStyle.iconAlignment', (
2440 WidgetTester tester,
2441 ) async {
2442 Widget buildButton({IconAlignment? iconAlignment}) {
2443 return MaterialApp(
2444 home: Center(
2445 child: ElevatedButton.icon(
2446 style: ButtonStyle(iconAlignment: iconAlignment),
2447 onPressed: () {},
2448 icon: const Icon(Icons.add),
2449 label: const Text('button'),
2450 ),
2451 ),
2452 );
2453 }
2454
2455 await tester.pumpWidget(buildButton());
2456
2457 final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last);
2458 final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add));
2459
2460 expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0);
2461
2462 await tester.pumpWidget(buildButton(iconAlignment: IconAlignment.end));
2463
2464 final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
2465 final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
2466
2467 expect(buttonTopRight.dx, iconTopRight.dx + 24.0);
2468 });
2469
2470 // Regression test for https://github.com/flutter/flutter/issues/154798.
2471 testWidgets('ElevatedButton.styleFrom can customize the button icon', (
2472 WidgetTester tester,
2473 ) async {
2474 const Color iconColor = Color(0xFFF000FF);
2475 const double iconSize = 32.0;
2476 const Color disabledIconColor = Color(0xFFFFF000);
2477 Widget buildButton({bool enabled = true}) {
2478 return MaterialApp(
2479 home: Material(
2480 child: Center(
2481 child: ElevatedButton.icon(
2482 style: ElevatedButton.styleFrom(
2483 iconColor: iconColor,
2484 iconSize: iconSize,
2485 iconAlignment: IconAlignment.end,
2486 disabledIconColor: disabledIconColor,
2487 ),
2488 onPressed: enabled ? () {} : null,
2489 icon: const Icon(Icons.add),
2490 label: const Text('Button'),
2491 ),
2492 ),
2493 ),
2494 );
2495 }
2496
2497 // Test enabled button.
2498 await tester.pumpWidget(buildButton());
2499 expect(tester.getSize(find.byIcon(Icons.add)), const Size(iconSize, iconSize));
2500 expect(iconStyle(tester, Icons.add).color, iconColor);
2501
2502 // Test disabled button.
2503 await tester.pumpWidget(buildButton(enabled: false));
2504 await tester.pumpAndSettle();
2505 expect(iconStyle(tester, Icons.add).color, disabledIconColor);
2506
2507 final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
2508 final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
2509 expect(buttonTopRight.dx, iconTopRight.dx + 24.0);
2510 });
2511
2512 // Regression test for https://github.com/flutter/flutter/issues/162839.
2513 testWidgets('ElevatedButton icon uses provided foregroundColor over default icon color', (
2514 WidgetTester tester,
2515 ) async {
2516 const Color foregroundColor = Color(0xFFFF1234);
2517
2518 await tester.pumpWidget(
2519 MaterialApp(
2520 home: Material(
2521 child: Center(
2522 child: ElevatedButton.icon(
2523 style: ElevatedButton.styleFrom(foregroundColor: foregroundColor),
2524 onPressed: () {},
2525 icon: const Icon(Icons.add),
2526 label: const Text('Button'),
2527 ),
2528 ),
2529 ),
2530 ),
2531 );
2532 expect(iconStyle(tester, Icons.add).color, foregroundColor);
2533 });
2534
2535 testWidgets('ElevatedButton text and icon respect animation duration', (
2536 WidgetTester tester,
2537 ) async {
2538 const String buttonText = 'Button';
2539 const IconData buttonIcon = Icons.add;
2540 const Color hoveredColor = Color(0xFFFF0000);
2541 const Color idleColor = Color(0xFF000000);
2542
2543 Widget buildButton({Duration? animationDuration}) {
2544 return MaterialApp(
2545 home: Material(
2546 child: Center(
2547 child: ElevatedButton.icon(
2548 style: ButtonStyle(
2549 animationDuration: animationDuration,
2550 iconColor: const WidgetStateProperty<Color>.fromMap(<WidgetStatesConstraint, Color>{
2551 WidgetState.hovered: hoveredColor,
2552 WidgetState.any: idleColor,
2553 }),
2554 foregroundColor: const WidgetStateProperty<Color>.fromMap(
2555 <WidgetStatesConstraint, Color>{
2556 WidgetState.hovered: hoveredColor,
2557 WidgetState.any: idleColor,
2558 },
2559 ),
2560 ),
2561 onPressed: () {},
2562 icon: const Icon(buttonIcon),
2563 label: const Text(buttonText),
2564 ),
2565 ),
2566 ),
2567 );
2568 }
2569
2570 // Test default animation duration.
2571 await tester.pumpWidget(buildButton());
2572
2573 expect(textColor(tester, buttonText), idleColor);
2574 expect(iconStyle(tester, buttonIcon).color, idleColor);
2575
2576 final Offset buttonCenter = tester.getCenter(find.text(buttonText));
2577 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
2578 await gesture.addPointer();
2579 addTearDown(gesture.removePointer);
2580 await gesture.moveTo(buttonCenter);
2581
2582 await tester.pump();
2583 await tester.pump(const Duration(milliseconds: 100));
2584 expect(textColor(tester, buttonText), hoveredColor.withValues(red: 0.5));
2585 expect(iconStyle(tester, buttonIcon).color, hoveredColor.withValues(red: 0.5));
2586
2587 await tester.pump();
2588 await tester.pump(const Duration(milliseconds: 200));
2589 expect(textColor(tester, buttonText), hoveredColor);
2590 expect(iconStyle(tester, buttonIcon).color, hoveredColor);
2591
2592 await gesture.removePointer();
2593
2594 // Test custom animation duration.
2595 await tester.pumpWidget(buildButton(animationDuration: const Duration(seconds: 2)));
2596 await tester.pumpAndSettle();
2597
2598 await gesture.moveTo(buttonCenter);
2599
2600 await tester.pump();
2601 await tester.pump(const Duration(seconds: 1));
2602 expect(textColor(tester, buttonText), hoveredColor.withValues(red: 0.5));
2603 expect(iconStyle(tester, buttonIcon).color, hoveredColor.withValues(red: 0.5));
2604
2605 await tester.pump();
2606 await tester.pump(const Duration(seconds: 1));
2607 expect(textColor(tester, buttonText), hoveredColor);
2608 expect(iconStyle(tester, buttonIcon).color, hoveredColor);
2609 });
2610}
2611