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

5 6 7 8
// This file is run as part of a reduced test set in CI on Mac and Windows
// machines.
@Tags(<String>['reduced-test-set'])

9
import 'package:flutter/material.dart';
10
import 'package:flutter/rendering.dart';
11 12
import 'package:flutter_test/flutter_test.dart';

13
import '../rendering/mock_canvas.dart';
14
import '../widgets/test_border.dart' show TestBorder;
15

16
class NotifyMaterial extends StatelessWidget {
17
  const NotifyMaterial({ super.key });
18 19
  @override
  Widget build(BuildContext context) {
20
    const LayoutChangedNotification().dispatch(context);
21
    return Container();
22 23 24
  }
}

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

46
RenderPhysicalShape getModel(WidgetTester tester) {
47
  return tester.renderObject(find.byType(PhysicalShape));
48 49
}

50 51 52
class PaintRecorder extends CustomPainter {
  PaintRecorder(this.log);

53
  final List<Size> log;
54 55 56 57

  @override
  void paint(Canvas canvas, Size size) {
    log.add(size);
58
    final Paint paint = Paint()..color = const Color(0xFF0000FF);
59
    canvas.drawRect(Offset.zero & size, paint);
60 61 62 63 64 65
  }

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

66 67 68 69 70 71
class ElevationColor {
  const ElevationColor(this.elevation, this.color);
  final double elevation;
  final Color color;
}

72
void main() {
73 74 75 76 77 78 79 80 81 82 83 84 85 86
  // Regression test for https://github.com/flutter/flutter/issues/81504
  testWidgets('MaterialApp.home nullable and update test', (WidgetTester tester) async {
    // _WidgetsAppState._usesNavigator == true
    await tester.pumpWidget(const MaterialApp(home: SizedBox.shrink()));

    // _WidgetsAppState._usesNavigator == false
    await tester.pumpWidget(const MaterialApp()); // Do not crash!

    // _WidgetsAppState._usesNavigator == true
    await tester.pumpWidget(const MaterialApp(home: SizedBox.shrink())); // Do not crash!

    expect(tester.takeException(), null);
  });

87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
  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(
      color: Color(0xFFFFFFFF),
103
      shadowColor: Color(0xffff0000),
104
      surfaceTintColor: Color(0xff0000ff),
105 106 107 108 109 110 111 112 113 114 115 116
      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)',
117
      'shadowColor: Color(0xffff0000)',
118
      'surfaceTintColor: Color(0xff0000ff)',
119 120
      'textStyle.inherit: true',
      'textStyle.color: Color(0xff00ff00)',
121
      'borderRadius: BorderRadiusDirectional.circular(10.0)',
122 123 124
    ]);
  });

125
  testWidgets('LayoutChangedNotification test', (WidgetTester tester) async {
126
    await tester.pumpWidget(
127
      const Material(
128
        child: NotifyMaterial(),
129
      ),
130 131
    );
  });
132 133

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

    await tester.pumpWidget(
137
      Directionality(
138
        textDirection: TextDirection.ltr,
139
        child: Column(
140
          children: <Widget>[
141
            SizedBox(
142 143
              width: 150.0,
              height: 150.0,
144 145
              child: CustomPaint(
                painter: PaintRecorder(log),
146
              ),
147
            ),
148 149 150
            Expanded(
              child: Material(
                child: Column(
151
                  children: <Widget>[
152 153
                    Expanded(
                      child: ListView(
154
                        children: <Widget>[
155
                          Container(
156 157 158 159 160
                            height: 2000.0,
                            color: const Color(0xFF00FF00),
                          ),
                        ],
                      ),
161
                    ),
162
                    SizedBox(
163 164
                      width: 100.0,
                      height: 100.0,
165 166
                      child: CustomPaint(
                        painter: PaintRecorder(log),
167
                      ),
168
                    ),
169 170
                  ],
                ),
171 172
              ),
            ),
173 174
          ],
        ),
175 176 177 178 179 180 181 182 183 184 185
      ),
    );

    // 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();

