material_test.dart 26.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';
6
import 'package:flutter/painting.dart';
7
import 'package:flutter/rendering.dart';
8 9
import 'package:flutter_test/flutter_test.dart';

10
import '../rendering/mock_canvas.dart';
11
import '../widgets/test_border.dart' show TestBorder;
12

13
class NotifyMaterial extends StatelessWidget {
14
  const NotifyMaterial({ Key? key }) : super(key: key);
15 16
  @override
  Widget build(BuildContext context) {
17 18
    LayoutChangedNotification().dispatch(context);
    return Container();
19 20 21
  }
}

22 23 24
Widget buildMaterial({
  double elevation = 0.0,
  Color shadowColor = const Color(0xFF00FF00),
25
  Color color = const Color(0xFF0000FF),
26
}) {
27 28
  return Center(
    child: SizedBox(
29 30
      height: 100.0,
      width: 100.0,
31
      child: Material(
32
        color: color,
33
        shadowColor: shadowColor,
34
        elevation: elevation,
35
        shape: const CircleBorder(),
36 37 38 39 40
      ),
    ),
  );
}

41
RenderPhysicalShape getModel(WidgetTester tester) {
42
  return tester.renderObject(find.byType(PhysicalShape));
43 44
}

45 46 47
class PaintRecorder extends CustomPainter {
  PaintRecorder(this.log);

48
  final List<Size> log;
49 50 51 52

  @override
  void paint(Canvas canvas, Size size) {
    log.add(size);
53
    final Paint paint = Paint()..color = const Color(0xFF0000FF);
54
    canvas.drawRect(Offset.zero & size, paint);
55 56 57 58 59 60
  }

  @override
  bool shouldRepaint(PaintRecorder oldDelegate) => false;
}

61 62 63 64 65 66
class ElevationColor {
  const ElevationColor(this.elevation, this.color);
  final double elevation;
  final Color color;
}

