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/gestures.dart'; |
6 | import 'package:flutter/material.dart'; |
7 | import 'package:flutter/rendering.dart'; |
8 | import 'package:flutter_test/flutter_test.dart'; |
9 | import '../widgets/semantics_tester.dart'; |
10 | |
11 | void main() { |
12 | setUp(() { |
13 | debugResetSemanticsIdCounter(); |
14 | }); |
15 | |
16 | testWidgets('MaterialButton defaults' , (WidgetTester tester) async { |
17 | final Finder rawButtonMaterial = find.descendant( |
18 | of: find.byType(MaterialButton), |
19 | matching: find.byType(Material), |
20 | ); |
21 | |
22 | // Enabled MaterialButton |
23 | await tester.pumpWidget( |
24 | Theme( |
25 | data: ThemeData(useMaterial3: false), |
26 | child: Directionality( |
27 | textDirection: TextDirection.ltr, |
28 | child: MaterialButton(onPressed: () {}, child: const Text('button' )), |
29 | ), |
30 | ), |
31 | ); |
32 | Material material = tester.widget<Material>(rawButtonMaterial); |
33 | expect(material.animationDuration, const Duration(milliseconds: 200)); |
34 | expect(material.borderOnForeground, true); |
35 | expect(material.borderRadius, null); |
36 | expect(material.clipBehavior, Clip.none); |
37 | expect(material.color, null); |
38 | expect(material.elevation, 2.0); |
39 | expect(material.shadowColor, null); |
40 | expect( |
41 | material.shape, |
42 | const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))), |
43 | ); |
44 | expect(material.textStyle!.color, const Color(0xdd000000)); |
45 | expect(material.textStyle!.fontFamily, 'Roboto' ); |
46 | expect(material.textStyle!.fontSize, 14); |
47 | expect(material.textStyle!.fontWeight, FontWeight.w500); |
48 | expect(material.type, MaterialType.transparency); |
49 | |
50 | final Offset center = tester.getCenter(find.byType(MaterialButton)); |
51 | final TestGesture gesture = await tester.startGesture(center); |
52 | await tester.pumpAndSettle(); |
53 | |
54 | // Only elevation changes when enabled and pressed. |
55 | material = tester.widget<Material>(rawButtonMaterial); |
56 | expect(material.animationDuration, const Duration(milliseconds: 200)); |
57 | expect(material.borderOnForeground, true); |
58 | expect(material.borderRadius, null); |
59 | expect(material.clipBehavior, Clip.none); |
60 | expect(material.color, null); |
61 | expect(material.elevation, 8.0); |
62 | expect(material.shadowColor, null); |
63 | expect( |
64 | material.shape, |
65 | const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))), |
66 | ); |
67 | expect(material.textStyle!.color, const Color(0xdd000000)); |
68 | expect(material.textStyle!.fontFamily, 'Roboto' ); |
69 | expect(material.textStyle!.fontSize, 14); |
70 | expect(material.textStyle!.fontWeight, FontWeight.w500); |
71 | expect(material.type, MaterialType.transparency); |
72 | |
73 | // Disabled MaterialButton |
74 | await tester.pumpWidget( |
75 | Theme( |
76 | data: ThemeData(useMaterial3: false), |
77 | child: const Directionality( |
78 | textDirection: TextDirection.ltr, |
79 | child: MaterialButton(onPressed: null, child: Text('button' )), |
80 | ), |
81 | ), |
82 | ); |
83 | material = tester.widget<Material>(rawButtonMaterial); |
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, null); |
89 | expect(material.elevation, 0.0); |
90 | expect(material.shadowColor, null); |
91 | expect( |
92 | material.shape, |
93 | const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))), |
94 | ); |
95 | expect(material.textStyle!.color, const Color(0x61000000)); |
96 | expect(material.textStyle!.fontFamily, 'Roboto' ); |
97 | expect(material.textStyle!.fontSize, 14); |
98 | expect(material.textStyle!.fontWeight, FontWeight.w500); |
99 | expect(material.type, MaterialType.transparency); |
100 | |
101 | // Finish gesture to release resources. |
102 | await gesture.up(); |
103 | await tester.pumpAndSettle(); |
104 | }); |
105 | |
106 | testWidgets('Does MaterialButton work with hover' , (WidgetTester tester) async { |
107 | const Color hoverColor = Color(0xff001122); |
108 | |
109 | await tester.pumpWidget( |
110 | Directionality( |
111 | textDirection: TextDirection.ltr, |
112 | child: MaterialButton( |
113 | hoverColor: hoverColor, |
114 | onPressed: () {}, |
115 | child: const Text('button' ), |
116 | ), |
117 | ), |
118 | ); |
119 | |
120 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
121 | await gesture.addPointer(); |
122 | await gesture.moveTo(tester.getCenter(find.byType(MaterialButton))); |
123 | await tester.pumpAndSettle(); |
124 | |
125 | final RenderObject inkFeatures = tester.allRenderObjects.firstWhere( |
126 | (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures' , |
127 | ); |
128 | expect(inkFeatures, paints..rect(color: hoverColor)); |
129 | }); |
130 | |
131 | testWidgets('Does MaterialButton work with focus' , (WidgetTester tester) async { |
132 | const Color focusColor = Color(0xff001122); |
133 | |
134 | final FocusNode focusNode = FocusNode(debugLabel: 'MaterialButton Node' ); |
135 | await tester.pumpWidget( |
136 | Directionality( |
137 | textDirection: TextDirection.ltr, |
138 | child: MaterialButton( |
139 | focusColor: focusColor, |
140 | focusNode: focusNode, |
141 | onPressed: () {}, |
142 | child: const Text('button' ), |
143 | ), |
144 | ), |
145 | ); |
146 | |
147 | FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
148 | focusNode.requestFocus(); |
149 | await tester.pumpAndSettle(); |
150 | |
151 | final RenderObject inkFeatures = tester.allRenderObjects.firstWhere( |
152 | (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures' , |
153 | ); |
154 | expect(inkFeatures, paints..rect(color: focusColor)); |
155 | |
156 | focusNode.dispose(); |
157 | }); |
158 | |
159 | testWidgets('MaterialButton elevation and colors have proper precedence' , ( |
160 | WidgetTester tester, |
161 | ) async { |
162 | const double elevation = 10.0; |
163 | const double focusElevation = 11.0; |
164 | const double hoverElevation = 12.0; |
165 | const double highlightElevation = 13.0; |
166 | const Color focusColor = Color(0xff001122); |
167 | const Color hoverColor = Color(0xff112233); |
168 | const Color highlightColor = Color(0xff223344); |
169 | |
170 | final Finder rawButtonMaterial = find.descendant( |
171 | of: find.byType(MaterialButton), |
172 | matching: find.byType(Material), |
173 | ); |
174 | |
175 | final FocusNode focusNode = FocusNode(debugLabel: 'MaterialButton Node' ); |
176 | await tester.pumpWidget( |
177 | Directionality( |
178 | textDirection: TextDirection.ltr, |
179 | child: MaterialButton( |
180 | focusColor: focusColor, |
181 | hoverColor: hoverColor, |
182 | highlightColor: highlightColor, |
183 | elevation: elevation, |
184 | focusElevation: focusElevation, |
185 | hoverElevation: hoverElevation, |
186 | highlightElevation: highlightElevation, |
187 | focusNode: focusNode, |
188 | onPressed: () {}, |
189 | child: const Text('button' ), |
190 | ), |
191 | ), |
192 | ); |
193 | await tester.pumpAndSettle(); |
194 | FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
195 | |
196 | // Base elevation |
197 | Material material = tester.widget<Material>(rawButtonMaterial); |
198 | expect(material.elevation, equals(elevation)); |
199 | |
200 | // Focus elevation overrides base |
201 | focusNode.requestFocus(); |
202 | await tester.pumpAndSettle(); |
203 | material = tester.widget<Material>(rawButtonMaterial); |
204 | RenderObject inkFeatures = tester.allRenderObjects.firstWhere( |
205 | (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures' , |
206 | ); |
207 | expect(inkFeatures, paints..rect(color: focusColor)); |
208 | expect(focusNode.hasPrimaryFocus, isTrue); |
209 | expect(material.elevation, equals(focusElevation)); |
210 | |
211 | // Hover elevation overrides focus |
212 | TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
213 | await gesture.addPointer(); |
214 | addTearDown(() => gesture?.removePointer()); |
215 | await gesture.moveTo(tester.getCenter(find.byType(MaterialButton))); |
216 | await tester.pumpAndSettle(); |
217 | material = tester.widget<Material>(rawButtonMaterial); |
218 | inkFeatures = tester.allRenderObjects.firstWhere( |
219 | (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures' , |
220 | ); |
221 | expect( |
222 | inkFeatures, |
223 | paints |
224 | ..rect(color: focusColor) |
225 | ..rect(color: hoverColor), |
226 | ); |
227 | expect(material.elevation, equals(hoverElevation)); |
228 | await gesture.removePointer(); |
229 | gesture = null; |
230 | |
231 | // Highlight elevation overrides hover |
232 | final TestGesture gesture2 = await tester.startGesture( |
233 | tester.getCenter(find.byType(MaterialButton)), |
234 | ); |
235 | addTearDown(gesture2.removePointer); |
236 | await tester.pumpAndSettle(); |
237 | material = tester.widget<Material>(rawButtonMaterial); |
238 | inkFeatures = tester.allRenderObjects.firstWhere( |
239 | (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures' , |
240 | ); |
241 | expect( |
242 | inkFeatures, |
243 | paints |
244 | ..rect(color: focusColor) |
245 | ..rect(color: highlightColor), |
246 | ); |
247 | expect(material.elevation, equals(highlightElevation)); |
248 | await gesture2.up(); |
249 | |
250 | focusNode.dispose(); |
251 | }); |
252 | |
253 | testWidgets("MaterialButton's disabledColor takes precedence over its default disabled color." , ( |
254 | WidgetTester tester, |
255 | ) async { |
256 | // Regression test for https://github.com/flutter/flutter/issues/30012. |
257 | |
258 | final Finder rawButtonMaterial = find.descendant( |
259 | of: find.byType(MaterialButton), |
260 | matching: find.byType(Material), |
261 | ); |
262 | |
263 | await tester.pumpWidget( |
264 | const Directionality( |
265 | textDirection: TextDirection.ltr, |
266 | child: MaterialButton( |
267 | disabledColor: Color(0xff00ff00), |
268 | onPressed: null, |
269 | child: Text('button' ), |
270 | ), |
271 | ), |
272 | ); |
273 | |
274 | final Material material = tester.widget<Material>(rawButtonMaterial); |
275 | expect(material.color, const Color(0xff00ff00)); |
276 | }); |
277 | |
278 | testWidgets( |
279 | 'Default MaterialButton meets a11y contrast guidelines' , |
280 | (WidgetTester tester) async { |
281 | await tester.pumpWidget( |
282 | MaterialApp( |
283 | home: Scaffold( |
284 | body: Center( |
285 | child: MaterialButton(child: const Text('MaterialButton' ), onPressed: () {}), |
286 | ), |
287 | ), |
288 | ), |
289 | ); |
290 | |
291 | // Default, not disabled. |
292 | await expectLater(tester, meetsGuideline(textContrastGuideline)); |
293 | |
294 | // Highlighted (pressed). |
295 | final Offset center = tester.getCenter(find.byType(MaterialButton)); |
296 | final TestGesture gesture = await tester.startGesture(center); |
297 | await tester.pump(); // Start the splash and highlight animations. |
298 | await tester.pump( |
299 | const Duration(milliseconds: 800), |
300 | ); // Wait for splash and highlight to be well under way. |
301 | await expectLater(tester, meetsGuideline(textContrastGuideline)); |
302 | |
303 | // Finish gesture to release resources. |
304 | await gesture.up(); |
305 | await tester.pumpAndSettle(); |
306 | }, |
307 | skip: isBrowser, // https://github.com/flutter/flutter/issues/44115 |
308 | ); |
309 | |
310 | testWidgets('MaterialButton gets focus when autofocus is set.' , (WidgetTester tester) async { |
311 | final FocusNode focusNode = FocusNode(debugLabel: 'MaterialButton' ); |
312 | await tester.pumpWidget( |
313 | MaterialApp( |
314 | home: Center( |
315 | child: MaterialButton( |
316 | focusNode: focusNode, |
317 | onPressed: () {}, |
318 | child: Container(width: 100, height: 100, color: const Color(0xffff0000)), |
319 | ), |
320 | ), |
321 | ), |
322 | ); |
323 | |
324 | await tester.pump(); |
325 | expect(focusNode.hasPrimaryFocus, isFalse); |
326 | |
327 | await tester.pumpWidget( |
328 | MaterialApp( |
329 | home: Center( |
330 | child: MaterialButton( |
331 | autofocus: true, |
332 | focusNode: focusNode, |
333 | onPressed: () {}, |
334 | child: Container(width: 100, height: 100, color: const Color(0xffff0000)), |
335 | ), |
336 | ), |
337 | ), |
338 | ); |
339 | |
340 | await tester.pump(); |
341 | expect(focusNode.hasPrimaryFocus, isTrue); |
342 | |
343 | focusNode.dispose(); |
344 | }); |
345 | |
346 | testWidgets( |
347 | 'MaterialButton onPressed and onLongPress callbacks are correctly called when non-null' , |
348 | (WidgetTester tester) async { |
349 | bool wasPressed; |
350 | Finder materialButton; |
351 | |
352 | Widget buildFrame({VoidCallback? onPressed, VoidCallback? onLongPress}) { |
353 | return Directionality( |
354 | textDirection: TextDirection.ltr, |
355 | child: MaterialButton( |
356 | onPressed: onPressed, |
357 | onLongPress: onLongPress, |
358 | child: const Text('button' ), |
359 | ), |
360 | ); |
361 | } |
362 | |
363 | // onPressed not null, onLongPress null. |
364 | wasPressed = false; |
365 | await tester.pumpWidget( |
366 | buildFrame( |
367 | onPressed: () { |
368 | wasPressed = true; |
369 | }, |
370 | ), |
371 | ); |
372 | materialButton = find.byType(MaterialButton); |
373 | expect(tester.widget<MaterialButton>(materialButton).enabled, true); |
374 | await tester.tap(materialButton); |
375 | expect(wasPressed, true); |
376 | |
377 | // onPressed null, onLongPress not null. |
378 | wasPressed = false; |
379 | await tester.pumpWidget( |
380 | buildFrame( |
381 | onLongPress: () { |
382 | wasPressed = true; |
383 | }, |
384 | ), |
385 | ); |
386 | materialButton = find.byType(MaterialButton); |
387 | expect(tester.widget<MaterialButton>(materialButton).enabled, true); |
388 | await tester.longPress(materialButton); |
389 | expect(wasPressed, true); |
390 | |
391 | // onPressed null, onLongPress null. |
392 | await tester.pumpWidget(buildFrame()); |
393 | materialButton = find.byType(MaterialButton); |
394 | expect(tester.widget<MaterialButton>(materialButton).enabled, false); |
395 | }, |
396 | ); |
397 | |
398 | testWidgets('MaterialButton onPressed and onLongPress callbacks are distinctly recognized' , ( |
399 | WidgetTester tester, |
400 | ) async { |
401 | bool didPressButton = false; |
402 | bool didLongPressButton = false; |
403 | |
404 | await tester.pumpWidget( |
405 | Directionality( |
406 | textDirection: TextDirection.ltr, |
407 | child: MaterialButton( |
408 | onPressed: () { |
409 | didPressButton = true; |
410 | }, |
411 | onLongPress: () { |
412 | didLongPressButton = true; |
413 | }, |
414 | child: const Text('button' ), |
415 | ), |
416 | ), |
417 | ); |
418 | |
419 | final Finder materialButton = find.byType(MaterialButton); |
420 | expect(tester.widget<MaterialButton>(materialButton).enabled, true); |
421 | |
422 | expect(didPressButton, isFalse); |
423 | await tester.tap(materialButton); |
424 | expect(didPressButton, isTrue); |
425 | |
426 | expect(didLongPressButton, isFalse); |
427 | await tester.longPress(materialButton); |
428 | expect(didLongPressButton, isTrue); |
429 | }); |
430 | |
431 | testWidgets('MaterialButton changes mouse cursor when hovered' , (WidgetTester tester) async { |
432 | await tester.pumpWidget( |
433 | Directionality( |
434 | textDirection: TextDirection.ltr, |
435 | child: MouseRegion( |
436 | cursor: SystemMouseCursors.forbidden, |
437 | child: MaterialButton(onPressed: () {}, mouseCursor: SystemMouseCursors.text), |
438 | ), |
439 | ), |
440 | ); |
441 | |
442 | final TestGesture gesture = await tester.createGesture( |
443 | kind: PointerDeviceKind.mouse, |
444 | pointer: 1, |
445 | ); |
446 | await gesture.addPointer(location: Offset.zero); |
447 | |
448 | await tester.pump(); |
449 | |
450 | expect( |
451 | RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
452 | SystemMouseCursors.text, |
453 | ); |
454 | |
455 | // Test default cursor |
456 | await tester.pumpWidget( |
457 | Directionality( |
458 | textDirection: TextDirection.ltr, |
459 | child: MouseRegion( |
460 | cursor: SystemMouseCursors.forbidden, |
461 | child: MaterialButton(onPressed: () {}), |
462 | ), |
463 | ), |
464 | ); |
465 | |
466 | expect( |
467 | RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
468 | SystemMouseCursors.click, |
469 | ); |
470 | |
471 | // Test default cursor when disabled |
472 | await tester.pumpWidget( |
473 | const Directionality( |
474 | textDirection: TextDirection.ltr, |
475 | child: MouseRegion( |
476 | cursor: SystemMouseCursors.forbidden, |
477 | child: MaterialButton(onPressed: null), |
478 | ), |
479 | ), |
480 | ); |
481 | |
482 | expect( |
483 | RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
484 | SystemMouseCursors.basic, |
485 | ); |
486 | }); |
487 | |
488 | // This test is very similar to the '...explicit splashColor and highlightColor' test |
489 | // in icon_button_test.dart. If you change this one, you may want to also change that one. |
490 | testWidgets('MaterialButton with explicit splashColor and highlightColor' , ( |
491 | WidgetTester tester, |
492 | ) async { |
493 | const Color directSplashColor = Color(0xFF000011); |
494 | const Color directHighlightColor = Color(0xFF000011); |
495 | |
496 | Widget buttonWidget = Center( |
497 | child: MaterialButton( |
498 | splashColor: directSplashColor, |
499 | highlightColor: directHighlightColor, |
500 | onPressed: () { |
501 | /* to make sure the button is enabled */ |
502 | }, |
503 | clipBehavior: Clip.antiAlias, |
504 | ), |
505 | ); |
506 | |
507 | await tester.pumpWidget( |
508 | Directionality( |
509 | textDirection: TextDirection.ltr, |
510 | child: Theme( |
511 | data: ThemeData( |
512 | useMaterial3: false, |
513 | materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, |
514 | ), |
515 | child: buttonWidget, |
516 | ), |
517 | ), |
518 | ); |
519 | |
520 | final Offset center = tester.getCenter(find.byType(MaterialButton)); |
521 | final TestGesture gesture = await tester.startGesture(center); |
522 | await tester.pump(); // start gesture |
523 | await tester.pump(const Duration(milliseconds: 200)); // wait for splash to be well under way |
524 | |
525 | // Painter is translated to the center by the Center widget and not |
526 | // the Material widget. |
527 | const Rect expectedClipRect = Rect.fromLTRB(0.0, 0.0, 88.0, 36.0); |
528 | final Path expectedClipPath = |
529 | Path()..addRRect(RRect.fromRectAndRadius(expectedClipRect, const Radius.circular(2.0))); |
530 | expect( |
531 | Material.of(tester.element(find.byType(InkWell))), |
532 | paints |
533 | ..clipPath( |
534 | pathMatcher: coversSameAreaAs( |
535 | expectedClipPath, |
536 | areaToCompare: expectedClipRect.inflate(10.0), |
537 | ), |
538 | ) |
539 | ..circle(color: directSplashColor) |
540 | ..rect(color: directHighlightColor), |
541 | ); |
542 | |
543 | const Color themeSplashColor1 = Color(0xFF001100); |
544 | const Color themeHighlightColor1 = Color(0xFF001100); |
545 | |
546 | buttonWidget = Center( |
547 | child: MaterialButton( |
548 | onPressed: () { |
549 | /* to make sure the button is enabled */ |
550 | }, |
551 | clipBehavior: Clip.antiAlias, |
552 | ), |
553 | ); |
554 | |
555 | await tester.pumpWidget( |
556 | Directionality( |
557 | textDirection: TextDirection.ltr, |
558 | child: Theme( |
559 | data: ThemeData( |
560 | useMaterial3: false, |
561 | highlightColor: themeHighlightColor1, |
562 | splashColor: themeSplashColor1, |
563 | materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, |
564 | ), |
565 | child: buttonWidget, |
566 | ), |
567 | ), |
568 | ); |
569 | |
570 | expect( |
571 | Material.of(tester.element(find.byType(InkWell))), |
572 | paints |
573 | ..clipPath( |
574 | pathMatcher: coversSameAreaAs( |
575 | expectedClipPath, |
576 | areaToCompare: expectedClipRect.inflate(10.0), |
577 | ), |
578 | ) |
579 | ..circle(color: themeSplashColor1) |
580 | ..rect(color: themeHighlightColor1), |
581 | ); |
582 | |
583 | const Color themeSplashColor2 = Color(0xFF002200); |
584 | const Color themeHighlightColor2 = Color(0xFF002200); |
585 | |
586 | await tester.pumpWidget( |
587 | Directionality( |
588 | textDirection: TextDirection.ltr, |
589 | child: Theme( |
590 | data: ThemeData( |
591 | useMaterial3: false, |
592 | highlightColor: themeHighlightColor2, |
593 | splashColor: themeSplashColor2, |
594 | materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, |
595 | ), |
596 | child: buttonWidget, // same widget, so does not get updated because of us |
597 | ), |
598 | ), |
599 | ); |
600 | |
601 | expect( |
602 | Material.of(tester.element(find.byType(InkWell))), |
603 | paints |
604 | ..circle(color: themeSplashColor2) |
605 | ..rect(color: themeHighlightColor2), |
606 | ); |
607 | |
608 | await gesture.up(); |
609 | }); |
610 | |
611 | testWidgets('MaterialButton has no clip by default' , (WidgetTester tester) async { |
612 | final GlobalKey buttonKey = GlobalKey(); |
613 | final Widget buttonWidget = Center( |
614 | child: MaterialButton( |
615 | key: buttonKey, |
616 | onPressed: () { |
617 | /* to make sure the button is enabled */ |
618 | }, |
619 | ), |
620 | ); |
621 | |
622 | await tester.pumpWidget( |
623 | Directionality( |
624 | textDirection: TextDirection.ltr, |
625 | child: Theme( |
626 | data: ThemeData(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap), |
627 | child: buttonWidget, |
628 | ), |
629 | ), |
630 | ); |
631 | |
632 | expect(tester.renderObject(find.byKey(buttonKey)), paintsExactlyCountTimes(#clipPath, 0)); |
633 | }); |
634 | |
635 | testWidgets( |
636 | 'Disabled MaterialButton has same semantic size as enabled and exposes disabled semantics' , |
637 | (WidgetTester tester) async { |
638 | final SemanticsTester semantics = SemanticsTester(tester); |
639 | |
640 | const Rect expectedButtonSize = Rect.fromLTRB(0.0, 0.0, 116.0, 48.0); |
641 | // Button is in center of screen |
642 | final Matrix4 expectedButtonTransform = |
643 | Matrix4.identity()..translate( |
644 | TestSemantics.fullScreen.width / 2 - expectedButtonSize.width / 2, |
645 | TestSemantics.fullScreen.height / 2 - expectedButtonSize.height / 2, |
646 | ); |
647 | |
648 | // enabled button |
649 | await tester.pumpWidget( |
650 | Theme( |
651 | data: ThemeData(useMaterial3: false), |
652 | child: Directionality( |
653 | textDirection: TextDirection.ltr, |
654 | child: Center( |
655 | child: MaterialButton( |
656 | child: const Text('Button' ), |
657 | onPressed: () { |
658 | /* to make sure the button is enabled */ |
659 | }, |
660 | ), |
661 | ), |
662 | ), |
663 | ), |
664 | ); |
665 | |
666 | expect( |
667 | semantics, |
668 | hasSemantics( |
669 | TestSemantics.root( |
670 | children: <TestSemantics>[ |
671 | TestSemantics.rootChild( |
672 | id: 1, |
673 | rect: expectedButtonSize, |
674 | transform: expectedButtonTransform, |
675 | label: 'Button' , |
676 | actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus], |
677 | flags: <SemanticsFlag>[ |
678 | SemanticsFlag.hasEnabledState, |
679 | SemanticsFlag.isButton, |
680 | SemanticsFlag.isEnabled, |
681 | SemanticsFlag.isFocusable, |
682 | ], |
683 | ), |
684 | ], |
685 | ), |
686 | ), |
687 | ); |
688 | |
689 | // disabled button |
690 | await tester.pumpWidget( |
691 | Theme( |
692 | data: ThemeData(useMaterial3: false), |
693 | child: const Directionality( |
694 | textDirection: TextDirection.ltr, |
695 | child: Center( |
696 | child: MaterialButton( |
697 | onPressed: null, // button is disabled |
698 | child: Text('Button' ), |
699 | ), |
700 | ), |
701 | ), |
702 | ), |
703 | ); |
704 | |
705 | expect( |
706 | semantics, |
707 | hasSemantics( |
708 | TestSemantics.root( |
709 | children: <TestSemantics>[ |
710 | TestSemantics.rootChild( |
711 | id: 1, |
712 | rect: expectedButtonSize, |
713 | transform: expectedButtonTransform, |
714 | label: 'Button' , |
715 | flags: <SemanticsFlag>[ |
716 | SemanticsFlag.hasEnabledState, |
717 | SemanticsFlag.isButton, |
718 | SemanticsFlag.isFocusable, |
719 | ], |
720 | actions: <SemanticsAction>[SemanticsAction.focus], |
721 | ), |
722 | ], |
723 | ), |
724 | ), |
725 | ); |
726 | |
727 | semantics.dispose(); |
728 | }, |
729 | ); |
730 | |
731 | testWidgets('MaterialButton minWidth and height parameters' , (WidgetTester tester) async { |
732 | Widget buildFrame({ |
733 | double? minWidth, |
734 | double? height, |
735 | EdgeInsets padding = EdgeInsets.zero, |
736 | Widget? child, |
737 | }) { |
738 | return Directionality( |
739 | textDirection: TextDirection.ltr, |
740 | child: Center( |
741 | child: MaterialButton( |
742 | padding: padding, |
743 | minWidth: minWidth, |
744 | height: height, |
745 | onPressed: null, |
746 | materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, |
747 | child: child, |
748 | ), |
749 | ), |
750 | ); |
751 | } |
752 | |
753 | await tester.pumpWidget(buildFrame(minWidth: 8.0, height: 24.0)); |
754 | expect(tester.getSize(find.byType(MaterialButton)), const Size(8.0, 24.0)); |
755 | |
756 | await tester.pumpWidget(buildFrame(minWidth: 8.0)); |
757 | // Default minHeight constraint is 36, see RawMaterialButton. |
758 | expect(tester.getSize(find.byType(MaterialButton)), const Size(8.0, 36.0)); |
759 | |
760 | await tester.pumpWidget(buildFrame(height: 8.0)); |
761 | // Default minWidth constraint is 88, see RawMaterialButton. |
762 | expect(tester.getSize(find.byType(MaterialButton)), const Size(88.0, 8.0)); |
763 | |
764 | await tester.pumpWidget(buildFrame()); |
765 | expect(tester.getSize(find.byType(MaterialButton)), const Size(88.0, 36.0)); |
766 | |
767 | await tester.pumpWidget(buildFrame(padding: const EdgeInsets.all(4.0))); |
768 | expect(tester.getSize(find.byType(MaterialButton)), const Size(88.0, 36.0)); |
769 | |
770 | // Size is defined by the padding. |
771 | await tester.pumpWidget( |
772 | buildFrame(minWidth: 0.0, height: 0.0, padding: const EdgeInsets.all(4.0)), |
773 | ); |
774 | expect(tester.getSize(find.byType(MaterialButton)), const Size(8.0, 8.0)); |
775 | |
776 | // Size is defined by the padded child. |
777 | await tester.pumpWidget( |
778 | buildFrame( |
779 | minWidth: 0.0, |
780 | height: 0.0, |
781 | padding: const EdgeInsets.all(4.0), |
782 | child: const SizedBox(width: 8.0, height: 8.0), |
783 | ), |
784 | ); |
785 | expect(tester.getSize(find.byType(MaterialButton)), const Size(16.0, 16.0)); |
786 | |
787 | // Size is defined by the minWidth, height constraints. |
788 | await tester.pumpWidget( |
789 | buildFrame( |
790 | minWidth: 18.0, |
791 | height: 18.0, |
792 | padding: const EdgeInsets.all(4.0), |
793 | child: const SizedBox(width: 8.0, height: 8.0), |
794 | ), |
795 | ); |
796 | expect(tester.getSize(find.byType(MaterialButton)), const Size(18.0, 18.0)); |
797 | }); |
798 | |
799 | testWidgets('MaterialButton size is configurable by ThemeData.materialTapTargetSize' , ( |
800 | WidgetTester tester, |
801 | ) async { |
802 | final Key key1 = UniqueKey(); |
803 | await tester.pumpWidget( |
804 | Theme( |
805 | data: ThemeData(materialTapTargetSize: MaterialTapTargetSize.padded), |
806 | child: Directionality( |
807 | textDirection: TextDirection.ltr, |
808 | child: Center( |
809 | child: MaterialButton( |
810 | key: key1, |
811 | child: const SizedBox(width: 50.0, height: 8.0), |
812 | onPressed: () {}, |
813 | ), |
814 | ), |
815 | ), |
816 | ), |
817 | ); |
818 | |
819 | expect(tester.getSize(find.byKey(key1)), const Size(88.0, 48.0)); |
820 | |
821 | final Key key2 = UniqueKey(); |
822 | await tester.pumpWidget( |
823 | Theme( |
824 | data: ThemeData(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap), |
825 | child: Directionality( |
826 | textDirection: TextDirection.ltr, |
827 | child: Center( |
828 | child: MaterialButton( |
829 | key: key2, |
830 | child: const SizedBox(width: 50.0, height: 8.0), |
831 | onPressed: () {}, |
832 | ), |
833 | ), |
834 | ), |
835 | ), |
836 | ); |
837 | |
838 | expect(tester.getSize(find.byKey(key2)), const Size(88.0, 36.0)); |
839 | }); |
840 | |
841 | testWidgets('MaterialButton shape overrides ButtonTheme shape' , (WidgetTester tester) async { |
842 | // Regression test for https://github.com/flutter/flutter/issues/29146 |
843 | await tester.pumpWidget( |
844 | Directionality( |
845 | textDirection: TextDirection.ltr, |
846 | child: MaterialButton( |
847 | onPressed: () {}, |
848 | shape: const StadiumBorder(), |
849 | child: const Text('button' ), |
850 | ), |
851 | ), |
852 | ); |
853 | |
854 | final Finder rawButtonMaterial = find.descendant( |
855 | of: find.byType(MaterialButton), |
856 | matching: find.byType(Material), |
857 | ); |
858 | expect(tester.widget<Material>(rawButtonMaterial).shape, const StadiumBorder()); |
859 | }); |
860 | |
861 | testWidgets('MaterialButton responds to density changes.' , (WidgetTester tester) async { |
862 | const Key key = Key('test' ); |
863 | const Key childKey = Key('test child' ); |
864 | |
865 | Future<void> buildTest(VisualDensity visualDensity, {bool useText = false}) async { |
866 | return tester.pumpWidget( |
867 | MaterialApp( |
868 | theme: ThemeData(useMaterial3: false), |
869 | home: Directionality( |
870 | textDirection: TextDirection.rtl, |
871 | child: Center( |
872 | child: MaterialButton( |
873 | visualDensity: visualDensity, |
874 | key: key, |
875 | onPressed: () {}, |
876 | child: |
877 | useText |
878 | ? const Text('Text' , key: childKey) |
879 | : Container( |
880 | key: childKey, |
881 | width: 100, |
882 | height: 100, |
883 | color: const Color(0xffff0000), |
884 | ), |
885 | ), |
886 | ), |
887 | ), |
888 | ), |
889 | ); |
890 | } |
891 | |
892 | await buildTest(VisualDensity.standard); |
893 | final RenderBox box = tester.renderObject(find.byKey(key)); |
894 | Rect childRect = tester.getRect(find.byKey(childKey)); |
895 | await tester.pumpAndSettle(); |
896 | expect(box.size, equals(const Size(132, 100))); |
897 | expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350))); |
898 | |
899 | await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0)); |
900 | await tester.pumpAndSettle(); |
901 | childRect = tester.getRect(find.byKey(childKey)); |
902 | expect(box.size, equals(const Size(156, 124))); |
903 | expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350))); |
904 | |
905 | await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0)); |
906 | await tester.pumpAndSettle(); |
907 | childRect = tester.getRect(find.byKey(childKey)); |
908 | expect(box.size, equals(const Size(108, 100))); |
909 | expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350))); |
910 | |
911 | await buildTest(VisualDensity.standard, useText: true); |
912 | await tester.pumpAndSettle(); |
913 | childRect = tester.getRect(find.byKey(childKey)); |
914 | expect(box.size, equals(const Size(88, 48))); |
915 | expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0))); |
916 | |
917 | await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0), useText: true); |
918 | await tester.pumpAndSettle(); |
919 | childRect = tester.getRect(find.byKey(childKey)); |
920 | expect(box.size, equals(const Size(112, 60))); |
921 | expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0))); |
922 | |
923 | await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0), useText: true); |
924 | await tester.pumpAndSettle(); |
925 | childRect = tester.getRect(find.byKey(childKey)); |
926 | expect(box.size, equals(const Size(76, 36))); |
927 | expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0))); |
928 | }); |
929 | |
930 | testWidgets('disabledElevation is passed to RawMaterialButton' , (WidgetTester tester) async { |
931 | const double disabledElevation = 16; |
932 | |
933 | final Finder rawMaterialButtonFinder = find.descendant( |
934 | of: find.byType(MaterialButton), |
935 | matching: find.byType(RawMaterialButton), |
936 | ); |
937 | |
938 | await tester.pumpWidget( |
939 | const Directionality( |
940 | textDirection: TextDirection.ltr, |
941 | child: MaterialButton( |
942 | disabledElevation: disabledElevation, |
943 | onPressed: null, // disabled button |
944 | child: Text('button' ), |
945 | ), |
946 | ), |
947 | ); |
948 | |
949 | final RawMaterialButton rawMaterialButton = tester.widget(rawMaterialButtonFinder); |
950 | expect(rawMaterialButton.disabledElevation, equals(disabledElevation)); |
951 | }); |
952 | |
953 | testWidgets('MaterialButton.disabledElevation defaults to 0.0 when not provided' , ( |
954 | WidgetTester tester, |
955 | ) async { |
956 | final Finder rawMaterialButtonFinder = find.descendant( |
957 | of: find.byType(MaterialButton), |
958 | matching: find.byType(RawMaterialButton), |
959 | ); |
960 | |
961 | await tester.pumpWidget( |
962 | const Directionality( |
963 | textDirection: TextDirection.ltr, |
964 | child: MaterialButton( |
965 | onPressed: null, // disabled button |
966 | child: Text('button' ), |
967 | ), |
968 | ), |
969 | ); |
970 | |
971 | final RawMaterialButton rawMaterialButton = tester.widget(rawMaterialButtonFinder); |
972 | expect(rawMaterialButton.disabledElevation, equals(0.0)); |
973 | }); |
974 | } |
975 | |