186
    await tester.drag(find.byType(ListView), const Offset(0.0, -300.0));
187 188 189 190
    await tester.pump();

    expect(log, isEmpty);
  });
191

192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
  testWidgets('Shadow color defaults', (WidgetTester tester) async {
    Widget buildWithShadow(Color? shadowColor) {
      return Center(
        child: SizedBox(
          height: 100.0,
          width: 100.0,
          child: Material(
            shadowColor: shadowColor,
            elevation: 10,
            shape: const CircleBorder(),
          ),
        )
      );
    }

    // Default M2 shadow color
    await tester.pumpWidget(
        Theme(
          data: ThemeData(
            useMaterial3: false,
          ),
          child: buildWithShadow(null),
        )
    );
    await tester.pumpAndSettle();
    expect(getModel(tester).shadowColor, ThemeData().shadowColor);

    // Default M3 shadow color
    await tester.pumpWidget(
        Theme(
          data: ThemeData(
            useMaterial3: true,
          ),
          child: buildWithShadow(null),
        )
    );
    await tester.pumpAndSettle();
    expect(getModel(tester).shadowColor, ThemeData().colorScheme.shadow);

    // Drop shadow can be turned off with a transparent color.
    await tester.pumpWidget(
        Theme(
          data: ThemeData(
            useMaterial3: true,
          ),
          child: buildWithShadow(Colors.transparent),
        )
    );
    await tester.pumpAndSettle();
    expect(getModel(tester).shadowColor, Colors.transparent);
  });

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

248
    await tester.pumpWidget(buildMaterial());
249
    final RenderPhysicalShape modelA = getModel(tester);
250 251
    expect(modelA.elevation, equals(0.0));

252
    await tester.pumpWidget(buildMaterial(elevation: 9.0));
253
    final RenderPhysicalShape modelB = getModel(tester);
254
    expect(modelB.elevation, equals(0.0));
255 256

    await tester.pump(const Duration(milliseconds: 1));
257
    final RenderPhysicalShape modelC = getModel(tester);
258
    expect(modelC.elevation, moreOrLessEquals(0.0, epsilon: 0.001));
259 260

    await tester.pump(kThemeChangeDuration ~/ 2);
261
    final RenderPhysicalShape modelD = getModel(tester);
262
    expect(modelD.elevation, isNot(moreOrLessEquals(0.0, epsilon: 0.001)));
263 264

    await tester.pump(kThemeChangeDuration);
265
    final RenderPhysicalShape modelE = getModel(tester);
266
    expect(modelE.elevation, equals(9.0));
267
  });
268 269

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

273
    await tester.pumpWidget(buildMaterial());
274
    final RenderPhysicalShape modelA = getModel(tester);
275 276 277
    expect(modelA.shadowColor, equals(const Color(0xFF00FF00)));

    await tester.pumpWidget(buildMaterial(shadowColor: const Color(0xFFFF0000)));
278
    final RenderPhysicalShape modelB = getModel(tester);
279 280 281
    expect(modelB.shadowColor, equals(const Color(0xFF00FF00)));

    await tester.pump(const Duration(milliseconds: 1));
282
    final RenderPhysicalShape modelC = getModel(tester);
283
    expect(modelC.shadowColor, within<Color>(distance: 1, from: const Color(0xFF00FF00)));
284 285

    await tester.pump(kThemeChangeDuration ~/ 2);
286
    final RenderPhysicalShape modelD = getModel(tester);
287
    expect(modelD.shadowColor, isNot(within<Color>(distance: 1, from: const Color(0xFF00FF00))));
288 289

    await tester.pump(kThemeChangeDuration);
290
    final RenderPhysicalShape modelE = getModel(tester);
291 292
    expect(modelE.shadowColor, equals(const Color(0xFFFF0000)));
  });
293