67
void main() {
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
  testWidgets('default Material debugFillProperties', (WidgetTester tester) async {
    final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
    const Material().debugFillProperties(builder);

    final List<String> description = builder.properties
      .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
      .map((DiagnosticsNode node) => node.toString())
      .toList();

    expect(description, <String>['type: canvas']);
  });

  testWidgets('Material implements debugFillProperties', (WidgetTester tester) async {
    final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
    const Material(
      type: MaterialType.canvas,
      color: Color(0xFFFFFFFF),
85
      shadowColor: Color(0xffff0000),
86 87 88 89 90 91 92 93 94 95 96 97
      textStyle: TextStyle(color: Color(0xff00ff00)),
      borderRadius: BorderRadiusDirectional.all(Radius.circular(10)),
    ).debugFillProperties(builder);

    final List<String> description = builder.properties
      .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
      .map((DiagnosticsNode node) => node.toString())
      .toList();

    expect(description, <String>[
      'type: canvas',
      'color: Color(0xffffffff)',
98
      'shadowColor: Color(0xffff0000)',
99 100
      'textStyle.inherit: true',
      'textStyle.color: Color(0xff00ff00)',
101
      'borderRadius: BorderRadiusDirectional.circular(10.0)',
102 103 104
    ]);
  });

105
  testWidgets('LayoutChangedNotification test', (WidgetTester tester) async {
106
    await tester.pumpWidget(
107
      const Material(
108
        child: NotifyMaterial(),
109
      ),
110 111
    );
  });
112 113

  testWidgets('ListView scroll does not repaint', (WidgetTester tester) async {
114
    final List<Size> log = <Size>[];
115 116

    await tester.pumpWidget(
117
      Directionality(
118
        textDirection: TextDirection.ltr,
119
        child: Column(
120
          children: <Widget>[
121
            SizedBox(
122 123
              width: 150.0,
              height: 150.0,
124 125
              child: CustomPaint(
                painter: PaintRecorder(log),
126
              ),
127
            ),
128 129 130
            Expanded(
              child: Material(
                child: Column(
131
                  children: <Widget>[
132 133
                    Expanded(
                      child: ListView(
134
                        children: <Widget>[
135
                          Container(
136 137 138 139 140
                            height: 2000.0,
                            color: const Color(0xFF00FF00),
                          ),
                        ],
                      ),
141
                    ),
142
                    SizedBox(
143 144
                      width: 100.0,
                      height: 100.0,
145 146
                      child: CustomPaint(
                        painter: PaintRecorder(log),
147
                      ),
148
                    ),
149 150
                  ],
                ),
151 152
              ),
            ),
153 154
          ],
        ),
155 156 157 158 159 160 161 162 163 164 165
      ),
    );

    // We paint twice because we have two CustomPaint widgets in the tree above
    // to test repainting both inside and outside the Material widget.
    expect(log, equals(<Size>[
      const Size(150.0, 150.0),
      const Size(100.0, 100.0),
    ]));
    log.clear();

166
    await tester.drag(find.byType(ListView), const Offset(0.0, -300.0));
167 168 169 170
    await tester.pump();

    expect(log, isEmpty);
  });
171 172

  testWidgets('Shadows animate smoothly', (WidgetTester tester) async {
173 174
    // This code verifies that the PhysicalModel's elevation animates over
    // a kThemeChangeDuration time interval.
175

176
    await tester.pumpWidget(buildMaterial(elevation: 0.0));
177
    final RenderPhysicalShape modelA = getModel(tester);
178 179
    expect(modelA.elevation, equals(0.0));

180
    await tester.pumpWidget(buildMaterial(elevation: 9.0));
181
    final RenderPhysicalShape modelB = getModel(tester);
182
    expect(modelB.elevation, equals(0.0));
183 184

    await tester.pump(const Duration(milliseconds: 1));
185
    final RenderPhysicalShape modelC = getModel(tester);
186
    expect(modelC.elevation, moreOrLessEquals(0.0, epsilon: 0.001));
187 188

    await tester.pump(kThemeChangeDuration ~/ 2);
189
    final RenderPhysicalShape modelD = getModel(tester);
190
    expect(modelD.elevation, isNot(moreOrLessEquals(0.0, epsilon: 0.001)));
191 192

    await tester.pump(kThemeChangeDuration);
193
    final RenderPhysicalShape modelE = getModel(tester);
194
    expect(modelE.elevation, equals(9.0));
195
  });
196 197

  testWidgets('Shadow colors animate smoothly', (WidgetTester tester) async {
198
    // This code verifies that the PhysicalModel's shadowColor animates over
199 200 201
    // a kThemeChangeDuration time interval.

    await tester.pumpWidget(buildMaterial(shadowColor: const Color(0xFF00FF00)));
202
    final RenderPhysicalShape modelA = getModel(tester);
203 204 205
    expect(modelA.shadowColor, equals(const Color(0xFF00FF00)));

    await tester.pumpWidget(buildMaterial(shadowColor: const Color(0xFFFF0000)));
206
    final RenderPhysicalShape modelB = getModel(tester);
207 208 209
    expect(modelB.shadowColor, equals(const Color(0xFF00FF00)));

    await tester.pump(const Duration(milliseconds: 1));
210
    final RenderPhysicalShape modelC = getModel(tester);
211
    expect(modelC.shadowColor, within<Color>(distance: 1, from: const Color(0xFF00FF00)));
212 213

    await tester.pump(kThemeChangeDuration ~/ 2);
214
    final RenderPhysicalShape modelD = getModel(tester);
215
    expect(modelD.shadowColor, isNot(within<Color>(distance: 1, from: const Color(0xFF00FF00))));
216 217

    await tester.pump(kThemeChangeDuration);
218
    final RenderPhysicalShape modelE = getModel(tester);
219 220
    expect(modelE.shadowColor, equals(const Color(0xFFFF0000)));
  });
221

222 223 224 225 226 227 228 229
  testWidgets('Transparent material widget does not absorb hit test', (WidgetTester tester) async {
    // This is a regression test for https://github.com/flutter/flutter/issues/58665.
    bool pressed = false;
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Stack(
            children: <Widget>[
230
              ElevatedButton(
231 232 233
                onPressed: () {
                  pressed = true;
                },
234
                child: null,
235 236 237 238 239 240 241 242 243 244 245 246 247
              ),
              Material(
                type: MaterialType.transparency,
                child: Container(
                  width: 400.0,
                  height: 500.0,
                ),
              ),
            ],
          ),
        ),
      ),
    );
