// 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(['reduced-test-set']) library; import 'package:flutter/foundation.dart' show kIsWeb; 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' // [WidgetState], which is not the intention. onSelected: enabled ? (_) {} : null, showCheckmark: true, checkmarkColor: checkmarkColor, ); } Future 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( find.descendant(of: find.byType(InputChip), matching: find.byType(CustomPaint)), ); } Material getMaterial(WidgetTester tester) { return tester.widget( 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 materials = tester.widgetList(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 disabledSelectedColor = Color(0xffffff00); const disabledColor = Color(0xff00ff00); const backgroundColor = Color(0xff0000ff); const selectedColor = Color(0xffff0000); Widget buildApp({required bool enabled, required bool selected}) { return wrapForChip( child: InputChip( onSelected: enabled ? (bool value) {} : null, selected: selected, color: WidgetStateProperty.resolveWith((Set states) { if (states.contains(WidgetState.disabled) && states.contains(WidgetState.selected)) { return disabledSelectedColor; } if (states.contains(WidgetState.disabled)) { return disabledColor; } if (states.contains(WidgetState.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 disabledColor = Color(0xff00ff00); const backgroundColor = Color(0xff0000ff); const 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(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 focusNode1 = FocusNode(debugLabel: 'InputChip 1'); final focusNode2 = FocusNode(debugLabel: 'InputChip 2'); await tester.pumpWidget( wrapForChip( child: FocusScope( child: Column( children: [ 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 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 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 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 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 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 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 border = 1.0; const iconSize = 18.0; const labelPadding = 8.0; const padding = 8.0; const 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 border = 1.0; const iconSize = 18.0; const labelPadding = 8.0; const padding = 8.0; const 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( enableAnimation: const AnimationStyle(duration: Durations.short2), selectAnimation: AnimationStyle.noAnimation, ); await tester.pumpWidget( wrapForChip( child: Center( child: InputChip(chipAnimationStyle: chipAnimationStyle, label: const Text('InputChip')), ), ), ); expect(tester.widget(find.byType(RawChip)).chipAnimationStyle, chipAnimationStyle); }); testWidgets('InputChip has expected default mouse cursor on hover', (WidgetTester tester) async { await tester.pumpWidget( wrapForChip( child: Center( child: InputChip(label: const Text('Chip'), onPressed: () {}), ), ), ); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(location: const Offset(10, 10)); 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), kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic, ); }); 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(debugLabel: 'Chip'); addTearDown(focusNode.dispose); Widget buildChip({required bool enabled}) { return wrapForChip( child: Center( child: InputChip( mouseCursor: const WidgetStateMouseCursor.fromMap({ 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, ); }); testWidgets('InputChip does not crash at zero area', (WidgetTester tester) async { Future testChip(Widget chip) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center(child: SizedBox.shrink(child: chip)), ), ), ); expect(tester.getSize(find.byType(InputChip)), Size.zero); } await testChip(const InputChip(label: Text('X'))); await testChip( const InputChip( label: Text('X'), avatar: CircleAvatar(child: Text('A')), ), ); await testChip(InputChip(label: const Text('X'), onDeleted: () {})); }); }