294 295 296 297 298 299 300 301
  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>[
302
              ElevatedButton(
303 304 305
                onPressed: () {
                  pressed = true;
                },
306
                child: null,
307
              ),
308
              const Material(
309
                type: MaterialType.transparency,
310
                child: SizedBox(
311 312 313 314 315 316 317 318 319
                  width: 400.0,
                  height: 500.0,
                ),
              ),
            ],
          ),
        ),
      ),
    );
320
    await tester.tap(find.byType(ElevatedButton));
321 322 323
    expect(pressed, isTrue);
  });

324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
  group('Surface Tint Overlay', () {
    testWidgets('applyElevationOverlayColor does not effect anything with useMaterial3 set to true', (WidgetTester tester) async {
      const Color surfaceColor = Color(0xFF121212);
      await tester.pumpWidget(Theme(
        data: ThemeData(
          useMaterial3: true,
          applyElevationOverlayColor: true,
          colorScheme: const ColorScheme.dark().copyWith(surface: surfaceColor),
        ),
        child: buildMaterial(color: surfaceColor, elevation: 8.0),
      ));
      final RenderPhysicalShape model = getModel(tester);
      expect(model.color, equals(surfaceColor));
    });

    testWidgets('surfaceTintColor is used to as an overlay to indicate elevation', (WidgetTester tester) async {
      const Color baseColor = Color(0xFF121212);
      const Color surfaceTintColor = Color(0xff44CCFF);
342

343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
      // With no surfaceTintColor specified, it should not apply an overlay
      await tester.pumpWidget(
        Theme(
          data: ThemeData(
            useMaterial3: true,
          ),
          child: buildMaterial(
            color: baseColor,
            elevation: 12.0,
          ),
        ),
      );
      await tester.pumpAndSettle();
      final RenderPhysicalShape noTintModel = getModel(tester);
      expect(noTintModel.color, equals(baseColor));

359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375
      // With transparent surfaceTintColor, it should not apply an overlay
      await tester.pumpWidget(
        Theme(
          data: ThemeData(
            useMaterial3: true,
          ),
          child: buildMaterial(
            color: baseColor,
            surfaceTintColor: Colors.transparent,
            elevation: 12.0,
          ),
        ),
      );
      await tester.pumpAndSettle();
      final RenderPhysicalShape transparentTintModel = getModel(tester);
      expect(transparentTintModel.color, equals(baseColor));

376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
      // With surfaceTintColor specified, it should not apply an overlay based
      // on the elevation.
      await tester.pumpWidget(
        Theme(
          data: ThemeData(
            useMaterial3: true,
          ),
          child: buildMaterial(
            color: baseColor,
            surfaceTintColor: surfaceTintColor,
            elevation: 12.0,
          ),
        ),
      );
      await tester.pumpAndSettle();
      final RenderPhysicalShape tintModel = getModel(tester);

      // Final color should be the base with a tint of 0.14 opacity or 0xff192c33
      expect(tintModel.color, equals(const Color(0xff192c33)));
    });

  }); // Surface Tint Overlay group

  group('Elevation Overlay M2', () {
    // These tests only apply to the Material 2 overlay mechanism. This group
    // can be removed after migration to Material 3 is complete.
402 403 404 405
    testWidgets('applyElevationOverlayColor set to false does not change surface color', (WidgetTester tester) async {
      const Color surfaceColor = Color(0xFF121212);
      await tester.pumpWidget(Theme(
          data: ThemeData(
406
            useMaterial3: false,
407 408 409
            applyElevationOverlayColor: false,
            colorScheme: const ColorScheme.dark().copyWith(surface: surfaceColor),
          ),
410 411
          child: buildMaterial(color: surfaceColor, elevation: 8.0),
      ));
412 413 414 415
      final RenderPhysicalShape model = getModel(tester);
      expect(model.color, equals(surfaceColor));
    });

416 417 418 419
    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;

420
      // The colors we should get with a base surface color of 0xFF121212 for
421
      // and a given elevation
422 423
      const List<ElevationColor> elevationColors = <ElevationColor>[
        ElevationColor(0.0, Color(0xFF121212)),
424 425 426 427 428 429 430 431 432
        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)),
433 434
      ];

