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 | |
5 | import 'package:flutter/foundation.dart'; |
6 | import 'package:flutter/gestures.dart'; |
7 | import 'package:flutter/material.dart'; |
8 | import 'package:flutter/rendering.dart'; |
9 | import 'package:flutter_test/flutter_test.dart'; |
10 | import '../widgets/semantics_tester.dart'; |
11 | |
12 | void 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 |
Definitions
- main
- iconStyle
- overlayColor
- elevation
- overlayColor
- elevation
- getTextColor
- textColor
- getTextColor
- buildFrame
- buildFrame
- getOverlayColor
- buildFrame
- buildTest
- closeOnWeb
- globalBounds
- paddingBetween
- buildFrame
- buildFrame
- testStatesController
- valueChanged
- valueChanged
- boxDecorationOf
- textChildOf
- sameStates
- buildWidget
- buildWidget
- buildButton
- buildButton
- buildWidget
- buildWidget
Learn more about Flutter for embedded and desktop on industrialflutter.com