248
    await tester.tap(find.byType(ElevatedButton));
249 250 251
    expect(pressed, isTrue);
  });

252 253 254 255 256 257 258 259 260
  group('Elevation Overlay', () {

    testWidgets('applyElevationOverlayColor set to false does not change surface color', (WidgetTester tester) async {
      const Color surfaceColor = Color(0xFF121212);
      await tester.pumpWidget(Theme(
          data: ThemeData(
            applyElevationOverlayColor: false,
            colorScheme: const ColorScheme.dark().copyWith(surface: surfaceColor),
          ),
261 262
          child: buildMaterial(color: surfaceColor, elevation: 8.0),
      ));
263 264 265 266
      final RenderPhysicalShape model = getModel(tester);
      expect(model.color, equals(surfaceColor));
    });

267 268 269 270
    testWidgets('applyElevationOverlayColor set to true applies a semi-transparent onSurface color to the surface color', (WidgetTester tester) async {
      const Color surfaceColor = Color(0xFF121212);
      const Color onSurfaceColor = Colors.greenAccent;

271
      // The colors we should get with a base surface color of 0xFF121212 for
272
      // and a given elevation
273 274
      const List<ElevationColor> elevationColors = <ElevationColor>[
        ElevationColor(0.0, Color(0xFF121212)),
275 276 277 278 279 280 281 282 283
        ElevationColor(1.0, Color(0xFF161D19)),
        ElevationColor(2.0, Color(0xFF18211D)),
        ElevationColor(3.0, Color(0xFF19241E)),
        ElevationColor(4.0, Color(0xFF1A2620)),
        ElevationColor(6.0, Color(0xFF1B2922)),
        ElevationColor(8.0, Color(0xFF1C2C24)),
        ElevationColor(12.0, Color(0xFF1D3027)),
        ElevationColor(16.0, Color(0xFF1E3329)),
        ElevationColor(24.0, Color(0xFF20362B)),
284 285
      ];

286
      for (final ElevationColor test in elevationColors) {
287 288 289 290
        await tester.pumpWidget(
            Theme(
              data: ThemeData(
                applyElevationOverlayColor: true,
291 292 293 294
                colorScheme: const ColorScheme.dark().copyWith(
                  surface: surfaceColor,
                  onSurface: onSurfaceColor,
                ),
295 296 297 298 299
              ),
              child: buildMaterial(
                color: surfaceColor,
                elevation: test.elevation,
              ),
300
            ),
301 302 303 304 305 306 307
        );
        await tester.pumpAndSettle(); // wait for the elevation animation to finish
        final RenderPhysicalShape model = getModel(tester);
        expect(model.color, equals(test.color));
      }
    });

308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
    testWidgets('overlay will not apply to materials using a non-surface color', (WidgetTester tester) async {
      await tester.pumpWidget(
        Theme(
          data: ThemeData(
            applyElevationOverlayColor: true,
            colorScheme: const ColorScheme.dark(),
          ),
          child: buildMaterial(
            color: Colors.cyan,
            elevation: 8.0,
          ),
        ),
      );
      final RenderPhysicalShape model = getModel(tester);
      // Shouldn't change, as it is not using a ColorScheme.surface color
      expect(model.color, equals(Colors.cyan));
    });

326
    testWidgets('overlay will not apply to materials using a light theme', (WidgetTester tester) async {
327 328 329 330
      await tester.pumpWidget(
          Theme(
            data: ThemeData(
              applyElevationOverlayColor: true,
331
              colorScheme: const ColorScheme.light(),
332 333
            ),
            child: buildMaterial(
334 335
              color: Colors.cyan,
              elevation: 8.0,
336
            ),
337
          ),
338 339
      );
      final RenderPhysicalShape model = getModel(tester);
340
      // Shouldn't change, as it was under a light color scheme.
341 342 343
      expect(model.color, equals(Colors.cyan));
    });

344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364
    testWidgets('overlay will apply to materials with a non-opaque surface color', (WidgetTester tester) async {
      const Color surfaceColor = Color(0xFF121212);
      const Color surfaceColorWithOverlay = Color(0xC6353535);

      await tester.pumpWidget(
        Theme(
          data: ThemeData(
            applyElevationOverlayColor: true,
            colorScheme: const ColorScheme.dark(surface: surfaceColor),
          ),
          child: buildMaterial(
            color: surfaceColor.withOpacity(.75),
            elevation: 8.0,
          ),
        ),
      );

      final RenderPhysicalShape model = getModel(tester);
      expect(model.color, equals(surfaceColorWithOverlay));
      expect(model.color, isNot(equals(surfaceColor)));
    });
365 366
  });