435
      for (final ElevationColor test in elevationColors) {
436 437 438
        await tester.pumpWidget(
            Theme(
              data: ThemeData(
439
                useMaterial3: false,
440
                applyElevationOverlayColor: true,
441 442 443 444
                colorScheme: const ColorScheme.dark().copyWith(
                  surface: surfaceColor,
                  onSurface: onSurfaceColor,
                ),
445 446 447 448 449
              ),
              child: buildMaterial(
                color: surfaceColor,
                elevation: test.elevation,
              ),
450
            ),
451 452 453 454 455 456 457
        );
        await tester.pumpAndSettle(); // wait for the elevation animation to finish
        final RenderPhysicalShape model = getModel(tester);
        expect(model.color, equals(test.color));
      }
    });

458 459 460 461
    testWidgets('overlay will not apply to materials using a non-surface color', (WidgetTester tester) async {
      await tester.pumpWidget(
        Theme(
          data: ThemeData(
462
            useMaterial3: false,
463 464 465 466 467 468 469 470 471 472 473 474 475 476
            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));
    });

477
    testWidgets('overlay will not apply to materials using a light theme', (WidgetTester tester) async {
478 479 480
      await tester.pumpWidget(
          Theme(
            data: ThemeData(
481
              useMaterial3: false,
482
              applyElevationOverlayColor: true,
483
              colorScheme: const ColorScheme.light(),
484 485
            ),
            child: buildMaterial(
486 487
              color: Colors.cyan,
              elevation: 8.0,
488
            ),
489
          ),
490 491
      );
      final RenderPhysicalShape model = getModel(tester);
492
      // Shouldn't change, as it was under a light color scheme.
493 494 495
      expect(model.color, equals(Colors.cyan));
    });

496 497 498 499 500 501 502
    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(
503
            useMaterial3: false,
504
            applyElevationOverlayColor: true,
505
            colorScheme: const ColorScheme.dark(),
506 507 508 509 510 511 512 513 514 515 516 517
          ),
          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)));
    });
518 519 520 521 522 523 524 525 526 527 528 529

    testWidgets('Expected overlay color can be computed using colorWithOverlay', (WidgetTester tester) async {
      const Color surfaceColor = Color(0xFF123456);
      const Color onSurfaceColor = Color(0xFF654321);
      const double elevation = 8.0;

      final Color surfaceColorWithOverlay =
        ElevationOverlay.colorWithOverlay(surfaceColor, onSurfaceColor, elevation);

      await tester.pumpWidget(
        Theme(
          data: ThemeData(
530
            useMaterial3: false,
531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547
            applyElevationOverlayColor: true,
            colorScheme: const ColorScheme.dark(
              surface: surfaceColor,
              onSurface: onSurfaceColor,
            ),
          ),
          child: buildMaterial(
            color: surfaceColor,
            elevation: elevation,
          ),
        ),
      );

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

  }); // Elevation Overlay M2 group
550

