// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // This file is run as part of a reduced test set in CI on Mac and Windows // machines. @Tags(<String>['reduced-test-set']) library; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/test_border.dart' show TestBorder; class NotifyMaterial extends StatelessWidget { const NotifyMaterial({ super.key }); @override Widget build(BuildContext context) { const LayoutChangedNotification().dispatch(context); return Container(); } } Widget buildMaterial({ double elevation = 0.0, Color shadowColor = const Color(0xFF00FF00), Color? surfaceTintColor, Color color = const Color(0xFF0000FF), }) { return Center( child: SizedBox( height: 100.0, width: 100.0, child: Material( color: color, shadowColor: shadowColor, surfaceTintColor: surfaceTintColor, elevation: elevation, shape: const CircleBorder(), ), ), ); } RenderPhysicalShape getModel(WidgetTester tester) { return tester.renderObject(find.byType(PhysicalShape)); } class PaintRecorder extends CustomPainter { PaintRecorder(this.log); final List<Size> log; @override void paint(Canvas canvas, Size size) { log.add(size); final Paint paint = Paint()..color = const Color(0xFF0000FF); canvas.drawRect(Offset.zero & size, paint); } @override bool shouldRepaint(PaintRecorder oldDelegate) => false; } class ElevationColor { const ElevationColor(this.elevation, this.color); final double elevation; final Color color; } void main() { // 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); }); 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), shadowColor: Color(0xffff0000), surfaceTintColor: Color(0xff0000ff), 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)', 'shadowColor: Color(0xffff0000)', 'surfaceTintColor: Color(0xff0000ff)', 'textStyle.inherit: true', 'textStyle.color: Color(0xff00ff00)', 'borderRadius: BorderRadiusDirectional.circular(10.0)', ]); }); testWidgets('LayoutChangedNotification test', (WidgetTester tester) async { await tester.pumpWidget( const Material( child: NotifyMaterial(), ), ); }); testWidgets('ListView scroll does not repaint', (WidgetTester tester) async { final List<Size> log = <Size>[]; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Column( children: <Widget>[ SizedBox( width: 150.0, height: 150.0, child: CustomPaint( painter: PaintRecorder(log), ), ), Expanded( child: Material( child: Column( children: <Widget>[ Expanded( child: ListView( children: <Widget>[ Container( height: 2000.0, color: const Color(0xFF00FF00), ), ], ), ), SizedBox( width: 100.0, height: 100.0, child: CustomPaint( painter: PaintRecorder(log), ), ), ], ), ), ), ], ), ), ); // 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(); await tester.drag(find.byType(ListView), const Offset(0.0, -300.0)); await tester.pump(); expect(log, isEmpty); }); 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); }); testWidgets('Shadows animate smoothly', (WidgetTester tester) async { // This code verifies that the PhysicalModel's elevation animates over // a kThemeChangeDuration time interval. await tester.pumpWidget(buildMaterial()); final RenderPhysicalShape modelA = getModel(tester); expect(modelA.elevation, equals(0.0)); await tester.pumpWidget(buildMaterial(elevation: 9.0)); final RenderPhysicalShape modelB = getModel(tester); expect(modelB.elevation, equals(0.0)); await tester.pump(const Duration(milliseconds: 1)); final RenderPhysicalShape modelC = getModel(tester); expect(modelC.elevation, moreOrLessEquals(0.0, epsilon: 0.001)); await tester.pump(kThemeChangeDuration ~/ 2); final RenderPhysicalShape modelD = getModel(tester); expect(modelD.elevation, isNot(moreOrLessEquals(0.0, epsilon: 0.001))); await tester.pump(kThemeChangeDuration); final RenderPhysicalShape modelE = getModel(tester); expect(modelE.elevation, equals(9.0)); }); testWidgets('Shadow colors animate smoothly', (WidgetTester tester) async { // This code verifies that the PhysicalModel's shadowColor animates over // a kThemeChangeDuration time interval. await tester.pumpWidget(buildMaterial()); final RenderPhysicalShape modelA = getModel(tester); expect(modelA.shadowColor, equals(const Color(0xFF00FF00))); await tester.pumpWidget(buildMaterial(shadowColor: const Color(0xFFFF0000))); final RenderPhysicalShape modelB = getModel(tester); expect(modelB.shadowColor, equals(const Color(0xFF00FF00))); await tester.pump(const Duration(milliseconds: 1)); final RenderPhysicalShape modelC = getModel(tester); expect(modelC.shadowColor, within<Color>(distance: 1, from: const Color(0xFF00FF00))); await tester.pump(kThemeChangeDuration ~/ 2); final RenderPhysicalShape modelD = getModel(tester); expect(modelD.shadowColor, isNot(within<Color>(distance: 1, from: const Color(0xFF00FF00)))); await tester.pump(kThemeChangeDuration); final RenderPhysicalShape modelE = getModel(tester); expect(modelE.shadowColor, equals(const Color(0xFFFF0000))); }); 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>[ ElevatedButton( onPressed: () { pressed = true; }, child: null, ), const Material( type: MaterialType.transparency, child: SizedBox( width: 400.0, height: 500.0, ), ), ], ), ), ), ); await tester.tap(find.byType(ElevatedButton)); expect(pressed, isTrue); }); 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); // 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)); // 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)); // 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. testWidgets('applyElevationOverlayColor set to false does not change surface color', (WidgetTester tester) async { const Color surfaceColor = Color(0xFF121212); await tester.pumpWidget(Theme( data: ThemeData( useMaterial3: false, applyElevationOverlayColor: false, 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('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; // The colors we should get with a base surface color of 0xFF121212 for // and a given elevation const List<ElevationColor> elevationColors = <ElevationColor>[ ElevationColor(0.0, Color(0xFF121212)), 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)), ]; for (final ElevationColor test in elevationColors) { await tester.pumpWidget( Theme( data: ThemeData( useMaterial3: false, applyElevationOverlayColor: true, colorScheme: const ColorScheme.dark().copyWith( surface: surfaceColor, onSurface: onSurfaceColor, ), ), child: buildMaterial( color: surfaceColor, elevation: test.elevation, ), ), ); await tester.pumpAndSettle(); // wait for the elevation animation to finish final RenderPhysicalShape model = getModel(tester); expect(model.color, equals(test.color)); } }); testWidgets('overlay will not apply to materials using a non-surface color', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: ThemeData( useMaterial3: false, 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)); }); testWidgets('overlay will not apply to materials using a light theme', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: ThemeData( useMaterial3: false, applyElevationOverlayColor: true, colorScheme: const ColorScheme.light(), ), child: buildMaterial( color: Colors.cyan, elevation: 8.0, ), ), ); final RenderPhysicalShape model = getModel(tester); // Shouldn't change, as it was under a light color scheme. expect(model.color, equals(Colors.cyan)); }); 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( useMaterial3: false, applyElevationOverlayColor: true, colorScheme: const ColorScheme.dark(), ), 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))); }); 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( useMaterial3: false, 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))); }); }); // Elevation Overlay M2 group group('Transparency clipping', () { testWidgets('No clip by default', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( key: materialKey, type: MaterialType.transparency, child: const SizedBox(width: 100.0, height: 100.0), ), ); final RenderClipPath renderClip = tester.allRenderObjects.whereType<RenderClipPath>().first; expect(renderClip.clipBehavior, equals(Clip.none)); }); testWidgets('clips to bounding rect by default given Clip.antiAlias', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( key: materialKey, type: MaterialType.transparency, clipBehavior: Clip.antiAlias, child: const SizedBox(width: 100.0, height: 100.0), ), ); expect(find.byKey(materialKey), clipsWithBoundingRect); }); testWidgets('clips to rounded rect when borderRadius provided given Clip.antiAlias', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( key: materialKey, type: MaterialType.transparency, borderRadius: const BorderRadius.all(Radius.circular(10.0)), clipBehavior: Clip.antiAlias, child: const SizedBox(width: 100.0, height: 100.0), ), ); expect( find.byKey(materialKey), clipsWithBoundingRRect( borderRadius: const BorderRadius.all(Radius.circular(10.0)), ), ); }); testWidgets('clips to shape when provided given Clip.antiAlias', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( key: materialKey, type: MaterialType.transparency, shape: const StadiumBorder(), clipBehavior: Clip.antiAlias, child: const SizedBox(width: 100.0, height: 100.0), ), ); expect( find.byKey(materialKey), clipsWithShapeBorder( shape: const StadiumBorder(), ), ); }); 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, child: const SizedBox(width: 100.0, height: 100.0), ); } 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', ]); }); }); group('PhysicalModels', () { testWidgets('canvas', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( key: materialKey, child: const SizedBox(width: 100.0, height: 100.0), ), ); expect(find.byKey(materialKey), rendersOnPhysicalModel( shape: BoxShape.rectangle, borderRadius: BorderRadius.zero, elevation: 0.0, )); }); testWidgets('canvas with borderRadius and elevation', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( key: materialKey, borderRadius: const BorderRadius.all(Radius.circular(5.0)), elevation: 1.0, child: const SizedBox(width: 100.0, height: 100.0), ), ); expect(find.byKey(materialKey), rendersOnPhysicalModel( shape: BoxShape.rectangle, borderRadius: const BorderRadius.all(Radius.circular(5.0)), elevation: 1.0, )); }); testWidgets('canvas with shape and elevation', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( key: materialKey, shape: const StadiumBorder(), elevation: 1.0, child: const SizedBox(width: 100.0, height: 100.0), ), ); expect(find.byKey(materialKey), rendersOnPhysicalShape( shape: const StadiumBorder(), elevation: 1.0, )); }); testWidgets('card', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( key: materialKey, type: MaterialType.card, child: const SizedBox(width: 100.0, height: 100.0), ), ); expect(find.byKey(materialKey), rendersOnPhysicalModel( shape: BoxShape.rectangle, borderRadius: const BorderRadius.all(Radius.circular(2.0)), elevation: 0.0, )); }); testWidgets('card with borderRadius and elevation', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( key: materialKey, type: MaterialType.card, borderRadius: const BorderRadius.all(Radius.circular(5.0)), elevation: 5.0, child: const SizedBox(width: 100.0, height: 100.0), ), ); expect(find.byKey(materialKey), rendersOnPhysicalModel( shape: BoxShape.rectangle, borderRadius: const BorderRadius.all(Radius.circular(5.0)), elevation: 5.0, )); }); testWidgets('card with shape and elevation', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( key: materialKey, type: MaterialType.card, shape: const StadiumBorder(), elevation: 5.0, child: const SizedBox(width: 100.0, height: 100.0), ), ); expect(find.byKey(materialKey), rendersOnPhysicalShape( shape: const StadiumBorder(), elevation: 5.0, )); }); testWidgets('circle', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( key: materialKey, type: MaterialType.circle, color: const Color(0xFF0000FF), child: const SizedBox(width: 100.0, height: 100.0), ), ); expect(find.byKey(materialKey), rendersOnPhysicalModel( shape: BoxShape.circle, elevation: 0.0, )); }); testWidgets('button', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( key: materialKey, type: MaterialType.button, color: const Color(0xFF0000FF), child: const SizedBox(width: 100.0, height: 100.0), ), ); expect(find.byKey(materialKey), rendersOnPhysicalModel( shape: BoxShape.rectangle, borderRadius: const BorderRadius.all(Radius.circular(2.0)), elevation: 0.0, )); }); testWidgets('button with elevation and borderRadius', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( key: materialKey, type: MaterialType.button, color: const Color(0xFF0000FF), borderRadius: const BorderRadius.all(Radius.circular(6.0)), elevation: 4.0, child: const SizedBox(width: 100.0, height: 100.0), ), ); expect(find.byKey(materialKey), rendersOnPhysicalModel( shape: BoxShape.rectangle, borderRadius: const BorderRadius.all(Radius.circular(6.0)), elevation: 4.0, )); }); testWidgets('button with elevation and shape', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( key: materialKey, type: MaterialType.button, color: const Color(0xFF0000FF), shape: const StadiumBorder(), elevation: 4.0, child: const SizedBox(width: 100.0, height: 100.0), ), ); expect(find.byKey(materialKey), rendersOnPhysicalShape( shape: const StadiumBorder(), elevation: 4.0, )); }); }); group('Border painting', () { testWidgets('border is painted on physical layers', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( key: materialKey, type: MaterialType.button, color: const Color(0xFF0000FF), shape: const CircleBorder( side: BorderSide( width: 2.0, color: Color(0xFF0000FF), ), ), child: const SizedBox(width: 100.0, height: 100.0), ), ); final RenderBox box = tester.renderObject(find.byKey(materialKey)); expect(box, paints..circle()); }); testWidgets('border is painted for transparent material', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( key: materialKey, type: MaterialType.transparency, shape: const CircleBorder( side: BorderSide( width: 2.0, color: Color(0xFF0000FF), ), ), child: const SizedBox(width: 100.0, height: 100.0), ), ); 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 { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( key: materialKey, type: MaterialType.transparency, shape: const CircleBorder(), child: const SizedBox(width: 100.0, height: 100.0), ), ); final RenderBox box = tester.renderObject(find.byKey(materialKey)); expect(box, isNot(paints..circle())); }); testWidgets('Material2 - border is painted above child by default', (WidgetTester tester) async { final Key painterKey = UniqueKey(); await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: Scaffold( body: RepaintBoundary( key: painterKey, child: Card( child: SizedBox( width: 200, height: 300, child: Material( clipBehavior: Clip.hardEdge, shape: const RoundedRectangleBorder( side: BorderSide(color: Colors.grey, width: 6), borderRadius: BorderRadius.all(Radius.circular(8)), ), child: Column( children: <Widget>[ Container( color: Colors.green, height: 150, ), ], ), ), ), ), ), ), )); await expectLater( find.byKey(painterKey), matchesGoldenFile('m2_material.border_paint_above.png'), ); }); testWidgets('Material3 - border is painted above child by default', (WidgetTester tester) async { final Key painterKey = UniqueKey(); await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: true), home: Scaffold( body: RepaintBoundary( key: painterKey, child: Card( child: SizedBox( width: 200, height: 300, child: Material( clipBehavior: Clip.hardEdge, shape: const RoundedRectangleBorder( side: BorderSide(color: Colors.grey, width: 6), borderRadius: BorderRadius.all(Radius.circular(8)), ), child: Column( children: <Widget>[ Container( color: Colors.green, height: 150, ), ], ), ), ), ), ), ), )); await expectLater( find.byKey(painterKey), matchesGoldenFile('m3_material.border_paint_above.png'), ); }); testWidgets('Material2 - border is painted below child when specified', (WidgetTester tester) async { final Key painterKey = UniqueKey(); await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: Scaffold( body: RepaintBoundary( key: painterKey, child: Card( child: SizedBox( width: 200, height: 300, child: Material( clipBehavior: Clip.hardEdge, shape: const RoundedRectangleBorder( side: BorderSide(color: Colors.grey, width: 6), borderRadius: BorderRadius.all(Radius.circular(8)), ), borderOnForeground: false, child: Column( children: <Widget>[ Container( color: Colors.green, height: 150, ), ], ), ), ), ), ), ), )); await expectLater( find.byKey(painterKey), matchesGoldenFile('m2_material.border_paint_below.png'), ); }); testWidgets('Material3 - border is painted below child when specified', (WidgetTester tester) async { final Key painterKey = UniqueKey(); await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: true), home: Scaffold( body: RepaintBoundary( key: painterKey, child: Card( child: SizedBox( width: 200, height: 300, child: Material( clipBehavior: Clip.hardEdge, shape: const RoundedRectangleBorder( side: BorderSide(color: Colors.grey, width: 6), borderRadius: BorderRadius.all(Radius.circular(8)), ), borderOnForeground: false, child: Column( children: <Widget>[ Container( color: Colors.green, height: 150, ), ], ), ), ), ), ), ), )); await expectLater( find.byKey(painterKey), matchesGoldenFile('m3_material.border_paint_below.png'), ); }); }); 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), ), )); final MaterialInkController controller = Material.of(sizedBoxKey.currentContext!); final TrackPaintInkFeature tracker = TrackPaintInkFeature( controller: controller, referenceBox: sizedBoxKey.currentContext!.findRenderObject()! as RenderBox, ); controller.addInkFeature(tracker); expect(tracker.paintCount, 0); final ContainerLayer layer1 = ContainerLayer(); addTearDown(layer1.dispose); // Force a repaint. Since it's offstage, the ink feature should not get painted. materialKey.currentContext!.findRenderObject()!.paint(PaintingContext(layer1, 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); final ContainerLayer layer2 = ContainerLayer(); addTearDown(layer2.dispose); // Force a repaint again. This time, it gets repainted because it is onstage. materialKey.currentContext!.findRenderObject()!.paint(PaintingContext(layer2, Rect.largest), Offset.zero); expect(tracker.paintCount, 2); tracker.dispose(); }); testWidgets('$InkFeature dispatches memory events', (WidgetTester tester) async { await tester.pumpWidget( const Material( child: SizedBox(width: 20, height: 20), ), ); final Element element = tester.element(find.byType(SizedBox)); final MaterialInkController controller = Material.of(element); final RenderBox referenceBox = element.findRenderObject()! as RenderBox; await expectLater( await memoryEvents( () => _InkFeature( controller: controller, referenceBox: referenceBox, ).dispose(), _InkFeature, ), areCreateAndDispose, ); }); 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' ), ); }); }); } class TrackPaintInkFeature extends InkFeature { TrackPaintInkFeature({required super.controller, required super.referenceBox}); int paintCount = 0; @override void paintFeature(Canvas canvas, Matrix4 transform) { paintCount += 1; } } class _InkFeature extends InkFeature { _InkFeature({ required super.controller, required super.referenceBox, }) { controller.addInkFeature(this); } @override void paintFeature(Canvas canvas, Matrix4 transform) {} }