367
  group('Transparency clipping', () {
368
    testWidgets('No clip by default', (WidgetTester tester) async {
369
      final GlobalKey materialKey = GlobalKey();
370
      await tester.pumpWidget(
371
          Material(
372 373 374
            key: materialKey,
            type: MaterialType.transparency,
            child: const SizedBox(width: 100.0, height: 100.0),
375
          ),
376 377 378
      );

      expect(find.byKey(materialKey), hasNoImmediateClip);
379
    });
380 381

    testWidgets('clips to bounding rect by default given Clip.antiAlias', (WidgetTester tester) async {
382
      final GlobalKey materialKey = GlobalKey();
383
      await tester.pumpWidget(
384
        Material(
385 386
          key: materialKey,
          type: MaterialType.transparency,
387 388
          child: const SizedBox(width: 100.0, height: 100.0),
          clipBehavior: Clip.antiAlias,
389
        ),
390 391
      );

392
      expect(find.byKey(materialKey), clipsWithBoundingRect);
393 394
    });

395
    testWidgets('clips to rounded rect when borderRadius provided given Clip.antiAlias', (WidgetTester tester) async {
396
      final GlobalKey materialKey = GlobalKey();
397
      await tester.pumpWidget(
398
        Material(
399 400
          key: materialKey,
          type: MaterialType.transparency,
401
          borderRadius: const BorderRadius.all(Radius.circular(10.0)),
402 403
          child: const SizedBox(width: 100.0, height: 100.0),
          clipBehavior: Clip.antiAlias,
404
        ),
405 406 407 408 409
      );

      expect(
        find.byKey(materialKey),
        clipsWithBoundingRRect(
410
          borderRadius: const BorderRadius.all(Radius.circular(10.0))
411 412 413
        ),
      );
    });
414

415
    testWidgets('clips to shape when provided given Clip.antiAlias', (WidgetTester tester) async {
416
      final GlobalKey materialKey = GlobalKey();
417
      await tester.pumpWidget(
418
        Material(
419 420 421
          key: materialKey,
          type: MaterialType.transparency,
          shape: const StadiumBorder(),
422 423
          child: const SizedBox(width: 100.0, height: 100.0),
          clipBehavior: Clip.antiAlias,
424
        ),
425 426 427 428 429 430 431 432 433
      );

      expect(
        find.byKey(materialKey),
        clipsWithShapeBorder(
          shape: const StadiumBorder(),
        ),
      );
    });
434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496

    testWidgets('supports directional clips', (WidgetTester tester) async {
      final List<String> logs = <String>[];
      final ShapeBorder shape = TestBorder((String message) { logs.add(message); });
      Widget buildMaterial() {
        return Material(
          type: MaterialType.transparency,
          shape: shape,
          child: const SizedBox(width: 100.0, height: 100.0),
          clipBehavior: Clip.antiAlias,
        );
      }
      final Widget material = buildMaterial();
      // verify that a regular clip works as one would expect
      logs.add('--0');
      await tester.pumpWidget(material);
      // verify that pumping again doesn't recompute the clip
      // even though the widget itself is new (the shape doesn't change identity)
      logs.add('--1');
      await tester.pumpWidget(buildMaterial());
      // verify that Material passes the TextDirection on to its shape when it's transparent
      logs.add('--2');
      await tester.pumpWidget(Directionality(
        textDirection: TextDirection.ltr,
        child: material,
      ));
      // verify that changing the text direction from LTR to RTL has an effect
      // even though the widget itself is identical
      logs.add('--3');
      await tester.pumpWidget(Directionality(
        textDirection: TextDirection.rtl,
        child: material,
      ));
      // verify that pumping again with a text direction has no effect
      logs.add('--4');
      await tester.pumpWidget(Directionality(
        textDirection: TextDirection.rtl,
        child: buildMaterial(),
      ));
      logs.add('--5');
      // verify that changing the text direction and the widget at the same time
      // works as expected
      await tester.pumpWidget(Directionality(
        textDirection: TextDirection.ltr,
        child: material,
      ));
      expect(logs, <String>[
        '--0',
        'getOuterPath Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) null',
        'paint Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) null',
        '--1',
        '--2',
        'getOuterPath Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) TextDirection.ltr',
        'paint Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) TextDirection.ltr',
        '--3',
        'getOuterPath Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) TextDirection.rtl',
        'paint Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) TextDirection.rtl',
        '--4',
        '--5',
        'getOuterPath Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) TextDirection.ltr',
        'paint Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) TextDirection.ltr',
      ]);
    });
497 498 499 500
  });

  group('PhysicalModels', () {
    testWidgets('canvas', (WidgetTester tester) async {
501
      final GlobalKey materialKey = GlobalKey();
502
      await tester.pumpWidget(
503
        Material(
504 505
          key: materialKey,
          type: MaterialType.canvas,
506
          child: const SizedBox(width: 100.0, height: 100.0),
507
        ),
508 509 510 511 512 513 514 515 516 517
      );

      expect(find.byKey(materialKey), rendersOnPhysicalModel(
          shape: BoxShape.rectangle,
          borderRadius: BorderRadius.zero,
          elevation: 0.0,
      ));
    });

    testWidgets('canvas with borderRadius and elevation', (WidgetTester tester) async {
518
      final GlobalKey materialKey = GlobalKey();
519
      await tester.pumpWidget(
520
        Material(
521 522
          key: materialKey,
          type: MaterialType.canvas,
523
          borderRadius: const BorderRadius.all(Radius.circular(5.0)),
524 525
          child: const SizedBox(width: 100.0, height: 100.0),
          elevation: 1.0,
526
        ),
527 528 529 530
      );

      expect(find.byKey(materialKey), rendersOnPhysicalModel(
          shape: BoxShape.rectangle,
531
          borderRadius: const BorderRadius.all(Radius.circular(5.0)),
532 533 534 535
          elevation: 1.0,
      ));
    });

536
    testWidgets('canvas with shape and elevation', (WidgetTester tester) async {
537
      final GlobalKey materialKey = GlobalKey();
538
      await tester.pumpWidget(
539
        Material(
540 541 542 543 544
          key: materialKey,
          type: MaterialType.canvas,
          shape: const StadiumBorder(),
          child: const SizedBox(width: 100.0, height: 100.0),
          elevation: 1.0,
545
        ),
546 547 548 549 550 551 552 553
      );

      expect(find.byKey(materialKey), rendersOnPhysicalShape(
          shape: const StadiumBorder(),
          elevation: 1.0,
      ));
    });

554
    testWidgets('card', (WidgetTester tester) async {
555
      final GlobalKey materialKey = GlobalKey();
556
      await tester.pumpWidget(
557
        Material(
558 559 560
          key: materialKey,
          type: MaterialType.card,
          child: const SizedBox(width: 100.0, height: 100.0),
561
        ),
562 563 564 565
      );

      expect(find.byKey(materialKey), rendersOnPhysicalModel(
          shape: BoxShape.rectangle,
566
          borderRadius: const BorderRadius.all(Radius.circular(2.0)),
567 568 569 570 571
          elevation: 0.0,
      ));
    });

    testWidgets('card with borderRadius and elevation', (WidgetTester tester) async {
572
      final GlobalKey materialKey = GlobalKey();
573
      await tester.pumpWidget(
574
        Material(
575 576
          key: materialKey,
          type: MaterialType.card,
577
          borderRadius: const BorderRadius.all(Radius.circular(5.0)),
578 579
          elevation: 5.0,
          child: const SizedBox(width: 100.0, height: 100.0),
580
        ),
581 582 583 584
      );

      expect(find.byKey(materialKey), rendersOnPhysicalModel(
          shape: BoxShape.rectangle,
585
          borderRadius: const BorderRadius.all(Radius.circular(5.0)),
586 587 588 589
          elevation: 5.0,
      ));
    });

590
    testWidgets('card with shape and elevation', (WidgetTester tester) async {
591
      final GlobalKey materialKey = GlobalKey();
592
      await tester.pumpWidget(
593
        Material(
594 595 596 597 598
          key: materialKey,
          type: MaterialType.card,
          shape: const StadiumBorder(),
          elevation: 5.0,
          child: const SizedBox(width: 100.0, height: 100.0),
599
        ),
600 601 602 603 604 605 606 607
      );

      expect(find.byKey(materialKey), rendersOnPhysicalShape(
          shape: const StadiumBorder(),
          elevation: 5.0,
      ));
    });

608
    testWidgets('circle', (WidgetTester tester) async {
609
      final GlobalKey materialKey = GlobalKey();
610
      await tester.pumpWidget(
611
        Material(
612 613 614 615
          key: materialKey,
          type: MaterialType.circle,
          child: const SizedBox(width: 100.0, height: 100.0),
          color: const Color(0xFF0000FF),
616
        ),
617 618 619 620 621 622 623 624 625
      );

      expect(find.byKey(materialKey), rendersOnPhysicalModel(
          shape: BoxShape.circle,
          elevation: 0.0,
      ));
    });

    testWidgets('button', (WidgetTester tester) async {
626
      final GlobalKey materialKey = GlobalKey();
627
      await tester.pumpWidget(
628
        Material(
629 630 631 632
          key: materialKey,
          type: MaterialType.button,
          child: const SizedBox(width: 100.0, height: 100.0),
          color: const Color(0xFF0000FF),
633
        ),
634 635 636 637
      );

      expect(find.byKey(materialKey), rendersOnPhysicalModel(
          shape: BoxShape.rectangle,
638
          borderRadius: const BorderRadius.all(Radius.circular(2.0)),
639 640 641 642 643
          elevation: 0.0,
      ));
    });

    testWidgets('button with elevation and borderRadius', (WidgetTester tester) async {
644
      final GlobalKey materialKey = GlobalKey();
645
      await tester.pumpWidget(
646
        Material(
647 648 649 650
          key: materialKey,
          type: MaterialType.button,
          child: const SizedBox(width: 100.0, height: 100.0),
          color: const Color(0xFF0000FF),
651
          borderRadius: const BorderRadius.all(Radius.circular(6.0)),
652
          elevation: 4.0,
653
        ),
654 655 656 657
      );

      expect(find.byKey(materialKey), rendersOnPhysicalModel(
          shape: BoxShape.rectangle,
658
          borderRadius: const BorderRadius.all(Radius.circular(6.0)),
659 660 661
          elevation: 4.0,
      ));
    });
662 663

    testWidgets('button with elevation and shape', (WidgetTester tester) async {
664
      final GlobalKey materialKey = GlobalKey();
665
      await tester.pumpWidget(
666
        Material(
667 668 669 670 671 672
          key: materialKey,
          type: MaterialType.button,
          child: const SizedBox(width: 100.0, height: 100.0),
          color: const Color(0xFF0000FF),
          shape: const StadiumBorder(),
          elevation: 4.0,
673
        ),
674 675 676 677 678 679 680
      );

      expect(find.byKey(materialKey), rendersOnPhysicalShape(
          shape: const StadiumBorder(),
          elevation: 4.0,
      ));
    });
681
  });