551
  group('Transparency clipping', () {
552
    testWidgets('No clip by default', (WidgetTester tester) async {
553
      final GlobalKey materialKey = GlobalKey();
554
      await tester.pumpWidget(
555
          Material(
556 557 558
            key: materialKey,
            type: MaterialType.transparency,
            child: const SizedBox(width: 100.0, height: 100.0),
559
          ),
560 561
      );

562 563
      final RenderClipPath renderClip = tester.allRenderObjects.whereType<RenderClipPath>().first;
      expect(renderClip.clipBehavior, equals(Clip.none));
564
    });
565 566

    testWidgets('clips to bounding rect by default given Clip.antiAlias', (WidgetTester tester) async {
567
      final GlobalKey materialKey = GlobalKey();
568
      await tester.pumpWidget(
569
        Material(
570 571
          key: materialKey,
          type: MaterialType.transparency,
572
          clipBehavior: Clip.antiAlias,
573
          child: const SizedBox(width: 100.0, height: 100.0),
574
        ),
575 576
      );

577
      expect(find.byKey(materialKey), clipsWithBoundingRect);
578 579
    });

580
    testWidgets('clips to rounded rect when borderRadius provided given Clip.antiAlias', (WidgetTester tester) async {
581
      final GlobalKey materialKey = GlobalKey();
582
      await tester.pumpWidget(
583
        Material(
584 585
          key: materialKey,
          type: MaterialType.transparency,
586
          borderRadius: const BorderRadius.all(Radius.circular(10.0)),
587
          clipBehavior: Clip.antiAlias,
588
          child: const SizedBox(width: 100.0, height: 100.0),
589
        ),
590 591 592 593 594
      );

      expect(
        find.byKey(materialKey),
        clipsWithBoundingRRect(
595
          borderRadius: const BorderRadius.all(Radius.circular(10.0)),
596 597 598
        ),
      );
    });
599

600
    testWidgets('clips to shape when provided given Clip.antiAlias', (WidgetTester tester) async {
601
      final GlobalKey materialKey = GlobalKey();
602
      await tester.pumpWidget(
603
        Material(
604 605 606
          key: materialKey,
          type: MaterialType.transparency,
          shape: const StadiumBorder(),
607
          clipBehavior: Clip.antiAlias,
608
          child: const SizedBox(width: 100.0, height: 100.0),
609
        ),
610 611 612 613 614 615 616 617 618
      );

      expect(
        find.byKey(materialKey),
        clipsWithShapeBorder(
          shape: const StadiumBorder(),
        ),
      );
    });
619 620 621 622 623 624 625 626 627

    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,
          clipBehavior: Clip.antiAlias,
628
          child: const SizedBox(width: 100.0, height: 100.0),
629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681
        );
      }
      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',
      ]);
    });
