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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com