682 683 684

  group('Border painting', () {
    testWidgets('border is painted on physical layers', (WidgetTester tester) async {
685
      final GlobalKey materialKey = GlobalKey();
686
      await tester.pumpWidget(
687
        Material(
688 689 690 691 692
          key: materialKey,
          type: MaterialType.button,
          child: const SizedBox(width: 100.0, height: 100.0),
          color: const Color(0xFF0000FF),
          shape: const CircleBorder(
693
            side: BorderSide(
694
              width: 2.0,
695
              color: Color(0xFF0000FF),
696
            ),
697
          ),
698
        ),
699 700 701 702 703 704 705
      );

      final RenderBox box = tester.renderObject(find.byKey(materialKey));
      expect(box, paints..circle());
    });

    testWidgets('border is painted for transparent material', (WidgetTester tester) async {
706
      final GlobalKey materialKey = GlobalKey();
707
      await tester.pumpWidget(
708
        Material(
709 710 711 712
          key: materialKey,
          type: MaterialType.transparency,
          child: const SizedBox(width: 100.0, height: 100.0),
          shape: const CircleBorder(
713
            side: BorderSide(
714
              width: 2.0,
715
              color: Color(0xFF0000FF),
716
            ),
717
          ),
718
        ),
719 720 721 722 723 724 725
      );

      final RenderBox box = tester.renderObject(find.byKey(materialKey));
      expect(box, paints..circle());
    });

    testWidgets('border is not painted for when border side is none', (WidgetTester tester) async {
726
      final GlobalKey materialKey = GlobalKey();
727
      await tester.pumpWidget(
728
        Material(
729 730 731 732
          key: materialKey,
          type: MaterialType.transparency,
          child: const SizedBox(width: 100.0, height: 100.0),
          shape: const CircleBorder(),
733
        ),
734 735 736 737 738
      );

      final RenderBox box = tester.renderObject(find.byKey(materialKey));
      expect(box, isNot(paints..circle()));
    });
739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762

    testWidgets('border is painted above child by default', (WidgetTester tester) async {
      final Key painterKey = UniqueKey();

      await tester.pumpWidget(MaterialApp(
        home: Scaffold(
          body: RepaintBoundary(
            key: painterKey,
            child: Card(
              child: SizedBox(
                width: 200,
                height: 300,
                child: Material(
                  clipBehavior: Clip.hardEdge,
                  elevation: 0,
                  shape: RoundedRectangleBorder(
                    side: const BorderSide(color: Colors.grey, width: 6),
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: Column(
                    children: <Widget>[
                      Container(
                        color: Colors.green,
                        height: 150,
763
                      ),
764 765 766 767
                    ],
                  ),
                ),
              ),
768 769
            ),
          ),
770 771 772 773 774
        ),
      ));

      await expectLater(
        find.byKey(painterKey),
775
        matchesGoldenFile('material.border_paint_above.png'),
776
      );
777
    });
778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802

    testWidgets('border is painted below child when specified', (WidgetTester tester) async {
      final Key painterKey = UniqueKey();

      await tester.pumpWidget(MaterialApp(
        home: Scaffold(
          body: RepaintBoundary(
            key: painterKey,
            child: Card(
              child: SizedBox(
                width: 200,
                height: 300,
                child: Material(
                  clipBehavior: Clip.hardEdge,
                  elevation: 0,
                  shape: RoundedRectangleBorder(
                    side: const BorderSide(color: Colors.grey, width: 6),
                    borderRadius: BorderRadius.circular(8),
                  ),
                  borderOnForeground: false,
                  child: Column(
                    children: <Widget>[
                      Container(
                        color: Colors.green,
                        height: 150,
803
                      ),
804 805 806 807
                    ],
                  ),
                ),
              ),
808 809
            ),
          ),
810 811 812 813 814
        ),
      ));

      await expectLater(
        find.byKey(painterKey),
815
        matchesGoldenFile('material.border_paint_below.png'),
816
      );
817
    });
818
  });
819
}