682 683 684 685
  });

  group('PhysicalModels', () {
    testWidgets('canvas', (WidgetTester tester) async {
686
      final GlobalKey materialKey = GlobalKey();
687
      await tester.pumpWidget(
688
        Material(
689
          key: materialKey,
690
          child: const SizedBox(width: 100.0, height: 100.0),
691
        ),
692 693 694 695 696 697 698 699 700 701
      );

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

    testWidgets('canvas with borderRadius and elevation', (WidgetTester tester) async {
702
      final GlobalKey materialKey = GlobalKey();
703
      await tester.pumpWidget(
704
        Material(
705
          key: materialKey,
706
          borderRadius: const BorderRadius.all(Radius.circular(5.0)),
707
          elevation: 1.0,
708
          child: const SizedBox(width: 100.0, height: 100.0),
709
        ),
710 711 712 713
      );

      expect(find.byKey(materialKey), rendersOnPhysicalModel(
          shape: BoxShape.rectangle,
714
          borderRadius: const BorderRadius.all(Radius.circular(5.0)),
715 716 717 718
          elevation: 1.0,
      ));
    });

719
    testWidgets('canvas with shape and elevation', (WidgetTester tester) async {
720
      final GlobalKey materialKey = GlobalKey();
721
      await tester.pumpWidget(
722
        Material(
723 724 725
          key: materialKey,
          shape: const StadiumBorder(),
          elevation: 1.0,
726
          child: const SizedBox(width: 100.0, height: 100.0),
727
        ),
728 729 730 731 732 733 734 735
      );

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

736
    testWidgets('card', (WidgetTester tester) async {
737
      final GlobalKey materialKey = GlobalKey();
738
      await tester.pumpWidget(
739
        Material(
740 741 742
          key: materialKey,
          type: MaterialType.card,
          child: const SizedBox(width: 100.0, height: 100.0),
743
        ),
744 745 746 747
      );

      expect(find.byKey(materialKey), rendersOnPhysicalModel(
          shape: BoxShape.rectangle,
748
          borderRadius: const BorderRadius.all(Radius.circular(2.0)),
749 750 751 752 753
          elevation: 0.0,
      ));
    });

    testWidgets('card with borderRadius and elevation', (WidgetTester tester) async {
754
      final GlobalKey materialKey = GlobalKey();
755
      await tester.pumpWidget(
756
        Material(
757 758
          key: materialKey,
          type: MaterialType.card,
759
          borderRadius: const BorderRadius.all(Radius.circular(5.0)),
760 761
          elevation: 5.0,
          child: const SizedBox(width: 100.0, height: 100.0),
762
        ),
763 764 765 766
      );

      expect(find.byKey(materialKey), rendersOnPhysicalModel(
          shape: BoxShape.rectangle,
767
          borderRadius: const BorderRadius.all(Radius.circular(5.0)),
768 769 770 771
          elevation: 5.0,
      ));
    });

772
    testWidgets('card with shape and elevation', (WidgetTester tester) async {
773
      final GlobalKey materialKey = GlobalKey();
774
      await tester.pumpWidget(
775
        Material(
776 777 778 779 780
          key: materialKey,
          type: MaterialType.card,
          shape: const StadiumBorder(),
          elevation: 5.0,
          child: const SizedBox(width: 100.0, height: 100.0),
781
        ),
782 783 784 785 786 787 788 789
      );

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

790
    testWidgets('circle', (WidgetTester tester) async {
791
      final GlobalKey materialKey = GlobalKey();
792
      await tester.pumpWidget(
793
        Material(
794 795 796
          key: materialKey,
          type: MaterialType.circle,
          color: const Color(0xFF0000FF),
797
          child: const SizedBox(width: 100.0, height: 100.0),
798
        ),
799 800 801 802 803 804 805 806 807
      );

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

    testWidgets('button', (WidgetTester tester) async {
808
      final GlobalKey materialKey = GlobalKey();
809
      await tester.pumpWidget(
810
        Material(
811 812 813
          key: materialKey,
          type: MaterialType.button,
          color: const Color(0xFF0000FF),
814
          child: const SizedBox(width: 100.0, height: 100.0),
815
        ),
816 817 818 819
      );

      expect(find.byKey(materialKey), rendersOnPhysicalModel(
          shape: BoxShape.rectangle,
820
          borderRadius: const BorderRadius.all(Radius.circular(2.0)),
821 822 823 824 825
          elevation: 0.0,
      ));
    });

    testWidgets('button with elevation and borderRadius', (WidgetTester tester) async {
826
      final GlobalKey materialKey = GlobalKey();
827
      await tester.pumpWidget(
828
        Material(
829 830 831
          key: materialKey,
          type: MaterialType.button,
          color: const Color(0xFF0000FF),
832
          borderRadius: const BorderRadius.all(Radius.circular(6.0)),
833
          elevation: 4.0,
834
          child: const SizedBox(width: 100.0, height: 100.0),
835
        ),
836 837 838 839
      );

      expect(find.byKey(materialKey), rendersOnPhysicalModel(
          shape: BoxShape.rectangle,
840
          borderRadius: const BorderRadius.all(Radius.circular(6.0)),
841 842 843
          elevation: 4.0,
      ));
    });
844 845

    testWidgets('button with elevation and shape', (WidgetTester tester) async {
846
      final GlobalKey materialKey = GlobalKey();
847
      await tester.pumpWidget(
848
        Material(
849 850 851 852 853
          key: materialKey,
          type: MaterialType.button,
          color: const Color(0xFF0000FF),
          shape: const StadiumBorder(),
          elevation: 4.0,
854
          child: const SizedBox(width: 100.0, height: 100.0),
855
        ),
856 857 858 859 860 861 862
      );

      expect(find.byKey(materialKey), rendersOnPhysicalShape(
          shape: const StadiumBorder(),
          elevation: 4.0,
      ));
    });
863
  });
864 865 866

  group('Border painting', () {
    testWidgets('border is painted on physical layers', (WidgetTester tester) async {
867
      final GlobalKey materialKey = GlobalKey();
868
      await tester.pumpWidget(
869
        Material(
870 871 872 873
          key: materialKey,
          type: MaterialType.button,
          color: const Color(0xFF0000FF),
          shape: const CircleBorder(
874
            side: BorderSide(
875
              width: 2.0,
876
              color: Color(0xFF0000FF),
877
            ),
878
          ),
879
          child: const SizedBox(width: 100.0, height: 100.0),
880
        ),
881 882 883 884 885 886 887
      );

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

    testWidgets('border is painted for transparent material', (WidgetTester tester) async {
888
      final GlobalKey materialKey = GlobalKey();
889
      await tester.pumpWidget(
890
        Material(
891 892 893
          key: materialKey,
          type: MaterialType.transparency,
          shape: const CircleBorder(
894
            side: BorderSide(
895
              width: 2.0,
896
              color: Color(0xFF0000FF),
897
            ),
898
          ),
899
          child: const SizedBox(width: 100.0, height: 100.0),
900
        ),
901 902 903 904 905 906 907
      );

      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 {
908
      final GlobalKey materialKey = GlobalKey();
909
      await tester.pumpWidget(
910
        Material(
911 912 913
          key: materialKey,
          type: MaterialType.transparency,
          shape: const CircleBorder(),
914
          child: const SizedBox(width: 100.0, height: 100.0),
915
        ),
916 917 918 919 920
      );

      final RenderBox box = tester.renderObject(find.byKey(materialKey));
      expect(box, isNot(paints..circle()));
    });
921 922 923 924 925 926 927 928 929 930 931 932 933 934

    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,
935 936 937
                  shape: const RoundedRectangleBorder(
                    side: BorderSide(color: Colors.grey, width: 6),
                    borderRadius: BorderRadius.all(Radius.circular(8)),
938 939 940 941 942 943
                  ),
                  child: Column(
                    children: <Widget>[
                      Container(
                        color: Colors.green,
                        height: 150,
944
                      ),
945 946 947 948
                    ],
                  ),
                ),
              ),
949 950
            ),
          ),
951 952 953 954 955
        ),
      ));

      await expectLater(
        find.byKey(painterKey),
956
        matchesGoldenFile('material.border_paint_above.png'),
957
      );
958
    });
959 960 961 962 963 964 965 966 967 968 969 970 971 972

    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,
973 974 975
                  shape: const RoundedRectangleBorder(
                    side: BorderSide(color: Colors.grey, width: 6),
                    borderRadius: BorderRadius.all(Radius.circular(8)),
976 977 978 979 980 981 982
                  ),
                  borderOnForeground: false,
                  child: Column(
                    children: <Widget>[
                      Container(
                        color: Colors.green,
                        height: 150,
983
                      ),
984 985 986 987
                    ],
                  ),
                ),
              ),
988 989
            ),
          ),
