Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

1273 lines
42 KiB
Dart

// 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/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
import '../widgets/multi_view_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()..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 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 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: ${const Color(0xffffffff)}',
'shadowColor: ${const Color(0xffff0000)}',
'surfaceTintColor: ${const Color(0xff0000ff)}',
'textStyle.inherit: true',
'textStyle.color: ${const 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 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(), 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(), 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.
var 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);
});
testWidgets('Material uses the dark SystemUIOverlayStyle when the background is light', (
WidgetTester tester,
) async {
final lightTheme = ThemeData();
await tester.pumpWidget(
MaterialApp(
theme: lightTheme,
home: const Scaffold(body: Center(child: Text('test'))),
),
);
expect(lightTheme.colorScheme.brightness, Brightness.light);
expect(SystemChrome.latestStyle, SystemUiOverlayStyle.dark);
});
testWidgets('Material uses the light SystemUIOverlayStyle when the background is dark', (
WidgetTester tester,
) async {
final darkTheme = ThemeData.dark();
await tester.pumpWidget(
MaterialApp(
theme: darkTheme,
home: const Scaffold(body: Center(child: Text('test'))),
),
);
expect(darkTheme.colorScheme.brightness, Brightness.dark);
expect(SystemChrome.latestStyle, SystemUiOverlayStyle.light);
});
group('Surface Tint Overlay', () {
testWidgets(
'applyElevationOverlayColor does not effect anything with useMaterial3 set to true',
(WidgetTester tester) async {
const surfaceColor = Color(0xFF121212);
await tester.pumpWidget(
Theme(
data: ThemeData(
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 baseColor = Color(0xFF121212);
const surfaceTintColor = Color(0xff44CCFF);
// With no surfaceTintColor specified, it should not apply an overlay
await tester.pumpWidget(
Theme(
data: ThemeData(),
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(),
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(),
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, isSameColorAs(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 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 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 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 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, isSameColorAs(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 surfaceColor = Color(0xFF121212);
const 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, isSameColorAs(surfaceColorWithOverlay));
expect(model.color, isNot(isSameColorAs(surfaceColor)));
});
testWidgets('Expected overlay color can be computed using colorWithOverlay', (
WidgetTester tester,
) async {
const surfaceColor = Color(0xFF123456);
const onSurfaceColor = Color(0xFF654321);
const 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 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(
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(
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 tracker = TrackPaintInkFeature(
controller: controller,
referenceBox: sizedBoxKey.currentContext!.findRenderObject()! as RenderBox,
);
controller.addInkFeature(tracker);
expect(tracker.paintCount, 0);
final 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 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 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 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 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',
),
);
});
});
testWidgets('Material is not visible from sub-views', (WidgetTester tester) async {
MaterialInkController? outsideView;
MaterialInkController? insideView;
MaterialInkController? outsideViewAnchor;
await tester.pumpWidget(
Material(
child: Builder(
builder: (BuildContext context) {
outsideViewAnchor = Material.maybeOf(context);
return ViewAnchor(
view: Builder(
builder: (BuildContext context) {
outsideView = Material.maybeOf(context);
return View(
view: FakeView(tester.view),
child: Builder(
builder: (BuildContext context) {
insideView = Material.maybeOf(context);
return const SizedBox();
},
),
);
},
),
child: const SizedBox(),
);
},
),
),
);
expect(outsideViewAnchor, isNotNull);
expect(outsideView, isNull);
expect(insideView, isNull);
});
testWidgets('Material does not crash at zero area', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Center(child: SizedBox(child: Material())),
),
);
expect(tester.getSize(find.byType(Material)), Size.zero);
});
}
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) {}
}