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

2503 lines
78 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.
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/src/gestures/constants.dart';
import 'package:flutter_test/flutter_test.dart';
import '../widgets/semantics_tester.dart';
void main() {
final theme = ThemeData();
setUp(() {
debugResetSemanticsIdCounter();
});
testWidgets('Checkbox size is configurable by ThemeData.materialTapTargetSize', (
WidgetTester tester,
) async {
await tester.pumpWidget(
Theme(
data: theme.copyWith(materialTapTargetSize: MaterialTapTargetSize.padded),
child: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(child: Checkbox(value: true, onChanged: (bool? newValue) {})),
),
),
),
);
expect(tester.getSize(find.byType(Checkbox)), const Size(48.0, 48.0));
await tester.pumpWidget(
Theme(
data: theme.copyWith(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap),
child: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(child: Checkbox(value: true, onChanged: (bool? newValue) {})),
),
),
),
);
expect(tester.getSize(find.byType(Checkbox)), const Size(40.0, 40.0));
});
testWidgets('Checkbox semantics', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(
Theme(
data: theme,
child: Material(child: Checkbox(value: false, onChanged: (bool? b) {})),
),
);
expect(
tester.getSemantics(find.byType(Focus).last),
matchesSemantics(
hasCheckedState: true,
hasEnabledState: true,
isEnabled: true,
hasTapAction: true,
hasFocusAction: true,
isFocusable: true,
),
);
await tester.pumpWidget(
Theme(
data: theme,
child: Material(child: Checkbox(value: true, onChanged: (bool? b) {})),
),
);
expect(
tester.getSemantics(find.byType(Focus).last),
matchesSemantics(
hasCheckedState: true,
hasEnabledState: true,
isChecked: true,
isEnabled: true,
hasTapAction: true,
hasFocusAction: true,
isFocusable: true,
),
);
await tester.pumpWidget(
Theme(
data: theme,
child: const Material(child: Checkbox(value: false, onChanged: null)),
),
);
expect(
tester.getSemantics(find.byType(Checkbox)),
matchesSemantics(
hasCheckedState: true,
hasEnabledState: true,
// isFocusable is delayed by 1 frame.
isFocusable: true,
hasFocusAction: true,
),
);
await tester.pump();
// isFocusable should be false now after the 1 frame delay.
expect(
tester.getSemantics(find.byType(Checkbox)),
matchesSemantics(hasCheckedState: true, hasEnabledState: true),
);
await tester.pumpWidget(
Theme(
data: theme,
child: const Material(child: Checkbox(value: true, onChanged: null)),
),
);
expect(
tester.getSemantics(find.byType(Checkbox)),
matchesSemantics(hasCheckedState: true, hasEnabledState: true, isChecked: true),
);
await tester.pumpWidget(
Theme(
data: theme,
child: const Material(child: Checkbox(value: null, tristate: true, onChanged: null)),
),
);
expect(
tester.getSemantics(find.byType(Checkbox)),
matchesSemantics(hasCheckedState: true, hasEnabledState: true, isCheckStateMixed: true),
);
await tester.pumpWidget(
Theme(
data: theme,
child: const Material(child: Checkbox(value: true, tristate: true, onChanged: null)),
),
);
expect(
tester.getSemantics(find.byType(Checkbox)),
matchesSemantics(hasCheckedState: true, hasEnabledState: true, isChecked: true),
);
await tester.pumpWidget(
Theme(
data: theme,
child: const Material(child: Checkbox(value: false, tristate: true, onChanged: null)),
),
);
expect(
tester.getSemantics(find.byType(Checkbox)),
matchesSemantics(hasCheckedState: true, hasEnabledState: true),
);
// Check if semanticLabel is there.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Theme(
data: theme,
child: Material(
child: Checkbox(semanticLabel: 'checkbox', value: true, onChanged: (bool? b) {}),
),
),
),
);
expect(
tester.getSemantics(find.byType(Focus).last),
matchesSemantics(
label: 'checkbox',
textDirection: TextDirection.ltr,
hasCheckedState: true,
hasEnabledState: true,
isChecked: true,
isEnabled: true,
hasTapAction: true,
hasFocusAction: true,
isFocusable: true,
),
);
handle.dispose();
});
testWidgets('Can wrap Checkbox with Semantics', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(
Theme(
data: theme,
child: Material(
child: Semantics(
label: 'foo',
textDirection: TextDirection.ltr,
child: Checkbox(value: false, onChanged: (bool? b) {}),
),
),
),
);
expect(
tester.getSemantics(find.byType(Focus).last),
matchesSemantics(
label: 'foo',
textDirection: TextDirection.ltr,
hasCheckedState: true,
hasEnabledState: true,
isEnabled: true,
hasTapAction: true,
hasFocusAction: true,
isFocusable: true,
),
);
handle.dispose();
});
testWidgets('Checkbox tristate: true', (WidgetTester tester) async {
bool? checkBoxValue;
await tester.pumpWidget(
Theme(
data: theme,
child: Material(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(
tristate: true,
value: checkBoxValue,
onChanged: (bool? value) {
setState(() {
checkBoxValue = value;
});
},
);
},
),
),
),
);
expect(tester.widget<Checkbox>(find.byType(Checkbox)).value, null);
await tester.tap(find.byType(Checkbox));
await tester.pumpAndSettle();
expect(checkBoxValue, false);
await tester.tap(find.byType(Checkbox));
await tester.pumpAndSettle();
expect(checkBoxValue, true);
await tester.tap(find.byType(Checkbox));
await tester.pumpAndSettle();
expect(checkBoxValue, null);
checkBoxValue = true;
await tester.pumpAndSettle();
expect(checkBoxValue, true);
checkBoxValue = null;
await tester.pumpAndSettle();
expect(checkBoxValue, null);
});
testWidgets('has semantics for tristate', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
Theme(
data: theme,
child: Material(
child: Checkbox(tristate: true, value: null, onChanged: (bool? newValue) {}),
),
),
);
expect(
semantics.nodesWith(
flags: <SemanticsFlag>[
SemanticsFlag.hasCheckedState,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
SemanticsFlag.isCheckStateMixed,
],
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
),
hasLength(1),
);
await tester.pumpWidget(
Theme(
data: theme,
child: Material(
child: Checkbox(tristate: true, value: true, onChanged: (bool? newValue) {}),
),
),
);
expect(
semantics.nodesWith(
flags: <SemanticsFlag>[
SemanticsFlag.hasCheckedState,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isChecked,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
),
hasLength(1),
);
await tester.pumpWidget(
Theme(
data: theme,
child: Material(
child: Checkbox(tristate: true, value: false, onChanged: (bool? newValue) {}),
),
),
);
expect(
semantics.nodesWith(
flags: <SemanticsFlag>[
SemanticsFlag.hasCheckedState,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
),
hasLength(1),
);
semantics.dispose();
});
testWidgets('has semantic events', (WidgetTester tester) async {
dynamic semanticEvent;
bool? checkboxValue = false;
tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(
SystemChannels.accessibility,
(dynamic message) async {
semanticEvent = message;
},
);
final semanticsTester = SemanticsTester(tester);
await tester.pumpWidget(
Theme(
data: theme,
child: Material(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(
value: checkboxValue,
onChanged: (bool? value) {
setState(() {
checkboxValue = value;
});
},
);
},
),
),
),
);
await tester.tap(find.byType(Checkbox));
final RenderObject object = tester.firstRenderObject(find.byType(Checkbox));
expect(checkboxValue, true);
expect(semanticEvent, <String, dynamic>{
'type': 'tap',
'nodeId': object.debugSemantics!.id,
'data': <String, dynamic>{},
});
expect(object.debugSemantics!.getSemanticsData().hasAction(SemanticsAction.tap), true);
tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(
SystemChannels.accessibility,
null,
);
semanticsTester.dispose();
});
testWidgets('Material2 - Checkbox tristate rendering, programmatic transitions', (
WidgetTester tester,
) async {
final theme = ThemeData(useMaterial3: false);
Widget buildFrame(bool? checkboxValue) {
return Theme(
data: theme,
child: Material(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(tristate: true, value: checkboxValue, onChanged: (bool? value) {});
},
),
),
);
}
RenderBox getCheckboxRenderer() {
return tester.renderObject<RenderBox>(find.byType(Checkbox));
}
await tester.pumpWidget(buildFrame(false));
await tester.pumpAndSettle();
expect(
getCheckboxRenderer(),
paints..path(color: Colors.transparent),
); // paint transparent border
expect(getCheckboxRenderer(), isNot(paints..line())); // null is rendered as a line (a "dash")
expect(getCheckboxRenderer(), paints..drrect()); // empty checkbox
await tester.pumpWidget(buildFrame(true));
await tester.pumpAndSettle();
expect(
getCheckboxRenderer(),
paints
..path(color: theme.colorScheme.secondary)
..path(color: const Color(0xFFFFFFFF)),
); // checkmark is rendered as a path
await tester.pumpWidget(buildFrame(false));
await tester.pumpAndSettle();
expect(
getCheckboxRenderer(),
paints..path(color: Colors.transparent),
); // paint transparent border
expect(getCheckboxRenderer(), isNot(paints..line())); // null is rendered as a line (a "dash")
expect(getCheckboxRenderer(), paints..drrect()); // empty checkbox
await tester.pumpWidget(buildFrame(null));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..line()); // null is rendered as a line (a "dash")
await tester.pumpWidget(buildFrame(true));
await tester.pumpAndSettle();
expect(
getCheckboxRenderer(),
paints
..path(color: theme.colorScheme.secondary)
..path(color: const Color(0xFFFFFFFF)),
); // checkmark is rendered as a path
await tester.pumpWidget(buildFrame(null));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..line()); // null is rendered as a line (a "dash")
});
testWidgets('Material3 - Checkbox tristate rendering, programmatic transitions', (
WidgetTester tester,
) async {
final theme = ThemeData();
Widget buildFrame(bool? checkboxValue) {
return Theme(
data: theme,
child: Material(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(tristate: true, value: checkboxValue, onChanged: (bool? value) {});
},
),
),
);
}
RenderBox getCheckboxRenderer() {
return tester.renderObject<RenderBox>(find.byType(Checkbox));
}
await tester.pumpWidget(buildFrame(false));
await tester.pumpAndSettle();
expect(
getCheckboxRenderer(),
paints..path(color: Colors.transparent),
); // paint transparent border
expect(getCheckboxRenderer(), isNot(paints..line())); // null is rendered as a line (a "dash")
expect(getCheckboxRenderer(), paints..drrect()); // empty checkbox
await tester.pumpWidget(buildFrame(true));
await tester.pumpAndSettle();
expect(
getCheckboxRenderer(),
paints
..path(color: theme.colorScheme.primary)
..path(color: theme.colorScheme.onPrimary),
); // checkmark is rendered as a path
await tester.pumpWidget(buildFrame(false));
await tester.pumpAndSettle();
expect(
getCheckboxRenderer(),
paints..path(color: Colors.transparent),
); // paint transparent border
expect(getCheckboxRenderer(), isNot(paints..line())); // null is rendered as a line (a "dash")
expect(getCheckboxRenderer(), paints..drrect()); // empty checkbox
await tester.pumpWidget(buildFrame(null));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..line()); // null is rendered as a line (a "dash")
await tester.pumpWidget(buildFrame(true));
await tester.pumpAndSettle();
expect(
getCheckboxRenderer(),
paints
..path(color: theme.colorScheme.primary)
..path(color: theme.colorScheme.onPrimary),
); // checkmark is rendered as a path
await tester.pumpWidget(buildFrame(null));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..line()); // null is rendered as a line (a "dash")
});
testWidgets('Material2 - Checkbox color rendering', (WidgetTester tester) async {
var theme = ThemeData(useMaterial3: false);
const borderColor = Color(0xff2196f3);
var checkColor = const Color(0xffFFFFFF);
Color activeColor;
Widget buildFrame({Color? activeColor, Color? checkColor, ThemeData? themeData}) {
return Material(
child: Theme(
data: themeData ?? theme,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(
value: true,
activeColor: activeColor,
checkColor: checkColor,
onChanged: (bool? value) {},
);
},
),
),
);
}
RenderBox getCheckboxRenderer() {
return tester.renderObject<RenderBox>(find.byType(Checkbox));
}
await tester.pumpWidget(buildFrame(checkColor: checkColor));
await tester.pumpAndSettle();
expect(
getCheckboxRenderer(),
paints
..path(color: borderColor)
..path(color: checkColor),
); // paints's color is 0xFFFFFFFF (default color)
checkColor = const Color(0xFF000000);
await tester.pumpWidget(buildFrame(checkColor: checkColor));
await tester.pumpAndSettle();
expect(
getCheckboxRenderer(),
paints
..path(color: borderColor)
..path(color: checkColor),
); // paints's color is 0xFF000000 (params)
activeColor = const Color(0xFF00FF00);
final ColorScheme colorScheme = const ColorScheme.light().copyWith(secondary: activeColor);
theme = theme.copyWith(colorScheme: colorScheme);
await tester.pumpWidget(buildFrame(themeData: theme));
await tester.pumpAndSettle();
expect(
getCheckboxRenderer(),
paints..path(color: activeColor),
); // paints's color is 0xFF00FF00 (theme)
activeColor = const Color(0xFF000000);
await tester.pumpWidget(buildFrame(activeColor: activeColor));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..path(color: activeColor));
});
testWidgets('Material3 - Checkbox color rendering', (WidgetTester tester) async {
var theme = ThemeData();
const borderColor = Color(0xFF6750A4);
var checkColor = const Color(0xffFFFFFF);
Color activeColor;
Widget buildFrame({Color? activeColor, Color? checkColor, ThemeData? themeData}) {
return Material(
child: Theme(
data: themeData ?? theme,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(
value: true,
activeColor: activeColor,
checkColor: checkColor,
onChanged: (bool? value) {},
);
},
),
),
);
}
RenderBox getCheckboxRenderer() {
return tester.renderObject<RenderBox>(find.byType(Checkbox));
}
await tester.pumpWidget(buildFrame(checkColor: checkColor));
await tester.pumpAndSettle();
expect(
getCheckboxRenderer(),
paints
..path(color: borderColor)
..path(color: checkColor),
); // paints's color is 0xFFFFFFFF (default color)
checkColor = const Color(0xFF000000);
await tester.pumpWidget(buildFrame(checkColor: checkColor));
await tester.pumpAndSettle();
expect(
getCheckboxRenderer(),
paints
..path(color: borderColor)
..path(color: checkColor),
); // paints's color is 0xFF000000 (params)
activeColor = const Color(0xFF00FF00);
final ColorScheme colorScheme = const ColorScheme.light().copyWith(primary: activeColor);
theme = theme.copyWith(colorScheme: colorScheme);
await tester.pumpWidget(buildFrame(themeData: theme));
await tester.pumpAndSettle();
expect(
getCheckboxRenderer(),
paints..path(color: activeColor),
); // paints's color is 0xFF00FF00 (theme)
activeColor = const Color(0xFF000000);
await tester.pumpWidget(buildFrame(activeColor: activeColor));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..path(color: activeColor));
});
testWidgets('Material2 - Checkbox is focusable and has correct focus color', (
WidgetTester tester,
) async {
final focusNode = FocusNode(debugLabel: 'Checkbox');
addTearDown(focusNode.dispose);
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
bool? value = true;
Widget buildApp({bool enabled = true}) {
return MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Material(
child: Center(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(
value: value,
onChanged: enabled
? (bool? newValue) {
setState(() {
value = newValue;
});
}
: null,
focusColor: Colors.orange[500],
autofocus: true,
focusNode: focusNode,
);
},
),
),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..circle(color: Colors.orange[500])
..path(color: const Color(0xff2196f3))
..path(color: Colors.white),
);
// Check the false value.
value = false;
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..circle(color: Colors.orange[500])
..drrect(
color: const Color(0x8a000000),
outer: RRect.fromLTRBR(15.0, 15.0, 33.0, 33.0, const Radius.circular(1.0)),
inner: RRect.fromLTRBR(17.0, 17.0, 31.0, 31.0, Radius.zero),
),
);
// Check what happens when disabled.
value = false;
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isFalse);
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..drrect(
color: const Color(0x61000000),
outer: RRect.fromLTRBR(15.0, 15.0, 33.0, 33.0, const Radius.circular(1.0)),
inner: RRect.fromLTRBR(17.0, 17.0, 31.0, 31.0, Radius.zero),
),
);
});
testWidgets('Material3 - Checkbox is focusable and has correct focus color', (
WidgetTester tester,
) async {
final focusNode = FocusNode(debugLabel: 'Checkbox');
addTearDown(focusNode.dispose);
final theme = ThemeData();
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
bool? value = true;
Widget buildApp({bool enabled = true}) {
return MaterialApp(
theme: theme,
home: Material(
child: Center(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(
value: value,
onChanged: enabled
? (bool? newValue) {
setState(() {
value = newValue;
});
}
: null,
focusColor: Colors.orange[500],
autofocus: true,
focusNode: focusNode,
);
},
),
),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..circle(color: Colors.orange[500])
..path(color: theme.colorScheme.primary)
..path(color: theme.colorScheme.onPrimary),
);
// Check the false value.
value = false;
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..circle(color: Colors.orange[500])
..drrect(
color: theme.colorScheme.onSurface,
outer: RRect.fromLTRBR(15.0, 15.0, 33.0, 33.0, const Radius.circular(2.0)),
inner: RRect.fromLTRBR(17.0, 17.0, 31.0, 31.0, Radius.zero),
),
);
// Check what happens when disabled.
value = false;
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isFalse);
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..drrect(
color: theme.colorScheme.onSurface.withOpacity(0.38),
outer: RRect.fromLTRBR(15.0, 15.0, 33.0, 33.0, const Radius.circular(2.0)),
inner: RRect.fromLTRBR(17.0, 17.0, 31.0, 31.0, Radius.zero),
),
);
});
testWidgets('Checkbox with splash radius set', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
const double splashRadius = 30;
Widget buildApp() {
return MaterialApp(
theme: theme,
home: Material(
child: Center(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(
value: false,
onChanged: (bool? newValue) {},
focusColor: Colors.orange[500],
autofocus: true,
splashRadius: splashRadius,
);
},
),
),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle(color: Colors.orange[500], radius: splashRadius),
);
});
testWidgets('Checkbox starts the splash in center, even when tap is on the corner', (
WidgetTester tester,
) async {
Widget buildApp() {
return MaterialApp(
theme: theme,
home: Material(
child: Center(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(value: false, onChanged: (bool? newValue) {});
},
),
),
),
);
}
await tester.pumpWidget(buildApp());
final Offset checkboxTopLeftGlobal = tester.getTopLeft(find.byType(Checkbox));
final Offset checkboxCenterGlobal = tester.getCenter(find.byType(Checkbox));
final Offset checkboxCenterLocal = checkboxCenterGlobal - checkboxTopLeftGlobal;
final TestGesture gesture = await tester.startGesture(checkboxTopLeftGlobal);
await tester.pump();
// Wait for the splash to be drawn, but not long enough for it to animate towards the center, since
// we want to catch it in its starting position.
await tester.pump(const Duration(milliseconds: 1));
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle(x: checkboxCenterLocal.dx, y: checkboxCenterLocal.dy),
);
// Finish gesture to release resources.
await gesture.up();
await tester.pumpAndSettle();
});
testWidgets('Material2 - Checkbox can be hovered and has correct hover color', (
WidgetTester tester,
) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
bool? value = true;
final theme = ThemeData(useMaterial3: false);
Widget buildApp({bool enabled = true}) {
return MaterialApp(
theme: theme,
home: Material(
child: Center(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(
value: value,
onChanged: enabled
? (bool? newValue) {
setState(() {
value = newValue;
});
}
: null,
hoverColor: Colors.orange[500],
);
},
),
),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..path(color: const Color(0xff2196f3))
..path(color: const Color(0xffffffff), style: PaintingStyle.stroke, strokeWidth: 2.0),
);
// Start hovering
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await gesture.moveTo(tester.getCenter(find.byType(Checkbox)));
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..circle(color: Colors.orange[500])
..path(color: const Color(0xff2196f3))
..path(color: const Color(0xffffffff), style: PaintingStyle.stroke, strokeWidth: 2.0),
);
// Check what happens when disabled.
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..path(color: const Color(0x61000000))
..path(color: const Color(0xffffffff), style: PaintingStyle.stroke, strokeWidth: 2.0),
);
});
testWidgets('Material3 - Checkbox can be hovered and has correct hover color', (
WidgetTester tester,
) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
bool? value = true;
final theme = ThemeData();
Widget buildApp({bool enabled = true}) {
return MaterialApp(
theme: theme,
home: Material(
child: Center(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(
value: value,
onChanged: enabled
? (bool? newValue) {
setState(() {
value = newValue;
});
}
: null,
hoverColor: Colors.orange[500],
);
},
),
),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..path(color: const Color(0xff6750a4))
..path(color: theme.colorScheme.onPrimary, style: PaintingStyle.stroke, strokeWidth: 2.0),
);
// Start hovering
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await gesture.moveTo(tester.getCenter(find.byType(Checkbox)));
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..circle(color: Colors.orange[500])
..path(color: const Color(0xff6750a4))
..path(color: theme.colorScheme.onPrimary, style: PaintingStyle.stroke, strokeWidth: 2.0),
);
// Check what happens when disabled.
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..path(color: theme.colorScheme.onSurface.withOpacity(0.38))
..path(color: theme.colorScheme.surface, style: PaintingStyle.stroke, strokeWidth: 2.0),
);
});
testWidgets('Checkbox can be toggled by keyboard shortcuts', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
bool? value = true;
Widget buildApp({bool enabled = true}) {
return MaterialApp(
theme: theme,
home: Material(
child: Center(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(
value: value,
onChanged: enabled
? (bool? newValue) {
setState(() {
value = newValue;
});
}
: null,
focusColor: Colors.orange[500],
autofocus: true,
);
},
),
),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle();
// On web, checkboxes don't respond to the enter key.
expect(value, kIsWeb ? isTrue : isFalse);
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle();
expect(value, isTrue);
await tester.sendKeyEvent(LogicalKeyboardKey.space);
await tester.pumpAndSettle();
expect(value, isFalse);
await tester.sendKeyEvent(LogicalKeyboardKey.space);
await tester.pumpAndSettle();
expect(value, isTrue);
});
testWidgets(
'Material3 - Checkbox visual density cannot be overriden by ThemeData.visualDensity',
(WidgetTester tester) async {
const key = Key('test');
Widget buldCheckbox() {
return MaterialApp(
theme: theme.copyWith(visualDensity: VisualDensity.compact),
home: Material(
child: Center(
child: Checkbox(key: key, value: true, onChanged: (bool? value) {}),
),
),
);
}
await tester.pumpWidget(buldCheckbox());
await tester.pumpAndSettle();
final RenderBox box = tester.renderObject(find.byKey(key));
expect(box.size, equals(const Size(48, 48)));
},
);
testWidgets(
'Material3 - Checkbox with MaterialTapTargetSize.padded meets Material Guidelines on desktop',
(WidgetTester tester) async {
const key = Key('test');
Widget buldCheckbox() {
return MaterialApp(
theme: theme,
home: Material(
child: Center(
child: Checkbox(
key: key,
materialTapTargetSize: MaterialTapTargetSize.padded,
value: true,
onChanged: (bool? value) {},
),
),
),
);
}
await tester.pumpWidget(buldCheckbox());
await tester.pumpAndSettle();
final RenderBox box = tester.renderObject(find.byKey(key));
expect(box.size, equals(const Size(48, 48)));
},
variant: TargetPlatformVariant.desktop(),
);
testWidgets('Checkbox responds to density changes', (WidgetTester tester) async {
const key = Key('test');
Future<void> buildTest({VisualDensity? visualDensity}) async {
return tester.pumpWidget(
MaterialApp(
theme: theme,
home: Material(
child: Center(
child: Checkbox(
visualDensity: visualDensity,
key: key,
onChanged: (bool? value) {},
value: true,
),
),
),
),
);
}
// Test the default visual density.
await buildTest();
await tester.pumpAndSettle();
final RenderBox box = tester.renderObject(find.byKey(key));
expect(box.size, equals(const Size(48, 48)));
await buildTest(visualDensity: VisualDensity.standard);
await tester.pumpAndSettle();
expect(box.size, equals(const Size(48, 48)));
await buildTest(visualDensity: VisualDensity.compact);
await tester.pumpAndSettle();
expect(box.size, equals(const Size(40, 40)));
await buildTest(visualDensity: const VisualDensity(horizontal: 3.0, vertical: 3.0));
await tester.pumpAndSettle();
expect(box.size, equals(const Size(60, 60)));
await buildTest(visualDensity: const VisualDensity(horizontal: -3.0, vertical: -3.0));
await tester.pumpAndSettle();
expect(box.size, equals(const Size(36, 36)));
await buildTest(visualDensity: const VisualDensity(horizontal: 3.0, vertical: -3.0));
await tester.pumpAndSettle();
expect(box.size, equals(const Size(60, 36)));
});
testWidgets('Checkbox stops hover animation when removed from the tree.', (
WidgetTester tester,
) async {
const checkboxKey = Key('checkbox');
bool? checkboxVal = true;
await tester.pumpWidget(
Theme(
data: ThemeData(materialTapTargetSize: MaterialTapTargetSize.padded),
child: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: StatefulBuilder(
builder: (_, StateSetter setState) => Checkbox(
key: checkboxKey,
value: checkboxVal,
onChanged: (bool? newValue) => setState(() {
checkboxVal = newValue;
}),
),
),
),
),
),
),
);
expect(find.byKey(checkboxKey), findsOneWidget);
final Offset checkboxCenter = tester.getCenter(find.byKey(checkboxKey));
final TestGesture testGesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await testGesture.moveTo(checkboxCenter);
await tester.pump(); // start animation
await tester.pump(
const Duration(milliseconds: 25),
); // hover animation duration is 50 ms. It is half-way.
await tester.pumpWidget(
Theme(
data: ThemeData(materialTapTargetSize: MaterialTapTargetSize.padded),
child: Directionality(
textDirection: TextDirection.ltr,
child: Material(child: Center(child: Container())),
),
),
);
// Hover animation should not trigger an exception when the checkbox is removed
// before the hover animation should complete.
expect(tester.takeException(), isNull);
await testGesture.removePointer();
});
testWidgets('Checkbox changes mouse cursor when hovered', (WidgetTester tester) async {
// Test Checkbox() constructor
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: Checkbox(
mouseCursor: SystemMouseCursors.text,
value: true,
onChanged: (_) {},
),
),
),
),
),
),
);
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
pointer: 1,
);
await gesture.addPointer(location: tester.getCenter(find.byType(Checkbox)));
addTearDown(gesture.removePointer);
await tester.pump();
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
SystemMouseCursors.text,
);
// Test default cursor
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: Checkbox(value: true, onChanged: (_) {}),
),
),
),
),
),
);
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic,
);
// Test default cursor when disabled
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: const Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: Checkbox(value: true, onChanged: null),
),
),
),
),
),
);
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
SystemMouseCursors.basic,
);
// Test cursor when tristate
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: const Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: Checkbox(
value: null,
tristate: true,
onChanged: null,
mouseCursor: _SelectedGrabMouseCursor(),
),
),
),
),
),
),
);
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
SystemMouseCursors.grab,
);
await tester.pumpAndSettle();
});
testWidgets('Checkbox fill color resolves in enabled/disabled states', (
WidgetTester tester,
) async {
const activeEnabledFillColor = Color(0xFF000001);
const activeDisabledFillColor = Color(0xFF000002);
Color getFillColor(Set<WidgetState> states) {
if (states.contains(WidgetState.disabled)) {
return activeDisabledFillColor;
}
return activeEnabledFillColor;
}
final WidgetStateProperty<Color> fillColor = WidgetStateColor.resolveWith(getFillColor);
Widget buildFrame({required bool enabled}) {
return Material(
child: Theme(
data: theme,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(
value: true,
fillColor: fillColor,
onChanged: enabled ? (bool? value) {} : null,
);
},
),
),
);
}
RenderBox getCheckboxRenderer() {
return tester.renderObject<RenderBox>(find.byType(Checkbox));
}
await tester.pumpWidget(buildFrame(enabled: true));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..path(color: activeEnabledFillColor));
await tester.pumpWidget(buildFrame(enabled: false));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..path(color: activeDisabledFillColor));
});
testWidgets('Checkbox fill color resolves in hovered/focused states', (
WidgetTester tester,
) async {
final focusNode = FocusNode(debugLabel: 'checkbox');
addTearDown(focusNode.dispose);
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
const hoveredFillColor = Color(0xFF000001);
const focusedFillColor = Color(0xFF000002);
Color getFillColor(Set<WidgetState> states) {
if (states.contains(WidgetState.hovered)) {
return hoveredFillColor;
}
if (states.contains(WidgetState.focused)) {
return focusedFillColor;
}
return Colors.transparent;
}
final WidgetStateProperty<Color> fillColor = WidgetStateColor.resolveWith(getFillColor);
Widget buildFrame() {
return Material(
child: Theme(
data: theme,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(
focusNode: focusNode,
autofocus: true,
value: true,
fillColor: fillColor,
onChanged: (bool? value) {},
);
},
),
),
);
}
RenderBox getCheckboxRenderer() {
return tester.renderObject<RenderBox>(find.byType(Checkbox));
}
await tester.pumpWidget(buildFrame());
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(getCheckboxRenderer(), paints..path(color: focusedFillColor));
// Start hovering
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(tester.getCenter(find.byType(Checkbox)));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..path(color: hoveredFillColor));
});
testWidgets('Checkbox respects shape and side', (WidgetTester tester) async {
const roundedRectangleBorder = RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
);
const side = BorderSide(width: 4, color: Color(0xfff44336));
Widget buildApp() {
return MaterialApp(
theme: theme,
home: Material(
child: Center(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(
value: false,
onChanged: (bool? newValue) {},
shape: roundedRectangleBorder,
side: side,
);
},
),
),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(tester.widget<Checkbox>(find.byType(Checkbox)).shape, roundedRectangleBorder);
expect(tester.widget<Checkbox>(find.byType(Checkbox)).side, side);
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..drrect(
color: const Color(0xfff44336),
outer: RRect.fromLTRBR(15.0, 15.0, 33.0, 33.0, const Radius.circular(5)),
inner: RRect.fromLTRBR(19.0, 19.0, 29.0, 29.0, const Radius.circular(1)),
),
);
});
testWidgets(
'Material2 - Checkbox default overlay color in active/pressed/focused/hovered states',
(WidgetTester tester) async {
final focusNode = FocusNode(debugLabel: 'Checkbox');
addTearDown(focusNode.dispose);
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
final theme = ThemeData(useMaterial3: false);
final ColorScheme colors = theme.colorScheme;
Widget buildCheckbox({bool active = false, bool focused = false}) {
return MaterialApp(
theme: theme,
home: Scaffold(
body: Checkbox(
focusNode: focusNode,
autofocus: focused,
value: active,
onChanged: (_) {},
),
),
);
}
await tester.pumpWidget(buildCheckbox());
final TestGesture gesture1 = await tester.startGesture(
tester.getCenter(find.byType(Checkbox)),
);
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle(color: theme.unselectedWidgetColor.withAlpha(kRadialReactionAlpha)),
reason:
'Default inactive pressed Checkbox should have overlay color from default fillColor',
);
await tester.pumpWidget(buildCheckbox(active: true));
final TestGesture gesture2 = await tester.startGesture(
tester.getCenter(find.byType(Checkbox)),
);
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle(color: colors.secondary.withAlpha(kRadialReactionAlpha)),
reason: 'Default active pressed Checkbox should have overlay color from default fillColor',
);
await tester.pumpWidget(Container()); // reset test
await tester.pumpWidget(buildCheckbox(focused: true));
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle(color: theme.focusColor),
reason: 'Focused Checkbox should use default focused overlay color',
);
await tester.pumpWidget(Container()); // reset test
await tester.pumpWidget(buildCheckbox());
final TestGesture gesture3 = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture3.addPointer();
addTearDown(gesture3.removePointer);
await gesture3.moveTo(tester.getCenter(find.byType(Checkbox)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle(color: theme.hoverColor),
reason: 'Hovered Checkbox should use default hovered overlay color',
);
// Finish gestures to release resources.
await gesture1.up();
await gesture2.up();
await tester.pumpAndSettle();
},
);
testWidgets(
'Material3 - Checkbox default overlay color in active/pressed/focused/hovered states',
(WidgetTester tester) async {
final focusNode = FocusNode(debugLabel: 'Checkbox');
addTearDown(focusNode.dispose);
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
final theme = ThemeData();
final ColorScheme colors = theme.colorScheme;
Widget buildCheckbox({bool active = false, bool focused = false}) {
return MaterialApp(
theme: theme,
home: Scaffold(
body: Checkbox(
focusNode: focusNode,
autofocus: focused,
value: active,
onChanged: (_) {},
),
),
);
}
await tester.pumpWidget(buildCheckbox());
final TestGesture gesture1 = await tester.startGesture(
tester.getCenter(find.byType(Checkbox)),
);
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle(color: colors.primary.withOpacity(0.1)),
reason:
'Default inactive pressed Checkbox should have overlay color from default fillColor',
);
await tester.pumpWidget(buildCheckbox(active: true));
final TestGesture gesture2 = await tester.startGesture(
tester.getCenter(find.byType(Checkbox)),
);
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle(color: colors.onSurface.withOpacity(0.1)),
reason: 'Default active pressed Checkbox should have overlay color from default fillColor',
);
await tester.pumpWidget(Container()); // reset test
await tester.pumpWidget(buildCheckbox(focused: true));
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle(color: colors.onSurface.withOpacity(0.1)),
reason: 'Focused Checkbox should use default focused overlay color',
);
await tester.pumpWidget(Container()); // reset test
await tester.pumpWidget(buildCheckbox());
final TestGesture gesture3 = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture3.addPointer();
addTearDown(gesture3.removePointer);
await gesture3.moveTo(tester.getCenter(find.byType(Checkbox)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle(color: colors.onSurface.withOpacity(0.08)),
reason: 'Hovered Checkbox should use default hovered overlay color',
);
// Finish gestures to release resources.
await gesture1.up();
await gesture2.up();
await tester.pumpAndSettle();
},
);
testWidgets('Checkbox overlay color resolves in active/pressed/focused/hovered states', (
WidgetTester tester,
) async {
final focusNode = FocusNode(debugLabel: 'Checkbox');
addTearDown(focusNode.dispose);
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
const fillColor = Color(0xFF000000);
const activePressedOverlayColor = Color(0xFF000001);
const inactivePressedOverlayColor = Color(0xFF000002);
const hoverOverlayColor = Color(0xFF000003);
const focusOverlayColor = Color(0xFF000004);
const hoverColor = Color(0xFF000005);
const focusColor = Color(0xFF000006);
Color? getOverlayColor(Set<WidgetState> states) {
if (states.contains(WidgetState.pressed)) {
if (states.contains(WidgetState.selected)) {
return activePressedOverlayColor;
}
return inactivePressedOverlayColor;
}
if (states.contains(WidgetState.hovered)) {
return hoverOverlayColor;
}
if (states.contains(WidgetState.focused)) {
return focusOverlayColor;
}
return null;
}
const splashRadius = 24.0;
Widget buildCheckbox({bool active = false, bool focused = false, bool useOverlay = true}) {
return MaterialApp(
theme: theme,
home: Scaffold(
body: Checkbox(
focusNode: focusNode,
autofocus: focused,
value: active,
onChanged: (_) {},
fillColor: const MaterialStatePropertyAll<Color>(fillColor),
overlayColor: useOverlay ? WidgetStateProperty.resolveWith(getOverlayColor) : null,
hoverColor: hoverColor,
focusColor: focusColor,
splashRadius: splashRadius,
),
),
);
}
await tester.pumpWidget(buildCheckbox(useOverlay: false));
final TestGesture gesture1 = await tester.startGesture(tester.getCenter(find.byType(Checkbox)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle(color: fillColor.withAlpha(kRadialReactionAlpha), radius: splashRadius),
reason: 'Default inactive pressed Checkbox should have overlay color from fillColor',
);
await tester.pumpWidget(buildCheckbox(active: true, useOverlay: false));
final TestGesture gesture2 = await tester.startGesture(tester.getCenter(find.byType(Checkbox)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle(color: fillColor.withAlpha(kRadialReactionAlpha), radius: splashRadius),
reason: 'Default active pressed Checkbox should have overlay color from fillColor',
);
await tester.pumpWidget(buildCheckbox());
final TestGesture gesture3 = await tester.startGesture(tester.getCenter(find.byType(Checkbox)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle(color: inactivePressedOverlayColor, radius: splashRadius),
reason: 'Inactive pressed Checkbox should have overlay color: $inactivePressedOverlayColor',
);
await tester.pumpWidget(buildCheckbox(active: true));
final TestGesture gesture4 = await tester.startGesture(tester.getCenter(find.byType(Checkbox)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle(color: activePressedOverlayColor, radius: splashRadius),
reason: 'Active pressed Checkbox should have overlay color: $activePressedOverlayColor',
);
await tester.pumpWidget(Container()); // reset test
await tester.pumpWidget(buildCheckbox(focused: true));
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle(color: focusOverlayColor, radius: splashRadius),
reason: 'Focused Checkbox should use overlay color $focusOverlayColor over $focusColor',
);
// Start hovering
final TestGesture gesture5 = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture5.addPointer();
addTearDown(gesture5.removePointer);
await gesture5.moveTo(tester.getCenter(find.byType(Checkbox)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle(color: hoverOverlayColor, radius: splashRadius),
reason: 'Hovered Checkbox should use overlay color $hoverOverlayColor over $hoverColor',
);
// Finish gestures to release resources.
await gesture1.up();
await gesture2.up();
await gesture3.up();
await gesture4.up();
await tester.pumpAndSettle();
});
testWidgets('Tristate Checkbox overlay color resolves in pressed active/inactive states', (
WidgetTester tester,
) async {
const activePressedOverlayColor = Color(0xFF000001);
const inactivePressedOverlayColor = Color(0xFF000002);
Color? getOverlayColor(Set<WidgetState> states) {
if (states.contains(WidgetState.pressed)) {
if (states.contains(WidgetState.selected)) {
return activePressedOverlayColor;
}
return inactivePressedOverlayColor;
}
return null;
}
const splashRadius = 24.0;
TestGesture gesture;
bool? value = false;
Widget buildTristateCheckbox() {
return MaterialApp(
theme: theme,
home: Scaffold(
body: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(
value: value,
tristate: true,
onChanged: (bool? v) {
setState(() {
value = v;
});
},
overlayColor: WidgetStateProperty.resolveWith(getOverlayColor),
splashRadius: splashRadius,
);
},
),
),
);
}
// The checkbox is inactive.
await tester.pumpWidget(buildTristateCheckbox());
gesture = await tester.press(find.byType(Checkbox));
await tester.pumpAndSettle();
expect(value, false);
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle(color: inactivePressedOverlayColor, radius: splashRadius),
reason: 'Inactive pressed Checkbox should have overlay color: $inactivePressedOverlayColor',
);
// The checkbox is active.
await gesture.up();
gesture = await tester.press(find.byType(Checkbox));
await tester.pumpAndSettle();
expect(value, true);
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle(color: activePressedOverlayColor, radius: splashRadius),
reason: 'Active pressed Checkbox should have overlay color: $activePressedOverlayColor',
);
// The checkbox is active in tri-state.
await gesture.up();
gesture = await tester.press(find.byType(Checkbox));
await tester.pumpAndSettle();
expect(value, null);
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle(color: activePressedOverlayColor, radius: splashRadius),
reason:
'Active (tristate) pressed Checkbox should have overlay color: $activePressedOverlayColor',
);
// The checkbox is inactive again.
await gesture.up();
gesture = await tester.press(find.byType(Checkbox));
await tester.pumpAndSettle();
expect(value, false);
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle(color: inactivePressedOverlayColor, radius: splashRadius),
reason: 'Inactive pressed Checkbox should have overlay color: $inactivePressedOverlayColor',
);
await gesture.up();
});
testWidgets('Do not crash when widget disappears while pointer is down', (
WidgetTester tester,
) async {
Widget buildCheckbox(bool show) {
return MaterialApp(
theme: theme,
home: Material(
child: Center(child: show ? Checkbox(value: true, onChanged: (_) {}) : Container()),
),
);
}
await tester.pumpWidget(buildCheckbox(true));
final Offset center = tester.getCenter(find.byType(Checkbox));
// Put a pointer down on the screen.
final TestGesture gesture = await tester.startGesture(center);
await tester.pump();
// While the pointer is down, the widget disappears.
await tester.pumpWidget(buildCheckbox(false));
expect(find.byType(Checkbox), findsNothing);
// Release pointer after widget disappeared.
await gesture.up();
});
testWidgets('Checkbox BorderSide side only applies when unselected in M2', (
WidgetTester tester,
) async {
const borderColor = Color(0xfff44336);
const activeColor = Color(0xff123456);
const side = BorderSide(width: 4, color: borderColor);
Widget buildApp({bool? value, bool enabled = true}) {
return MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Material(
child: Center(
child: Checkbox(
value: value,
tristate: value == null,
activeColor: activeColor,
onChanged: enabled ? (bool? newValue) {} : null,
side: side,
),
),
),
);
}
RenderBox getCheckboxRenderer() {
return tester.renderObject<RenderBox>(find.byType(Checkbox));
}
void expectBorder() {
expect(
getCheckboxRenderer(),
paints..drrect(
color: borderColor,
outer: RRect.fromLTRBR(15, 15, 33, 33, const Radius.circular(1)),
inner: RRect.fromLTRBR(19, 19, 29, 29, Radius.zero),
),
);
}
// Checkbox is unselected, so the specified BorderSide appears.
await tester.pumpWidget(buildApp(value: false));
await tester.pumpAndSettle();
expectBorder();
await tester.pumpWidget(buildApp(value: false, enabled: false));
await tester.pumpAndSettle();
expectBorder();
// Checkbox is selected/indeterminate, so the specified BorderSide is transparent
await tester.pumpWidget(buildApp(value: true));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..drrect(color: Colors.transparent));
expect(getCheckboxRenderer(), paints..path(color: activeColor)); // checkbox fill
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..drrect(color: Colors.transparent));
expect(getCheckboxRenderer(), paints..path(color: activeColor)); // checkbox fill
});
testWidgets('Material2 - Checkbox WidgetStateBorderSide applies unconditionally', (
WidgetTester tester,
) async {
const borderColor = Color(0xfff44336);
const side = BorderSide(width: 4, color: borderColor);
final theme = ThemeData(useMaterial3: false);
Widget buildApp({bool? value, bool enabled = true}) {
return MaterialApp(
theme: theme,
home: Material(
child: Center(
child: Checkbox(
value: value,
tristate: value == null,
onChanged: enabled ? (bool? newValue) {} : null,
side: WidgetStateBorderSide.resolveWith((Set<WidgetState> states) => side),
),
),
),
);
}
void expectBorder() {
expect(
tester.renderObject<RenderBox>(find.byType(Checkbox)),
paints..drrect(
color: borderColor,
outer: RRect.fromLTRBR(15, 15, 33, 33, const Radius.circular(1)),
inner: RRect.fromLTRBR(19, 19, 29, 29, Radius.zero),
),
);
}
await tester.pumpWidget(buildApp(value: false));
await tester.pumpAndSettle();
expectBorder();
await tester.pumpWidget(buildApp(value: false, enabled: false));
await tester.pumpAndSettle();
expectBorder();
await tester.pumpWidget(buildApp(value: true));
await tester.pumpAndSettle();
expectBorder();
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expectBorder();
});
testWidgets('Material3 - Checkbox WidgetStateBorderSide applies unconditionally', (
WidgetTester tester,
) async {
const borderColor = Color(0xfff44336);
const side = BorderSide(width: 4, color: borderColor);
final theme = ThemeData();
Widget buildApp({bool? value, bool enabled = true}) {
return MaterialApp(
theme: theme,
home: Material(
child: Center(
child: Checkbox(
value: value,
tristate: value == null,
onChanged: enabled ? (bool? newValue) {} : null,
side: WidgetStateBorderSide.resolveWith((Set<WidgetState> states) => side),
),
),
),
);
}
void expectBorder() {
expect(
tester.renderObject<RenderBox>(find.byType(Checkbox)),
paints..drrect(
color: borderColor,
outer: RRect.fromLTRBR(15, 15, 33, 33, const Radius.circular(2)),
inner: RRect.fromLTRBR(19, 19, 29, 29, Radius.zero),
),
);
}
await tester.pumpWidget(buildApp(value: false));
await tester.pumpAndSettle();
expectBorder();
await tester.pumpWidget(buildApp(value: false, enabled: false));
await tester.pumpAndSettle();
expectBorder();
await tester.pumpWidget(buildApp(value: true));
await tester.pumpAndSettle();
expectBorder();
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expectBorder();
});
testWidgets('disabled checkbox shows tooltip', (WidgetTester tester) async {
const longPressTooltip = 'long press tooltip';
const tapTooltip = 'tap tooltip';
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: Tooltip(message: longPressTooltip, child: Checkbox(value: true, onChanged: null)),
),
),
);
// Default tooltip shows up after long pressed.
final Finder tooltip0 = find.byType(Tooltip);
expect(find.text(longPressTooltip), findsNothing);
await tester.tap(tooltip0);
await tester.pump(const Duration(milliseconds: 10));
expect(find.text(longPressTooltip), findsNothing);
final TestGesture gestureLongPress = await tester.startGesture(tester.getCenter(tooltip0));
await tester.pump();
await tester.pump(kLongPressTimeout);
await gestureLongPress.up();
await tester.pump();
expect(find.text(longPressTooltip), findsOneWidget);
// Tooltip shows up after tapping when set triggerMode to TooltipTriggerMode.tap.
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: Tooltip(
triggerMode: TooltipTriggerMode.tap,
message: tapTooltip,
child: Checkbox(value: true, onChanged: null),
),
),
),
);
await tester.pump(const Duration(days: 1));
await tester.pumpAndSettle();
expect(find.text(tapTooltip), findsNothing);
expect(find.text(longPressTooltip), findsNothing);
final Finder tooltip1 = find.byType(Tooltip);
await tester.tap(tooltip1);
await tester.pump(const Duration(milliseconds: 10));
expect(find.text(tapTooltip), findsOneWidget);
});
testWidgets('Material3 - Checkbox has default error color when isError is set to true', (
WidgetTester tester,
) async {
final focusNode = FocusNode(debugLabel: 'Checkbox');
addTearDown(focusNode.dispose);
final themeData = ThemeData();
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
bool? value = true;
Widget buildApp({bool autoFocus = true}) {
return MaterialApp(
theme: themeData,
home: Material(
child: Center(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(
isError: true,
value: value,
onChanged: (bool? newValue) {
setState(() {
value = newValue;
});
},
autofocus: autoFocus,
focusNode: focusNode,
);
},
),
),
),
);
}
// Focused
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..circle(color: themeData.colorScheme.error.withOpacity(0.1))
..path(color: themeData.colorScheme.error)
..path(color: themeData.colorScheme.onError),
);
// Default color
await tester.pumpWidget(Container());
await tester.pumpWidget(buildApp(autoFocus: false));
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isFalse);
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..path(color: themeData.colorScheme.error)
..path(color: themeData.colorScheme.onError),
);
// Start hovering
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(tester.getCenter(find.byType(Checkbox)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..circle(color: themeData.colorScheme.error.withOpacity(0.08))
..path(color: themeData.colorScheme.error),
);
// Start pressing
final TestGesture gestureLongPress = await tester.startGesture(
tester.getCenter(find.byType(Checkbox)),
);
await tester.pump();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..circle(color: themeData.colorScheme.error.withOpacity(0.1))
..path(color: themeData.colorScheme.error),
);
await gestureLongPress.up();
await tester.pump();
});
testWidgets('Material3 - Checkbox WidgetStateBorderSide applies in error states', (
WidgetTester tester,
) async {
final focusNode = FocusNode(debugLabel: 'Checkbox');
addTearDown(focusNode.dispose);
final themeData = ThemeData();
const borderColor = Color(0xffffeb3b);
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
bool? value = false;
Widget buildApp({bool autoFocus = true}) {
return MaterialApp(
theme: themeData,
home: Material(
child: Center(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(
isError: true,
side: WidgetStateBorderSide.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.error)) {
return const BorderSide(color: borderColor, width: 4);
}
return const BorderSide(color: Colors.red, width: 2);
}),
value: value,
onChanged: (bool? newValue) {
setState(() {
value = newValue;
});
},
autofocus: autoFocus,
focusNode: focusNode,
);
},
),
),
),
);
}
void expectBorder() {
expect(
tester.renderObject<RenderBox>(find.byType(Checkbox)),
paints..drrect(
color: borderColor,
outer: RRect.fromLTRBR(15, 15, 33, 33, const Radius.circular(2)),
inner: RRect.fromLTRBR(19, 19, 29, 29, Radius.zero),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expectBorder();
// Focused
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expectBorder();
// Default color
await tester.pumpWidget(Container());
await tester.pumpWidget(buildApp(autoFocus: false));
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isFalse);
expectBorder();
// Start hovering
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(tester.getCenter(find.byType(Checkbox)));
await tester.pumpAndSettle();
expectBorder();
// Start pressing
final TestGesture gestureLongPress = await tester.startGesture(
tester.getCenter(find.byType(Checkbox)),
);
await tester.pump();
expectBorder();
await gestureLongPress.up();
await tester.pump();
});
testWidgets('Material3 - Checkbox has correct default shape', (WidgetTester tester) async {
final themeData = ThemeData();
Widget buildApp() {
return MaterialApp(
theme: themeData,
home: Material(
child: Center(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(value: false, onChanged: (bool? newValue) {});
},
),
),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
final OutlinedBorder? expectedShape = themeData.checkboxTheme.shape;
expect(tester.widget<Checkbox>(find.byType(Checkbox)).shape, expectedShape);
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..drrect(
outer: RRect.fromLTRBR(15.0, 15.0, 33.0, 33.0, const Radius.circular(2)),
inner: RRect.fromLTRBR(17.0, 17.0, 31.0, 31.0, Radius.zero),
),
);
});
testWidgets('Checkbox.adaptive shows the correct platform widget', (WidgetTester tester) async {
Widget buildApp(TargetPlatform platform) {
return MaterialApp(
theme: ThemeData(platform: platform),
home: Material(
child: Center(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox.adaptive(value: false, onChanged: (bool? newValue) {});
},
),
),
),
);
}
for (final platform in <TargetPlatform>[TargetPlatform.iOS, TargetPlatform.macOS]) {
await tester.pumpWidget(buildApp(platform));
await tester.pumpAndSettle();
expect(find.byType(CupertinoCheckbox), findsOneWidget);
}
for (final platform in <TargetPlatform>[
TargetPlatform.android,
TargetPlatform.fuchsia,
TargetPlatform.linux,
TargetPlatform.windows,
]) {
await tester.pumpWidget(buildApp(platform));
await tester.pumpAndSettle();
expect(find.byType(CupertinoCheckbox), findsNothing);
}
});
testWidgets(
'Checkbox.adaptive respects Checkbox.mouseCursor on iOS/macOS',
(WidgetTester tester) async {
Widget buildApp({MouseCursor? mouseCursor}) {
return MaterialApp(
home: Material(
child: Checkbox.adaptive(
value: true,
onChanged: (bool? newValue) {},
mouseCursor: mouseCursor,
),
),
);
}
await tester.pumpWidget(buildApp());
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
pointer: 1,
);
await gesture.addPointer(location: tester.getCenter(find.byType(CupertinoCheckbox)));
await tester.pump();
await gesture.moveTo(tester.getCenter(find.byType(CupertinoCheckbox)));
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic,
);
// Test mouse cursor can be configured.
await tester.pumpWidget(buildApp(mouseCursor: SystemMouseCursors.click));
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
SystemMouseCursors.click,
);
// Test Checkbox.adaptive can resolve a WidgetStateMouseCursor.
await tester.pumpWidget(buildApp(mouseCursor: const _SelectedGrabMouseCursor()));
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
SystemMouseCursors.grab,
);
await gesture.removePointer();
},
variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.iOS,
TargetPlatform.macOS,
}),
);
testWidgets('Material2 - Checkbox respects fillColor when it is unchecked', (
WidgetTester tester,
) async {
final theme = ThemeData(useMaterial3: false);
const activeBackgroundColor = Color(0xff123456);
const inactiveBackgroundColor = Color(0xff654321);
Widget buildApp({bool enabled = true}) {
return MaterialApp(
theme: theme,
home: Material(
child: Center(
child: Checkbox(
fillColor: WidgetStateProperty.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return activeBackgroundColor;
}
return inactiveBackgroundColor;
}),
value: false,
onChanged: enabled ? (bool? newValue) {} : null,
),
),
),
);
}
RenderBox getCheckboxRenderer() {
return tester.renderObject<RenderBox>(find.byType(Checkbox));
}
// Checkbox is unselected, so the default BorderSide appears and fillColor is checkbox's background color.
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..drrect(color: theme.unselectedWidgetColor));
expect(getCheckboxRenderer(), paints..path(color: inactiveBackgroundColor));
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..drrect(color: theme.disabledColor));
expect(getCheckboxRenderer(), paints..path(color: inactiveBackgroundColor));
});
testWidgets('Material3 - Checkbox respects fillColor when it is unchecked', (
WidgetTester tester,
) async {
final theme = ThemeData();
const activeBackgroundColor = Color(0xff123456);
const inactiveBackgroundColor = Color(0xff654321);
Widget buildApp({bool enabled = true}) {
return MaterialApp(
theme: theme,
home: Material(
child: Center(
child: Checkbox(
fillColor: WidgetStateProperty.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return activeBackgroundColor;
}
return inactiveBackgroundColor;
}),
value: false,
onChanged: enabled ? (bool? newValue) {} : null,
),
),
),
);
}
RenderBox getCheckboxRenderer() {
return tester.renderObject<RenderBox>(find.byType(Checkbox));
}
// Checkbox is unselected, so the default BorderSide appears and fillColor is checkbox's background color.
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..drrect(color: theme.colorScheme.onSurfaceVariant));
expect(getCheckboxRenderer(), paints..path(color: inactiveBackgroundColor));
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(
getCheckboxRenderer(),
paints..drrect(color: theme.colorScheme.onSurface.withOpacity(0.38)),
);
expect(getCheckboxRenderer(), paints..path(color: inactiveBackgroundColor));
});
testWidgets('Checkbox renders at zero area', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Center(
child: SizedBox.shrink(child: Scaffold(body: Checkbox(value: true, onChanged: null))),
),
),
);
expect(tester.takeException(), isNull);
});
}
class _SelectedGrabMouseCursor extends WidgetStateMouseCursor {
const _SelectedGrabMouseCursor();
@override
MouseCursor resolve(Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return SystemMouseCursors.grab;
}
return SystemMouseCursors.basic;
}
@override
String get debugDescription => '_SelectedGrabMouseCursor()';
}