990 991 992 993 994
        ),
      ));

      await expectLater(
        find.byKey(painterKey),
995
        matchesGoldenFile('material.border_paint_below.png'),
996
      );
997
    });
998
  });
999 1000 1001 1002 1003 1004 1005 1006 1007 1008

  testWidgets('InkFeature skips painting if intermediate node skips', (WidgetTester tester) async {
    final GlobalKey sizedBoxKey = GlobalKey();
    final GlobalKey materialKey = GlobalKey();
    await tester.pumpWidget(Material(
      key: materialKey,
      child: Offstage(
        child: SizedBox(key: sizedBoxKey, width: 20, height: 20),
      ),
    ));
1009
    final MaterialInkController controller = Material.of(sizedBoxKey.currentContext!);
1010 1011 1012 1013 1014 1015 1016 1017

    final TrackPaintInkFeature tracker = TrackPaintInkFeature(
      controller: controller,
      referenceBox: sizedBoxKey.currentContext!.findRenderObject()! as RenderBox,
    );
    controller.addInkFeature(tracker);
    expect(tracker.paintCount, 0);

1018
    // Force a repaint. Since it's offstage, the ink feature should not get painted.
1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036
    materialKey.currentContext!.findRenderObject()!.paint(PaintingContext(ContainerLayer(), Rect.largest), Offset.zero);
    expect(tracker.paintCount, 0);

    await tester.pumpWidget(Material(
      key: materialKey,
      child: Offstage(
        offstage: false,
        child: SizedBox(key: sizedBoxKey, width: 20, height: 20),
      ),
    ));
    // Gets a paint because the global keys have reused the elements and it is
    // now onstage.
    expect(tracker.paintCount, 1);

    // Force a repaint again. This time, it gets repainted because it is onstage.
    materialKey.currentContext!.findRenderObject()!.paint(PaintingContext(ContainerLayer(), Rect.largest), Offset.zero);
    expect(tracker.paintCount, 2);
  });
