flutter/packages/flutter/test/material/input_chip_test.dart
Valentin Vignal 628ab7f493
Add mouseCursor parameter to Chips (#159422)
Part of https://github.com/flutter/flutter/issues/58192

## Pre-launch Checklist

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

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

<!-- 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

---------

Co-authored-by: Victor Sanni <victorsanniay@gmail.com>
2024-12-04 04:56:01 +00:00

696 lines
24 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/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
/// Adds the basic requirements for a Chip.
Widget wrapForChip({
required Widget child,
TextDirection textDirection = TextDirection.ltr,
double textScaleFactor = 1.0,
ThemeData? theme,
}) {
return MaterialApp(
theme: theme,
home: Directionality(
textDirection: textDirection,
child: MediaQuery.withClampedTextScaling(
minScaleFactor: textScaleFactor,
maxScaleFactor: textScaleFactor,
child: Material(child: child),
),
),
);
}
Widget selectedInputChip({
Color? checkmarkColor,
bool enabled = false,
}) {
return InputChip(
label: const Text('InputChip'),
selected: true,
isEnabled: enabled,
// When [enabled] is true we also need to provide one of the chip
// callbacks, otherwise the chip would have a 'disabled'
// [MaterialState], which is not the intention.
onSelected: enabled ? (_) {} : null,
showCheckmark: true,
checkmarkColor: checkmarkColor,
);
}
Future<void> pumpCheckmarkChip(
WidgetTester tester, {
required Widget chip,
Color? themeColor,
ThemeData? theme,
}) async {
await tester.pumpWidget(
wrapForChip(
theme: theme,
child: Builder(
builder: (BuildContext context) {
final ChipThemeData chipTheme = ChipTheme.of(context);
return ChipTheme(
data: themeColor == null ? chipTheme : chipTheme.copyWith(
checkmarkColor: themeColor,
),
child: chip,
);
},
),
),
);
}
void expectCheckmarkColor(Finder finder, Color color) {
expect(
finder,
paints
// Physical model layer path
..path()
// The first layer that is painted is the selection overlay. We do not care
// how it is painted but it has to be added it to this pattern so that the
// check mark can be checked next.
..rrect()
// The second layer that is painted is the check mark.
..path(color: color),
);
}
RenderBox getMaterialBox(WidgetTester tester) {
return tester.firstRenderObject<RenderBox>(
find.descendant(
of: find.byType(InputChip),
matching: find.byType(CustomPaint),
),
);
}
Material getMaterial(WidgetTester tester) {
return tester.widget<Material>(
find.descendant(
of: find.byType(InputChip),
matching: find.byType(Material),
),
);
}
IconThemeData getIconData(WidgetTester tester) {
final IconTheme iconTheme = tester.firstWidget(
find.descendant(
of: find.byType(RawChip),
matching: find.byType(IconTheme),
),
);
return iconTheme.data;
}
void checkChipMaterialClipBehavior(WidgetTester tester, Clip clipBehavior) {
final Iterable<Material> materials = tester.widgetList<Material>(find.byType(Material));
// There should be two Material widgets, first Material is from the "_wrapForChip" and
// last Material is from the "RawChip".
expect(materials.length, 2);
// The last Material from `RawChip` should have the clip behavior.
expect(materials.last.clipBehavior, clipBehavior);
}
// Finds any container of a tooltip.
Finder findTooltipContainer(String tooltipText) {
return find.ancestor(
of: find.text(tooltipText),
matching: find.byType(Container),
);
}
void main() {
testWidgets('InputChip.color resolves material states', (WidgetTester tester) async {
const Color disabledSelectedColor = Color(0xffffff00);
const Color disabledColor = Color(0xff00ff00);
const Color backgroundColor = Color(0xff0000ff);
const Color selectedColor = Color(0xffff0000);
Widget buildApp({ required bool enabled, required bool selected }) {
return wrapForChip(
child: InputChip(
onSelected: enabled ? (bool value) { } : null,
selected: selected,
color: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled) && states.contains(MaterialState.selected)) {
return disabledSelectedColor;
}
if (states.contains(MaterialState.disabled)) {
return disabledColor;
}
if (states.contains(MaterialState.selected)) {
return selectedColor;
}
return backgroundColor;
}),
label: const Text('InputChip'),
),
);
}
// Test enabled chip.
await tester.pumpWidget(buildApp(enabled: true, selected: false));
// Enabled chip should have the provided backgroundColor.
expect(getMaterialBox(tester), paints..rrect(color: backgroundColor));
// Test disabled chip.
await tester.pumpWidget(buildApp(enabled: false, selected: false));
await tester.pumpAndSettle();
// Disabled chip should have the provided disabledColor.
expect(getMaterialBox(tester), paints..rrect(color: disabledColor));
// Test enabled & selected chip.
await tester.pumpWidget(buildApp(enabled: true, selected: true));
await tester.pumpAndSettle();
// Enabled & selected chip should have the provided selectedColor.
expect(getMaterialBox(tester), paints..rrect(color: selectedColor));
// Test disabled & selected chip.
await tester.pumpWidget(buildApp(enabled: false, selected: true));
await tester.pumpAndSettle();
// Disabled & selected chip should have the provided disabledSelectedColor.
expect(getMaterialBox(tester), paints..rrect(color: disabledSelectedColor));
});
testWidgets('InputChip uses provided state color properties', (WidgetTester tester) async {
const Color disabledColor = Color(0xff00ff00);
const Color backgroundColor = Color(0xff0000ff);
const Color selectedColor = Color(0xffff0000);
Widget buildApp({ required bool enabled, required bool selected }) {
return wrapForChip(
child: InputChip(
onSelected: enabled ? (bool value) { } : null,
selected: selected,
disabledColor: disabledColor,
backgroundColor: backgroundColor,
selectedColor: selectedColor,
label: const Text('InputChip'),
),
);
}
// Test enabled chip.
await tester.pumpWidget(buildApp(enabled: true, selected: false));
// Enabled chip should have the provided backgroundColor.
expect(getMaterialBox(tester), paints..rrect(color: backgroundColor));
// Test disabled chip.
await tester.pumpWidget(buildApp(enabled: false, selected: false));
await tester.pumpAndSettle();
// Disabled chip should have the provided disabledColor.
expect(getMaterialBox(tester), paints..rrect(color: disabledColor));
// Test enabled & selected chip.
await tester.pumpWidget(buildApp(enabled: true, selected: true));
await tester.pumpAndSettle();
// Enabled & selected chip should have the provided selectedColor.
expect(getMaterialBox(tester), paints..rrect(color: selectedColor));
});
testWidgets('InputChip can be tapped', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: InputChip(
label: Text('input chip'),
),
),
),
);
await tester.tap(find.byType(InputChip));
expect(tester.takeException(), null);
});
testWidgets('loses focus when disabled', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'InputChip');
await tester.pumpWidget(
wrapForChip(
child: InputChip(
focusNode: focusNode,
autofocus: true,
shape: const RoundedRectangleBorder(),
avatar: const CircleAvatar(child: Text('A')),
label: const Text('Chip A'),
onPressed: () { },
),
),
);
await tester.pump();
expect(focusNode.hasPrimaryFocus, isTrue);
await tester.pumpWidget(
wrapForChip(
child: InputChip(
focusNode: focusNode,
autofocus: true,
shape: const RoundedRectangleBorder(),
avatar: const CircleAvatar(child: Text('A')),
label: const Text('Chip A'),
),
),
);
await tester.pump();
expect(focusNode.hasPrimaryFocus, isFalse);
focusNode.dispose();
});
testWidgets('cannot be traversed to when disabled', (WidgetTester tester) async {
final FocusNode focusNode1 = FocusNode(debugLabel: 'InputChip 1');
final FocusNode focusNode2 = FocusNode(debugLabel: 'InputChip 2');
await tester.pumpWidget(
wrapForChip(
child: FocusScope(
child: Column(
children: <Widget>[
InputChip(
focusNode: focusNode1,
autofocus: true,
label: const Text('Chip A'),
onPressed: () { },
),
InputChip(
focusNode: focusNode2,
autofocus: true,
label: const Text('Chip B'),
),
],
),
),
),
);
await tester.pump();
expect(focusNode1.hasPrimaryFocus, isTrue);
expect(focusNode2.hasPrimaryFocus, isFalse);
expect(focusNode1.nextFocus(), isFalse);
await tester.pump();
expect(focusNode1.hasPrimaryFocus, isTrue);
expect(focusNode2.hasPrimaryFocus, isFalse);
focusNode1.dispose();
focusNode2.dispose();
});
testWidgets('Material2 - Input chip disabled check mark color is determined by platform brightness when light', (WidgetTester tester) async {
await pumpCheckmarkChip(
tester,
chip: selectedInputChip(),
theme: ThemeData(useMaterial3: false),
);
expectCheckmarkColor(find.byType(InputChip), Colors.black.withAlpha(0xde));
});
testWidgets('Material3 - Input chip disabled check mark color is determined by platform brightness when light', (WidgetTester tester) async {
final ThemeData theme = ThemeData();
await pumpCheckmarkChip(tester, chip: selectedInputChip(), theme: theme);
expectCheckmarkColor(find.byType(InputChip), theme.colorScheme.onSurface);
});
testWidgets('Material2 - Input chip disabled check mark color is determined by platform brightness when dark', (WidgetTester tester) async {
await pumpCheckmarkChip(
tester,
chip: selectedInputChip(),
theme: ThemeData.dark(useMaterial3: false),
);
expectCheckmarkColor(find.byType(InputChip), Colors.white.withAlpha(0xde));
});
testWidgets('Material3 - Input chip disabled check mark color is determined by platform brightness when dark', (WidgetTester tester) async {
final ThemeData theme = ThemeData.dark();
await pumpCheckmarkChip(
tester,
chip: selectedInputChip(),
theme: theme,
);
expectCheckmarkColor(find.byType(InputChip), theme.colorScheme.onSurface);
});
testWidgets('Input chip check mark color can be set by the chip theme', (WidgetTester tester) async {
await pumpCheckmarkChip(
tester,
chip: selectedInputChip(),
themeColor: const Color(0xff00ff00),
);
expectCheckmarkColor(find.byType(InputChip), const Color(0xff00ff00));
});
testWidgets('Input chip check mark color can be set by the chip constructor', (WidgetTester tester) async {
await pumpCheckmarkChip(
tester,
chip: selectedInputChip(checkmarkColor: const Color(0xff00ff00)),
);
expectCheckmarkColor(find.byType(InputChip), const Color(0xff00ff00));
});
testWidgets('Input chip check mark color is set by chip constructor even when a theme color is specified', (WidgetTester tester) async {
await pumpCheckmarkChip(
tester,
chip: selectedInputChip(checkmarkColor: const Color(0xffff0000)),
themeColor: const Color(0xff00ff00),
);
expectCheckmarkColor(find.byType(InputChip), const Color(0xffff0000));
});
testWidgets('InputChip clipBehavior properly passes through to the Material', (WidgetTester tester) async {
const Text label = Text('label');
await tester.pumpWidget(wrapForChip(child: const InputChip(label: label)));
checkChipMaterialClipBehavior(tester, Clip.none);
await tester.pumpWidget(wrapForChip(child: const InputChip(label: label, clipBehavior: Clip.antiAlias)));
checkChipMaterialClipBehavior(tester, Clip.antiAlias);
});
testWidgets('Material3 - Input chip has correct selected color when enabled', (WidgetTester tester) async {
final ThemeData theme = ThemeData();
await pumpCheckmarkChip(
tester,
chip: selectedInputChip(enabled: true),
theme: theme,
);
final RenderBox materialBox = getMaterialBox(tester);
expect(materialBox, paints..rrect(color: theme.colorScheme.secondaryContainer));
});
testWidgets('Material3 - Input chip has correct selected color when disabled', (WidgetTester tester) async {
final ThemeData theme = ThemeData();
await pumpCheckmarkChip(
tester,
chip: selectedInputChip(),
theme: theme,
);
final RenderBox materialBox = getMaterialBox(tester);
expect(materialBox, paints..path(color: theme.colorScheme.onSurface));
});
testWidgets('InputChip uses provided iconTheme', (WidgetTester tester) async {
final ThemeData theme = ThemeData();
Widget buildChip({ IconThemeData? iconTheme }) {
return MaterialApp(
theme: theme,
home: Material(
child: InputChip(
iconTheme: iconTheme,
avatar: const Icon(Icons.add),
label: const Text('Test'),
),
),
);
}
// Test default icon theme.
await tester.pumpWidget(buildChip());
expect(getIconData(tester).color, theme.colorScheme.onSurfaceVariant);
// Test provided icon theme.
await tester.pumpWidget(buildChip(iconTheme: const IconThemeData(color: Color(0xff00ff00))));
expect(getIconData(tester).color, const Color(0xff00ff00));
});
testWidgets('Delete button is visible on disabled InputChip', (WidgetTester tester) async {
await tester.pumpWidget(
wrapForChip(
child: InputChip(
isEnabled: false,
label: const Text('Label'),
onDeleted: () { },
)
),
);
// Delete button should be visible.
await expectLater(find.byType(RawChip), matchesGoldenFile('input_chip.disabled.delete_button.png'));
});
testWidgets('Delete button tooltip is not shown on disabled InputChip', (WidgetTester tester) async {
Widget buildChip({ bool enabled = true }) {
return wrapForChip(
child: InputChip(
isEnabled: enabled,
label: const Text('Label'),
onDeleted: () { },
)
);
}
// Test enabled chip.
await tester.pumpWidget(buildChip());
final Offset deleteButtonLocation = tester.getCenter(find.byType(Icon));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.moveTo(deleteButtonLocation);
await tester.pump();
// Delete button tooltip should be visible.
expect(findTooltipContainer('Delete'), findsOneWidget);
// Test disabled chip.
await tester.pumpWidget(buildChip(enabled: false));
await tester.pump();
// Delete button tooltip should not be visible.
expect(findTooltipContainer('Delete'), findsNothing);
});
testWidgets('InputChip avatar layout constraints can be customized', (WidgetTester tester) async {
const double border = 1.0;
const double iconSize = 18.0;
const double labelPadding = 8.0;
const double padding = 8.0;
const Size labelSize = Size(100, 100);
Widget buildChip({BoxConstraints? avatarBoxConstraints}) {
return wrapForChip(
child: Center(
child: InputChip(
avatarBoxConstraints: avatarBoxConstraints,
avatar: const Icon(Icons.favorite),
label: Container(
width: labelSize.width,
height: labelSize.width,
color: const Color(0xFFFF0000),
),
),
),
);
}
// Test default avatar layout constraints.
await tester.pumpWidget(buildChip());
expect(tester.getSize(find.byType(InputChip)).width, equals(234.0));
expect(tester.getSize(find.byType(InputChip)).height, equals(118.0));
// Calculate the distance between avatar and chip edges.
Offset chipTopLeft = tester.getTopLeft(find.byWidget(getMaterial(tester)));
final Offset avatarCenter = tester.getCenter(find.byIcon(Icons.favorite));
expect(chipTopLeft.dx, avatarCenter.dx - (labelSize.width / 2) - padding - border);
expect(chipTopLeft.dy, avatarCenter.dy - (labelSize.width / 2) - padding - border);
// Calculate the distance between avatar and label.
Offset labelTopLeft = tester.getTopLeft(find.byType(Container));
expect(labelTopLeft.dx, avatarCenter.dx + (labelSize.width / 2) + labelPadding);
// Test custom avatar layout constraints.
await tester.pumpWidget(buildChip(avatarBoxConstraints: const BoxConstraints.tightForFinite()));
await tester.pump();
expect(tester.getSize(find.byType(InputChip)).width, equals(152.0));
expect(tester.getSize(find.byType(InputChip)).height, equals(118.0));
// Calculate the distance between avatar and chip edges.
chipTopLeft = tester.getTopLeft(find.byWidget(getMaterial(tester)));
expect(chipTopLeft.dx, avatarCenter.dx - (iconSize / 2) - padding - border);
expect(chipTopLeft.dy, avatarCenter.dy - (labelSize.width / 2) - padding - border);
// Calculate the distance between avatar and label.
labelTopLeft = tester.getTopLeft(find.byType(Container));
expect(labelTopLeft.dx, avatarCenter.dx + (iconSize / 2) + labelPadding);
});
testWidgets('InputChip delete icon layout constraints can be customized', (WidgetTester tester) async {
const double border = 1.0;
const double iconSize = 18.0;
const double labelPadding = 8.0;
const double padding = 8.0;
const Size labelSize = Size(100, 100);
Widget buildChip({BoxConstraints? deleteIconBoxConstraints}) {
return wrapForChip(
child: Center(
child: InputChip(
deleteIconBoxConstraints: deleteIconBoxConstraints,
onDeleted: () { },
label: Container(
width: labelSize.width,
height: labelSize.width,
color: const Color(0xFFFF0000),
),
),
),
);
}
// Test default delete icon layout constraints.
await tester.pumpWidget(buildChip());
expect(tester.getSize(find.byType(InputChip)).width, equals(234.0));
expect(tester.getSize(find.byType(InputChip)).height, equals(118.0));
// Calculate the distance between delete icon and chip edges.
Offset chipTopRight = tester.getTopRight(find.byWidget(getMaterial(tester)));
final Offset deleteIconCenter = tester.getCenter(find.byIcon(Icons.clear));
expect(chipTopRight.dx, deleteIconCenter.dx + (labelSize.width / 2) + padding + border);
expect(chipTopRight.dy, deleteIconCenter.dy - (labelSize.width / 2) - padding - border);
// Calculate the distance between delete icon and label.
Offset labelTopRight = tester.getTopRight(find.byType(Container));
expect(labelTopRight.dx, deleteIconCenter.dx - (labelSize.width / 2) - labelPadding);
// Test custom avatar layout constraints.
await tester.pumpWidget(buildChip(
deleteIconBoxConstraints: const BoxConstraints.tightForFinite(),
));
await tester.pump();
expect(tester.getSize(find.byType(InputChip)).width, equals(152.0));
expect(tester.getSize(find.byType(InputChip)).height, equals(118.0));
// Calculate the distance between delete icon and chip edges.
chipTopRight = tester.getTopRight(find.byWidget(getMaterial(tester)));
expect(chipTopRight.dx, deleteIconCenter.dx + (iconSize / 2) + padding + border);
expect(chipTopRight.dy, deleteIconCenter.dy - (labelSize.width / 2) - padding - border);
// Calculate the distance between delete icon and label.
labelTopRight = tester.getTopRight(find.byType(Container));
expect(labelTopRight.dx, deleteIconCenter.dx - (iconSize / 2) - labelPadding);
});
testWidgets('InputChip.chipAnimationStyle is passed to RawChip', (WidgetTester tester) async {
final ChipAnimationStyle chipAnimationStyle = ChipAnimationStyle(
enableAnimation: AnimationStyle(duration: Durations.short2),
selectAnimation: AnimationStyle.noAnimation,
);
await tester.pumpWidget(wrapForChip(
child: Center(
child: InputChip(
chipAnimationStyle: chipAnimationStyle,
label: const Text('InputChip'),
),
),
));
expect(tester.widget<RawChip>(find.byType(RawChip)).chipAnimationStyle, chipAnimationStyle);
});
testWidgets('InputChip mouse cursor behavior', (WidgetTester tester) async {
const SystemMouseCursor customCursor = SystemMouseCursors.grab;
await tester.pumpWidget(wrapForChip(
child: const Center(
child: InputChip(
mouseCursor: customCursor,
label: Text('Chip'),
),
),
));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: const Offset(10, 10));
await tester.pump();
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
SystemMouseCursors.basic,
);
final Offset chip = tester.getCenter(find.text('Chip'));
await gesture.moveTo(chip);
await tester.pump();
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
customCursor,
);
});
testWidgets('Mouse cursor resolves in focused/unfocused/disabled states', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
final FocusNode focusNode = FocusNode(debugLabel: 'Chip');
addTearDown(focusNode.dispose);
Widget buildChip({ required bool enabled }) {
return wrapForChip(
child: Center(
child: InputChip(
mouseCursor: const WidgetStateMouseCursor.fromMap(
<WidgetStatesConstraint, MouseCursor>{
WidgetState.disabled: SystemMouseCursors.forbidden,
WidgetState.focused: SystemMouseCursors.grab,
WidgetState.selected: SystemMouseCursors.click,
WidgetState.any: SystemMouseCursors.basic,
},
),
focusNode: focusNode,
label: const Text('Chip'),
onSelected: enabled ? (bool value) {} : null,
),
),
);
}
// Unfocused case.
await tester.pumpWidget(buildChip(enabled: true));
final TestGesture gesture1 = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
addTearDown(gesture1.removePointer);
await gesture1.addPointer(location: tester.getCenter(find.text('Chip')));
await tester.pump();
await gesture1.moveTo(tester.getCenter(find.text('Chip')));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
// Focused case.
focusNode.requestFocus();
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grab);
// Disabled case.
await tester.pumpWidget(buildChip(enabled: false));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden);
});
}