1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131

  group('LookupBoundary', () {
    testWidgets('hides Material from Material.maybeOf', (WidgetTester tester) async {
      MaterialInkController? material;

      await tester.pumpWidget(
        Material(
          child: LookupBoundary(
            child: Builder(
              builder: (BuildContext context) {
                material = Material.maybeOf(context);
                return Container();
              },
            ),
          ),
        ),
      );

      expect(material, isNull);
    });

    testWidgets('hides Material from Material.of', (WidgetTester tester) async {
      await tester.pumpWidget(
        Material(
          child: LookupBoundary(
            child: Builder(
              builder: (BuildContext context) {
                Material.of(context);
                return Container();
              },
            ),
          ),
        ),
      );
      final Object? exception = tester.takeException();
      expect(exception, isFlutterError);
      final FlutterError error = exception! as FlutterError;

      expect(
        error.toStringDeep(),
        'FlutterError\n'
        '   Material.of() was called with a context that does not have access\n'
        '   to a Material widget.\n'
        '   The context provided to Material.of() does have a Material widget\n'
        '   ancestor, but it is hidden by a LookupBoundary. This can happen\n'
        '   because you are using a widget that looks for a Material\n'
        '   ancestor, but no such ancestor exists within the closest\n'
        '   LookupBoundary.\n'
        '   The context used was:\n'
        '     Builder(dirty)\n'
      );
    });

    testWidgets('hides Material from debugCheckHasMaterial', (WidgetTester tester) async {
      await tester.pumpWidget(
        Material(
          child: LookupBoundary(
            child: Builder(
              builder: (BuildContext context) {
                debugCheckHasMaterial(context);
                return Container();
              },
            ),
          ),
        ),
      );
      final Object? exception = tester.takeException();
      expect(exception, isFlutterError);
      final FlutterError error = exception! as FlutterError;

      expect(
        error.toStringDeep(), startsWith(
          'FlutterError\n'
          '   No Material widget found within the closest LookupBoundary.\n'
          '   There is an ancestor Material widget, but it is hidden by a\n'
          '   LookupBoundary.\n'
          '   Builder widgets require a Material widget ancestor within the\n'
          '   closest LookupBoundary.\n'
          '   In Material Design, most widgets are conceptually "printed" on a\n'
          "   sheet of material. In Flutter's material library, that material\n"
          '   is represented by the Material widget. It is the Material widget\n'
          '   that renders ink splashes, for instance. Because of this, many\n'
          '   material library widgets require that there be a Material widget\n'
          '   in the tree above them.\n'
          '   To introduce a Material widget, you can either directly include\n'
          '   one, or use a widget that contains Material itself, such as a\n'
          '   Card, Dialog, Drawer, or Scaffold.\n'
          '   The specific widget that could not find a Material ancestor was:\n'
          '     Builder\n'
          '   The ancestors of this widget were:\n'
          '     LookupBoundary\n'
        ),
      );
    });
  });
1132 1133 1134 1135 1136 1137 1138 1139 1140 1141
}

class TrackPaintInkFeature extends InkFeature {
  TrackPaintInkFeature({required super.controller, required super.referenceBox});

  int paintCount = 0;
  @override
  void paintFeature(Canvas canvas, Matrix4 transform) {
    paintCount += 1;
  }
1142
}