| // 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 'dart:async'; |
| import 'dart:ui'; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| const String hintText = 'hint'; |
| const String inputText = 'text'; |
| const String labelText = 'label'; |
| const String errorText = 'error'; |
| const String helperText = 'helper'; |
| const String counterText = 'counter'; |
| |
| const Key customLabelKey = Key('label'); |
| const Widget customLabel = Text.rich( |
| key: customLabelKey, |
| TextSpan( |
| children: <InlineSpan>[ |
| TextSpan(text: 'label'), |
| WidgetSpan( |
| child: Text('*', style: TextStyle(color: Colors.red)), |
| ), |
| ], |
| ), |
| ); |
| |
| const String twoLines = 'line1\nline2'; |
| const String threeLines = 'line1\nline2\nline3'; |
| |
| Widget buildInputDecorator({ |
| InputDecoration decoration = const InputDecoration(), |
| ThemeData? theme, |
| InputDecorationTheme? inputDecorationTheme, |
| TextDirection textDirection = TextDirection.ltr, |
| bool expands = false, |
| bool isEmpty = false, |
| bool isFocused = false, |
| bool isHovering = false, |
| bool useIntrinsicWidth = false, |
| TextStyle? baseStyle, |
| TextAlignVertical? textAlignVertical, |
| VisualDensity? visualDensity, |
| Widget child = const Text( |
| inputText, |
| // Use a text style compliant with M3 specification (which is bodyLarge for text fields). |
| style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.w400, letterSpacing: 0.5, height: 1.50) |
| ), |
| }) { |
| Widget widget = InputDecorator( |
| expands: expands, |
| decoration: decoration, |
| isEmpty: isEmpty, |
| isFocused: isFocused, |
| isHovering: isHovering, |
| baseStyle: baseStyle, |
| textAlignVertical: textAlignVertical, |
| child: child, |
| ); |
| |
| if (useIntrinsicWidth) { |
| widget = IntrinsicWidth(child: widget); |
| } |
| |
| return MaterialApp( |
| home: Material( |
| child: Builder( |
| builder: (BuildContext context) { |
| return Theme( |
| data: (theme ?? Theme.of(context)).copyWith( |
| inputDecorationTheme: inputDecorationTheme, |
| visualDensity: visualDensity, |
| ), |
| child: Align( |
| alignment: Alignment.topLeft, |
| child: Directionality( |
| textDirection: textDirection, |
| child: widget, |
| ), |
| ), |
| ); |
| }, |
| ), |
| ), |
| ); |
| } |
| |
| Finder findBorderPainter() { |
| return find.descendant( |
| of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_BorderContainer'), |
| matching: find.byWidgetPredicate((Widget w) => w is CustomPaint), |
| ); |
| } |
| |
| double getBorderBottom(WidgetTester tester) { |
| final RenderBox box = InputDecorator.containerOf(tester.element(findBorderPainter()))!; |
| return box.size.height; |
| } |
| |
| Finder findLabel() { |
| return find.descendant( |
| of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_Shaker'), |
| matching: find.byWidgetPredicate((Widget w) => w is Text), |
| ); |
| } |
| |
| Rect getLabelRect(WidgetTester tester) { |
| return tester.getRect(findLabel()); |
| } |
| |
| Offset getLabelCenter(WidgetTester tester) { |
| return getLabelRect(tester).center; |
| } |
| |
| TextStyle getLabelStyle(WidgetTester tester) { |
| return tester.firstWidget<AnimatedDefaultTextStyle>( |
| find.ancestor( |
| of: findLabel(), |
| matching: find.byType(AnimatedDefaultTextStyle), |
| ), |
| ).style; |
| } |
| |
| Finder findCustomLabel() { |
| return find.byKey(customLabelKey); |
| } |
| |
| Rect getCustomLabelRect(WidgetTester tester) { |
| return tester.getRect(findCustomLabel()); |
| } |
| |
| Offset getCustomLabelCenter(WidgetTester tester) { |
| return getCustomLabelRect(tester).center; |
| } |
| |
| Finder findInputText() { |
| return find.text(inputText); |
| } |
| |
| Rect getInputRect(WidgetTester tester) { |
| return tester.getRect(findInputText()); |
| } |
| |
| Offset getInputCenter(WidgetTester tester) { |
| return getInputRect(tester).center; |
| } |
| |
| Finder findHint() { |
| return find.text(hintText); |
| } |
| |
| Rect getHintRect(WidgetTester tester) { |
| return tester.getRect(findHint()); |
| } |
| |
| Offset getHintCenter(WidgetTester tester) { |
| return getHintRect(tester).center; |
| } |
| |
| double getHintOpacity(WidgetTester tester) { |
| return getOpacity(tester, hintText); |
| } |
| |
| Finder findHelper() { |
| return find.text(helperText); |
| } |
| |
| Rect getHelperRect(WidgetTester tester) { |
| return tester.getRect(findHelper()); |
| } |
| |
| TextStyle getHelperStyle(WidgetTester tester) { |
| return tester.widget<RichText>( |
| find.descendant(of: findHelper(), matching: find.byType(RichText)), |
| ).text.style!; |
| } |
| |
| Finder findError() { |
| return find.text(errorText); |
| } |
| |
| Rect getErrorRect(WidgetTester tester) { |
| return tester.getRect(findError()); |
| } |
| |
| TextStyle getErrorStyle(WidgetTester tester) { |
| return tester.widget<RichText>( |
| find.descendant(of: findError(), matching: find.byType(RichText)), |
| ).text.style!; |
| } |
| |
| Finder findCounter() { |
| return find.text(counterText); |
| } |
| |
| Rect getCounterRect(WidgetTester tester) { |
| return tester.getRect(findCounter()); |
| } |
| |
| TextStyle getCounterStyle(WidgetTester tester) { |
| return tester.widget<RichText>( |
| find.descendant(of: findCounter(), matching: find.byType(RichText)), |
| ).text.style!; |
| } |
| |
| Finder findDecorator() { |
| return find.byType(InputDecorator); |
| } |
| |
| Rect getDecoratorRect(WidgetTester tester) { |
| return tester.getRect(findDecorator()); |
| } |
| |
| Offset getDecoratorCenter(WidgetTester tester) { |
| return getDecoratorRect(tester).center; |
| } |
| |
| InputBorder? getBorder(WidgetTester tester) { |
| if (!tester.any(findBorderPainter())) { |
| return null; |
| } |
| final CustomPaint customPaint = tester.widget(findBorderPainter()); |
| final dynamic/*_InputBorderPainter*/ inputBorderPainter = customPaint.foregroundPainter; |
| // ignore: avoid_dynamic_calls |
| final dynamic/*_InputBorderTween*/ inputBorderTween = inputBorderPainter.border; |
| // ignore: avoid_dynamic_calls |
| final Animation<double> animation = inputBorderPainter.borderAnimation as Animation<double>; |
| // ignore: avoid_dynamic_calls |
| final InputBorder border = inputBorderTween.evaluate(animation) as InputBorder; |
| return border; |
| } |
| |
| BorderSide? getBorderSide(WidgetTester tester) { |
| return getBorder(tester)!.borderSide; |
| } |
| |
| BorderRadius? getBorderRadius(WidgetTester tester) { |
| final InputBorder border = getBorder(tester)!; |
| if (border is UnderlineInputBorder) { |
| return border.borderRadius; |
| } |
| return null; |
| } |
| |
| double getBorderWeight(WidgetTester tester) => getBorderSide(tester)!.width; |
| |
| Color getBorderColor(WidgetTester tester) => getBorderSide(tester)!.color; |
| |
| Color getContainerColor(WidgetTester tester) { |
| final CustomPaint customPaint = tester.widget(findBorderPainter()); |
| final dynamic/*_InputBorderPainter*/ inputBorderPainter = customPaint.foregroundPainter; |
| // ignore: avoid_dynamic_calls |
| return inputBorderPainter.blendedColor as Color; |
| } |
| |
| double getOpacity(WidgetTester tester, String textValue) { |
| final FadeTransition opacityWidget = tester.widget<FadeTransition>( |
| find.ancestor( |
| of: find.text(textValue), |
| matching: find.byType(FadeTransition), |
| ).first, |
| ); |
| return opacityWidget.opacity.value; |
| } |
| |
| TextStyle? getIconStyle(WidgetTester tester, IconData icon) { |
| final RichText iconRichText = tester.widget<RichText>( |
| find.descendant(of: find.byIcon(icon), matching: find.byType(RichText)), |
| ); |
| return iconRichText.text.style; |
| } |
| |
| void main() { |
| // TODO(bleroux): migrate all M2 tests to M3. |
| // See https://github.com/flutter/flutter/issues/139076 |
| // Work in progress. |
| |
| group('Material3 - InputDecoration labelText layout', () { |
| testWidgets('The label appears above input', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| labelText: labelText, |
| ), |
| ), |
| ); |
| |
| // Overall height for this InputDecorator is 56dp on mobile: |
| // 8 - top padding |
| // 12 - floating label (font size = 16 * 0.75, line height is forced to 1.0) |
| // 4 - gap between label and input (this is not part of the M3 spec) |
| // 24 - input text (font size = 16, line height = 1.5) |
| // 8 - bottom padding |
| // TODO(bleroux): fix input decorator to not rely on a 4 pixels gap between the label and the input, |
| // this gap is not compliant with the M3 spec (M3 spec uses line height for this purpose). |
| expect(getDecoratorRect(tester).size, const Size(800.0, 56.0)); |
| expect(getLabelRect(tester).top, 8.0); |
| expect(getLabelRect(tester).bottom, 20.0); |
| expect(getInputRect(tester).top, 24.0); |
| expect(getInputRect(tester).bottom, 48.0); |
| }); |
| |
| testWidgets('The label appears within the input when there is no text content', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isEmpty: true, |
| decoration: const InputDecoration( |
| labelText: labelText, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).size, const Size(800.0, 56.0)); |
| // Label line height is forced to 1.0 and font size is 16.0, |
| // the label should be vertically centered (20 pixels above and below). |
| expect(getLabelRect(tester).top, 20.0); |
| expect(getLabelRect(tester).bottom, 36.0); |
| // From the M3 specification, centering the label is right, but setting the line height to 1.0 is not |
| // compliant (the expected text style is bodyLarge which font size is 16.0 and its line height 1.5). |
| // TODO(bleroux): fix input decorator to not rely on forcing the label text line height to 1.0. |
| }); |
| |
| testWidgets( |
| 'The label appears above the input when there is no content and floatingLabelBehavior is always', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isEmpty: true, |
| decoration: const InputDecoration( |
| labelText: labelText, |
| floatingLabelBehavior: FloatingLabelBehavior.always, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).size, const Size(800.0, 56.0)); |
| expect(getLabelRect(tester).top, 8.0); |
| expect(getLabelRect(tester).bottom, 20.0); |
| }, |
| ); |
| |
| testWidgets( |
| 'The label appears within the input text when there is content and floatingLabelBehavior is never', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| labelText: labelText, |
| floatingLabelBehavior: FloatingLabelBehavior.never, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).size, const Size(800.0, 56.0)); |
| expect(getLabelRect(tester).top, 20.0); |
| expect(getLabelRect(tester).bottom, 36.0); |
| }, |
| ); |
| |
| testWidgets('Floating label animation duration and curve', (WidgetTester tester) async { |
| Future<void> pumpInputDecorator({ |
| required bool isFocused, |
| }) async { |
| return tester.pumpWidget( |
| buildInputDecorator( |
| isEmpty: true, |
| isFocused: isFocused, |
| decoration: const InputDecoration( |
| labelText: labelText, |
| floatingLabelBehavior: FloatingLabelBehavior.auto, |
| ), |
| ), |
| ); |
| } |
| await pumpInputDecorator(isFocused: false); |
| expect(getLabelRect(tester).top, 20.0); |
| |
| // The label animates upwards and scales down. |
| // The animation duration is 167ms and the curve is fastOutSlowIn. |
| await pumpInputDecorator(isFocused: true); |
| await tester.pump(const Duration(milliseconds: 42)); |
| expect(getLabelRect(tester).top, closeTo(17.09, 0.5)); |
| await tester.pump(const Duration(milliseconds: 42)); |
| expect(getLabelRect(tester).top, closeTo(10.66, 0.5)); |
| await tester.pump(const Duration(milliseconds: 42)); |
| expect(getLabelRect(tester).top, closeTo(8.47, 0.5)); |
| await tester.pump(const Duration(milliseconds: 41)); |
| expect(getLabelRect(tester).top, 8.0); |
| |
| // If the animation changes direction without first reaching the |
| // AnimationStatus.completed or AnimationStatus.dismissed status, |
| // the CurvedAnimation stays on the same curve in the opposite direction. |
| // The pumpAndSettle is used to prevent this behavior. |
| await tester.pumpAndSettle(); |
| |
| // The label animates downwards and scales up. |
| // The animation duration is 167ms and the curve is fastOutSlowIn. |
| await pumpInputDecorator(isFocused: false); |
| await tester.pump(const Duration(milliseconds: 42)); |
| expect(getLabelRect(tester).top, closeTo(10.90, 0.5)); |
| await tester.pump(const Duration(milliseconds: 42)); |
| expect(getLabelRect(tester).top, closeTo(17.34, 0.5)); |
| await tester.pump(const Duration(milliseconds: 42)); |
| expect(getLabelRect(tester).top, closeTo(19.69, 0.5)); |
| await tester.pump(const Duration(milliseconds: 41)); |
| expect(getLabelRect(tester).top, 20.0); |
| }); |
| }); |
| |
| group('Material3 - InputDecoration label layout', () { |
| testWidgets('The label appears above input', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| label: customLabel, |
| ), |
| ), |
| ); |
| |
| // Overall height for this InputDecorator is 56dp on mobile: |
| // 8 - top padding |
| // 12 - floating label (font size = 16 * 0.75, line height is forced to 1.0) |
| // 4 - gap between label and input (this is not part of the M3 spec) |
| // 24 - input text (font size = 16, line height = 1.5) |
| // 8 - bottom padding |
| expect(getDecoratorRect(tester).size, const Size(800.0, 56.0)); |
| expect(getCustomLabelRect(tester).top, 8.0); |
| expect(getCustomLabelRect(tester).bottom, 20.0); |
| expect(getInputRect(tester).top, 24.0); |
| expect(getInputRect(tester).bottom, 48.0); |
| }); |
| |
| testWidgets('The label appears within the input when there is no text content', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isEmpty: true, |
| decoration: const InputDecoration( |
| label: customLabel, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).size, const Size(800.0, 56.0)); |
| // Label line height is forced to 1.0 and font size is 16.0, |
| // the label should be vertically centered (20 pixels above and below). |
| expect(getCustomLabelRect(tester).top, 20.0); |
| expect(getCustomLabelRect(tester).bottom, 36.0); |
| }); |
| |
| testWidgets( |
| 'The label appears above the input when there is no content and floatingLabelBehavior is always', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isEmpty: true, |
| decoration: const InputDecoration( |
| label: customLabel, |
| floatingLabelBehavior: FloatingLabelBehavior.always, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).size, const Size(800.0, 56.0)); |
| // 8 - top padding |
| // 12 - floating label height (font size = 16 * 0.75, line height is forced to 1.0) |
| expect(getCustomLabelRect(tester).top, 8.0); |
| expect(getCustomLabelRect(tester).bottom, 20.0); |
| }, |
| ); |
| |
| testWidgets( |
| 'The label appears within the input text when there is content and floatingLabelBehavior is never', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| label: customLabel, |
| floatingLabelBehavior: FloatingLabelBehavior.never, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).size, const Size(800.0, 56.0)); |
| // Label line height is forced to 1.0 and font size is 16.0, |
| // the label should be vertically centered (20 pixels above and below). |
| expect(getCustomLabelRect(tester).top, 20.0); |
| expect(getCustomLabelRect(tester).bottom, 36.0); |
| }, |
| ); |
| |
| testWidgets('Floating label animation duration and curve', (WidgetTester tester) async { |
| Future<void> pumpInputDecorator({ |
| required bool isFocused, |
| }) async { |
| return tester.pumpWidget( |
| buildInputDecorator( |
| isEmpty: true, |
| isFocused: isFocused, |
| decoration: const InputDecoration( |
| label: customLabel, |
| floatingLabelBehavior: FloatingLabelBehavior.auto, |
| ), |
| ), |
| ); |
| } |
| await pumpInputDecorator(isFocused: false); |
| // Label line height is forced to 1.0 and font size is 16.0, |
| // the label should be vertically centered (20 pixels above and below). |
| expect(getCustomLabelRect(tester).top, 20.0); |
| |
| // The label animates upwards and scales down. |
| // The animation duration is 167ms and the curve is fastOutSlowIn. |
| await pumpInputDecorator(isFocused: true); |
| await tester.pump(const Duration(milliseconds: 42)); |
| expect(getCustomLabelRect(tester).top, closeTo(17.09, 0.5)); |
| await tester.pump(const Duration(milliseconds: 42)); |
| expect(getCustomLabelRect(tester).top, closeTo(10.66, 0.5)); |
| await tester.pump(const Duration(milliseconds: 42)); |
| expect(getCustomLabelRect(tester).top, closeTo(8.47, 0.5)); |
| await tester.pump(const Duration(milliseconds: 41)); |
| expect(getCustomLabelRect(tester).top, 8.0); |
| |
| // If the animation changes direction without first reaching the |
| // AnimationStatus.completed or AnimationStatus.dismissed status, |
| // the CurvedAnimation stays on the same curve in the opposite direction. |
| // The pumpAndSettle is used to prevent this behavior. |
| await tester.pumpAndSettle(); |
| |
| // The label animates downwards and scales up. |
| // The animation duration is 167ms and the curve is fastOutSlowIn. |
| await pumpInputDecorator(isFocused: false); |
| await tester.pump(const Duration(milliseconds: 42)); |
| expect(getCustomLabelRect(tester).top, closeTo(10.90, 0.5)); |
| await tester.pump(const Duration(milliseconds: 42)); |
| expect(getCustomLabelRect(tester).top, closeTo(17.34, 0.5)); |
| await tester.pump(const Duration(milliseconds: 42)); |
| expect(getCustomLabelRect(tester).top, closeTo(19.69, 0.5)); |
| await tester.pump(const Duration(milliseconds: 41)); |
| expect(getCustomLabelRect(tester).top, 20.0); |
| }); |
| }); |
| |
| group('Material3 - InputDecoration border', () { |
| testWidgets('Compliant border when enabled and not focused', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| labelText: labelText, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).size, const Size(800.0, 56.0)); |
| expect(getBorderBottom(tester), 56.0); |
| expect(getBorderWeight(tester), 1.0); |
| final ThemeData theme = Theme.of(tester.element(find.byType(InputDecorator))); |
| expect(getBorderColor(tester), theme.colorScheme.outline); |
| }); |
| |
| testWidgets('Compliant border when focused', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isFocused: true, |
| decoration: const InputDecoration( |
| labelText: labelText, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).size, const Size(800.0, 56.0)); |
| expect(getBorderBottom(tester), 56.0); |
| expect(getBorderWeight(tester), 2.0); |
| final ThemeData theme = Theme.of(tester.element(find.byType(InputDecorator))); |
| expect(getBorderColor(tester), theme.colorScheme.primary); |
| }); |
| |
| testWidgets('Compliant border when disabled', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| labelText: labelText, |
| enabled: false, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).size, const Size(800.0, 56.0)); |
| expect(getBorderBottom(tester), 56.0); |
| expect(getBorderWeight(tester), 1.0); |
| final ThemeData theme = Theme.of(tester.element(find.byType(InputDecorator))); |
| expect(getBorderColor(tester), theme.colorScheme.onSurface.withOpacity(0.12)); |
| }); |
| |
| testWidgets('Compliant border when filled, enabled and not focused', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| labelText: labelText, |
| filled: true, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).size, const Size(800.0, 56.0)); |
| expect(getBorderBottom(tester), 56.0); |
| expect(getBorderWeight(tester), 1.0); |
| final ThemeData theme = Theme.of(tester.element(find.byType(InputDecorator))); |
| expect(getBorderColor(tester), theme.colorScheme.onSurfaceVariant); |
| }); |
| |
| testWidgets('Compliant border when filled and focused', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isFocused: true, |
| decoration: const InputDecoration( |
| labelText: labelText, |
| filled: true, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).size, const Size(800.0, 56.0)); |
| expect(getBorderBottom(tester), 56.0); |
| expect(getBorderWeight(tester), 2.0); |
| final ThemeData theme = Theme.of(tester.element(find.byType(InputDecorator))); |
| expect(getBorderColor(tester), theme.colorScheme.primary); |
| }); |
| |
| testWidgets('Compliant border when filled and disabled', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| labelText: labelText, |
| enabled: false, |
| filled: true, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).size, const Size(800.0, 56.0)); |
| expect(getBorderBottom(tester), 56.0); |
| expect(getBorderWeight(tester), 1.0); |
| final ThemeData theme = Theme.of(tester.element(find.byType(InputDecorator))); |
| expect(getBorderColor(tester), theme.colorScheme.onSurface.withOpacity(0.38)); |
| }); |
| |
| testWidgets('InputDecorator with no input border', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isEmpty: true, |
| decoration: const InputDecoration( |
| border: InputBorder.none, |
| ), |
| ), |
| ); |
| expect(getBorderWeight(tester), 0.0); |
| }); |
| |
| testWidgets('OutlineInputBorder radius carries over when lerping', (WidgetTester tester) async { |
| // This is a regression test for https://github.com/flutter/flutter/issues/23982 |
| const Key key = Key('textField'); |
| |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: TextField( |
| key: key, |
| decoration: InputDecoration( |
| fillColor: Colors.white, |
| filled: true, |
| border: UnderlineInputBorder( |
| borderSide: BorderSide(color: Colors.blue, width: 2.0), |
| borderRadius: BorderRadius.zero, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // TextField has the given border |
| expect(getBorderRadius(tester), BorderRadius.zero); |
| |
| // Focusing does not change the border |
| await tester.tap(find.byKey(key)); |
| await tester.pump(); |
| expect(getBorderRadius(tester), BorderRadius.zero); |
| await tester.pump(const Duration(milliseconds: 100)); |
| expect(getBorderRadius(tester), BorderRadius.zero); |
| await tester.pumpAndSettle(); |
| expect(getBorderRadius(tester), BorderRadius.zero); |
| }); |
| |
| testWidgets('OutlineInputBorder async lerp', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/28724 |
| |
| final Completer<void> completer = Completer<void>(); |
| bool waitIsOver = false; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setState) { |
| return GestureDetector( |
| onTap: () async { |
| setState(() { waitIsOver = true; }); |
| await completer.future; |
| setState(() { waitIsOver = false; }); |
| }, |
| child: InputDecorator( |
| decoration: InputDecoration( |
| labelText: 'Test', |
| enabledBorder: !waitIsOver ? null : const OutlineInputBorder(borderSide: BorderSide(color: Colors.blue)), |
| ), |
| ), |
| ); |
| }, |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(StatefulBuilder)); |
| await tester.pumpAndSettle(); |
| |
| completer.complete(); |
| await tester.pumpAndSettle(); |
| }); |
| |
| test('InputBorder equality', () { |
| // OutlineInputBorder's equality is defined by the borderRadius, borderSide, & gapPadding. |
| const OutlineInputBorder outlineInputBorder = OutlineInputBorder( |
| borderRadius: BorderRadius.all(Radius.circular(9.0)), |
| borderSide: BorderSide(color: Colors.blue), |
| gapPadding: 32.0, |
| ); |
| expect(outlineInputBorder, const OutlineInputBorder( |
| borderSide: BorderSide(color: Colors.blue), |
| borderRadius: BorderRadius.all(Radius.circular(9.0)), |
| gapPadding: 32.0, |
| )); |
| expect(outlineInputBorder, isNot(const OutlineInputBorder())); |
| expect(outlineInputBorder, isNot(const OutlineInputBorder( |
| borderSide: BorderSide(color: Colors.red), |
| borderRadius: BorderRadius.all(Radius.circular(9.0)), |
| gapPadding: 32.0, |
| ))); |
| expect(outlineInputBorder, isNot(const OutlineInputBorder( |
| borderSide: BorderSide(color: Colors.blue), |
| borderRadius: BorderRadius.all(Radius.circular(10.0)), |
| gapPadding: 32.0, |
| ))); |
| expect(outlineInputBorder, isNot(const OutlineInputBorder( |
| borderSide: BorderSide(color: Colors.blue), |
| borderRadius: BorderRadius.all(Radius.circular(9.0)), |
| gapPadding: 33.0, |
| ))); |
| |
| // UnderlineInputBorder's equality is defined by the borderSide and borderRadius. |
| const UnderlineInputBorder underlineInputBorder = UnderlineInputBorder( |
| borderSide: BorderSide(color: Colors.blue), |
| borderRadius: BorderRadius.only(topLeft: Radius.circular(5.0), topRight: Radius.circular(5.0)), |
| ); |
| expect(underlineInputBorder, const UnderlineInputBorder( |
| borderSide: BorderSide(color: Colors.blue), |
| borderRadius: BorderRadius.only(topLeft: Radius.circular(5.0), topRight: Radius.circular(5.0)), |
| )); |
| expect(underlineInputBorder, isNot(const UnderlineInputBorder())); |
| expect(underlineInputBorder, isNot(const UnderlineInputBorder( |
| borderSide: BorderSide(color: Colors.red), |
| borderRadius: BorderRadius.only(topLeft: Radius.circular(5.0), topRight: Radius.circular(5.0)), |
| ))); |
| expect(underlineInputBorder, isNot(const UnderlineInputBorder( |
| borderSide: BorderSide(color: Colors.blue), |
| borderRadius: BorderRadius.only(topLeft: Radius.circular(6.0), topRight: Radius.circular(6.0)), |
| ))); |
| }); |
| |
| test('InputBorder hashCodes', () { |
| // OutlineInputBorder's hashCode is defined by the borderRadius, borderSide, & gapPadding. |
| const OutlineInputBorder outlineInputBorder = OutlineInputBorder( |
| borderRadius: BorderRadius.all(Radius.circular(9.0)), |
| borderSide: BorderSide(color: Colors.blue), |
| gapPadding: 32.0, |
| ); |
| expect(outlineInputBorder.hashCode, const OutlineInputBorder( |
| borderRadius: BorderRadius.all(Radius.circular(9.0)), |
| borderSide: BorderSide(color: Colors.blue), |
| gapPadding: 32.0, |
| ).hashCode); |
| expect(outlineInputBorder.hashCode, isNot(const OutlineInputBorder().hashCode)); |
| expect(outlineInputBorder.hashCode, isNot(const OutlineInputBorder( |
| borderRadius: BorderRadius.all(Radius.circular(9.0)), |
| borderSide: BorderSide(color: Colors.red), |
| gapPadding: 32.0, |
| ).hashCode)); |
| expect(outlineInputBorder.hashCode, isNot(const OutlineInputBorder( |
| borderRadius: BorderRadius.all(Radius.circular(10.0)), |
| borderSide: BorderSide(color: Colors.blue), |
| gapPadding: 32.0, |
| ).hashCode)); |
| expect(outlineInputBorder.hashCode, isNot(const OutlineInputBorder( |
| borderRadius: BorderRadius.all(Radius.circular(9.0)), |
| borderSide: BorderSide(color: Colors.blue), |
| gapPadding: 33.0, |
| ).hashCode)); |
| |
| // UnderlineInputBorder's hashCode is defined by the borderSide and borderRadius. |
| const UnderlineInputBorder underlineInputBorder = UnderlineInputBorder( |
| borderSide: BorderSide(color: Colors.blue), |
| borderRadius: BorderRadius.only(topLeft: Radius.circular(5.0), topRight: Radius.circular(5.0)), |
| ); |
| expect(underlineInputBorder.hashCode, const UnderlineInputBorder( |
| borderSide: BorderSide(color: Colors.blue), |
| borderRadius: BorderRadius.only(topLeft: Radius.circular(5.0), topRight: Radius.circular(5.0)), |
| ).hashCode); |
| expect(underlineInputBorder.hashCode, isNot(const UnderlineInputBorder( |
| borderSide: BorderSide(color: Colors.red), |
| borderRadius: BorderRadius.only(topLeft: Radius.circular(5.0), topRight: Radius.circular(5.0)), |
| ).hashCode)); |
| expect(underlineInputBorder.hashCode, isNot(const UnderlineInputBorder( |
| borderSide: BorderSide(color: Colors.blue), |
| borderRadius: BorderRadius.only(topLeft: Radius.circular(6.0), topRight: Radius.circular(6.0)), |
| ).hashCode)); |
| }); |
| }); |
| |
| group('Material3 - InputDecoration hintText', () { |
| group('without label', () { |
| // Overall height for this InputDecorator is 48dp on mobile: |
| // 12 - Top padding |
| // 24 - Input and hint (font size = 16, line height = 1.5) |
| // 12 - Bottom padding |
| |
| testWidgets('hint and input align vertically when decorator is empty and not focused', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isEmpty: true, |
| decoration: const InputDecoration( |
| hintText: hintText, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).size, const Size(800.0, 48.0)); |
| expect(getInputRect(tester).top, 12.0); |
| expect(getInputRect(tester).bottom, 36.0); |
| expect(getHintRect(tester).top, getInputRect(tester).top); |
| expect(getHintRect(tester).bottom, getInputRect(tester).bottom); |
| }); |
| |
| testWidgets('hint and input align vertically when decorator is empty and focused', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isEmpty: true, |
| isFocused: true, |
| decoration: const InputDecoration( |
| hintText: hintText, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).size, const Size(800.0, 48.0)); |
| expect(getInputRect(tester).top, 12.0); |
| expect(getInputRect(tester).bottom, 36.0); |
| expect(getHintRect(tester).top, getInputRect(tester).top); |
| expect(getHintRect(tester).bottom, getInputRect(tester).bottom); |
| }); |
| |
| testWidgets('hint and input align vertically when decorator is empty and not focused', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isFocused: true, |
| decoration: const InputDecoration( |
| hintText: hintText, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).size, const Size(800.0, 48.0)); |
| expect(getInputRect(tester).top, 12.0); |
| expect(getInputRect(tester).bottom, 36.0); |
| expect(getHintRect(tester).top, getInputRect(tester).top); |
| expect(getHintRect(tester).bottom, getInputRect(tester).bottom); |
| }); |
| |
| testWidgets('hint and input align vertically when decorator is not empty and not focused', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| hintText: hintText, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).size, const Size(800.0, 48.0)); |
| expect(getInputRect(tester).top, 12.0); |
| expect(getInputRect(tester).bottom, 36.0); |
| expect(getHintRect(tester).top, getInputRect(tester).top); |
| expect(getHintRect(tester).bottom, getInputRect(tester).bottom); |
| }); |
| }); |
| |
| group('with label', () { |
| testWidgets('hint is not visible when decorator is empty and not focused', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isEmpty: true, |
| decoration: const InputDecoration( |
| labelText: labelText, |
| hintText: hintText, |
| ), |
| ), |
| ); |
| |
| expect(getHintOpacity(tester), 0.0); |
| }); |
| |
| testWidgets('hint is not visible when decorator is not empty and focused', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isFocused: true, |
| decoration: const InputDecoration( |
| labelText: labelText, |
| hintText: hintText, |
| ), |
| ), |
| ); |
| |
| expect(getHintOpacity(tester), 0.0); |
| }); |
| |
| testWidgets('hint is not visible when decorator is empty and not focused', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isEmpty: true, |
| decoration: const InputDecoration( |
| labelText: labelText, |
| hintText: hintText, |
| ), |
| ), |
| ); |
| |
| expect(getHintOpacity(tester), 0.0); |
| }); |
| |
| testWidgets('hint is visible and aligned with input text when decorator is empty and focused', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isEmpty: true, |
| isFocused: true, |
| decoration: const InputDecoration( |
| labelText: labelText, |
| hintText: hintText, |
| ), |
| ), |
| ); |
| |
| expect(getHintOpacity(tester), 1.0); |
| |
| // Overall height for this InputDecorator is 56dp on mobile: |
| // 8 - Top padding |
| // 12 - Floating label (font size = 16 * 0.75, line height is forced to 1.0) |
| // 4 - Gap between label and input (this is not part of the M3 spec) |
| // 24 - Input/Hint (font size = 16, line height = 1.5) |
| // 8 - Bottom padding |
| expect(getDecoratorRect(tester).size, const Size(800.0, 56.0)); |
| expect(getInputRect(tester).top, 24.0); |
| expect(getInputRect(tester).bottom, 48.0); |
| expect(getHintRect(tester).top, getInputRect(tester).top); |
| expect(getHintRect(tester).bottom, getInputRect(tester).bottom); |
| expect(getLabelRect(tester).top, 8.0); |
| expect(getLabelRect(tester).bottom, 20.0); |
| }); |
| |
| group('hint opacity animation', () { |
| testWidgets('default duration', (WidgetTester tester) async { |
| // Build once without focus. |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isEmpty: true, |
| decoration: const InputDecoration( |
| labelText: labelText, |
| hintText: hintText, |
| ), |
| ), |
| ); |
| |
| // Hint is not visible (opacity 0.0). |
| expect(getHintOpacity(tester), 0.0); |
| |
| // Focus the decorator to trigger the animation. |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isEmpty: true, |
| isFocused: true, |
| decoration: const InputDecoration( |
| labelText: labelText, |
| hintText: hintText, |
| ), |
| ), |
| ); |
| |
| // The hint's opacity animates from 0.0 to 1.0. |
| // The animation's default duration is 20ms. |
| await tester.pump(const Duration(milliseconds: 9)); |
| double hintOpacity9ms = getHintOpacity(tester); |
| expect(hintOpacity9ms, inExclusiveRange(0.0, 1.0)); |
| await tester.pump(const Duration(milliseconds: 9)); |
| double hintOpacity18ms = getHintOpacity(tester); |
| expect(hintOpacity18ms, inExclusiveRange(hintOpacity9ms, 1.0)); |
| |
| await tester.pumpAndSettle(); // Let the animation finish. |
| // Hint is fully visible (opacity 1.0). |
| expect(getHintOpacity(tester), 1.0); |
| |
| // Unfocus the decorator to trigger the reversed animation. |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isEmpty: true, |
| decoration: const InputDecoration( |
| labelText: labelText, |
| hintText: hintText, |
| ), |
| ), |
| ); |
| |
| // The hint's opacity animates from 1.0 to 0.0. |
| // The animation's default duration is 20ms. |
| await tester.pump(const Duration(milliseconds: 9)); |
| hintOpacity9ms = getHintOpacity(tester); |
| expect(hintOpacity9ms, inExclusiveRange(0.0, 1.0)); |
| await tester.pump(const Duration(milliseconds: 9)); |
| hintOpacity18ms = getHintOpacity(tester); |
| expect(hintOpacity18ms, inExclusiveRange(0.0, hintOpacity9ms)); |
| }); |
| |
| testWidgets('custom duration', (WidgetTester tester) async { |
| // Build once without focus. |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isEmpty: true, |
| decoration: const InputDecoration( |
| labelText: labelText, |
| hintText: hintText, |
| hintFadeDuration: Duration(milliseconds: 120), |
| ), |
| ), |
| ); |
| |
| // Hint is not visible (opacity 0.0). |
| expect(getHintOpacity(tester), 0.0); |
| |
| // Focus the decorator to trigger the animation. |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isEmpty: true, |
| isFocused: true, |
| decoration: const InputDecoration( |
| labelText: labelText, |
| hintText: hintText, |
| hintFadeDuration: Duration(milliseconds: 120), |
| ), |
| ), |
| ); |
| |
| // The hint's opacity animates from 0.0 to 1.0. |
| // The animation's duration is set to 120ms. |
| await tester.pump(const Duration(milliseconds: 50)); |
| double hintOpacity50ms = getHintOpacity(tester); |
| expect(hintOpacity50ms, inExclusiveRange(0.0, 1.0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| double hintOpacity100ms = getHintOpacity(tester); |
| expect(hintOpacity100ms, inExclusiveRange(hintOpacity50ms, 1.0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect(getHintOpacity(tester), 1.0); |
| |
| // Unfocus the decorator to trigger the reversed animation. |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isEmpty: true, |
| decoration: const InputDecoration( |
| labelText: labelText, |
| hintText: hintText, |
| hintFadeDuration: Duration(milliseconds: 120), |
| ), |
| ), |
| ); |
| |
| // The hint's opacity animates from 1.0 to 0.0. |
| // The animation's default duration is 20ms. |
| await tester.pump(const Duration(milliseconds: 50)); |
| hintOpacity50ms = getHintOpacity(tester); |
| expect(hintOpacity50ms, inExclusiveRange(0.0, 1.0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| hintOpacity100ms = getHintOpacity(tester); |
| expect(hintOpacity100ms, inExclusiveRange(0.0, hintOpacity50ms)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect(getHintOpacity(tester), 0.0); |
| }); |
| |
| testWidgets('duration from theme', (WidgetTester tester) async { |
| // Build once without focus. |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isEmpty: true, |
| decoration: const InputDecoration( |
| labelText: labelText, |
| hintText: hintText, |
| ), |
| inputDecorationTheme: const InputDecorationTheme( |
| hintFadeDuration: Duration(milliseconds: 120), |
| ), |
| ), |
| ); |
| |
| // Hint is not visible (opacity 0.0). |
| expect(getHintOpacity(tester), 0.0); |
| |
| // Focus the decorator to trigger the animation. |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isEmpty: true, |
| isFocused: true, |
| decoration: const InputDecoration( |
| labelText: labelText, |
| hintText: hintText, |
| ), |
| inputDecorationTheme: const InputDecorationTheme( |
| hintFadeDuration: Duration(milliseconds: 120), |
| ), |
| ), |
| ); |
| |
| // The hint's opacity animates from 0.0 to 1.0. |
| // The animation's duration is set to 120ms. |
| await tester.pump(const Duration(milliseconds: 50)); |
| double hintOpacity50ms = getHintOpacity(tester); |
| expect(hintOpacity50ms, inExclusiveRange(0.0, 1.0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| double hintOpacity100ms = getHintOpacity(tester); |
| expect(hintOpacity100ms, inExclusiveRange(hintOpacity50ms, 1.0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect(getHintOpacity(tester), 1.0); |
| |
| // Unfocus the decorator to trigger the reversed animation. |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isEmpty: true, |
| decoration: const InputDecoration( |
| labelText: labelText, |
| hintText: hintText, |
| ), |
| inputDecorationTheme: const InputDecorationTheme( |
| hintFadeDuration: Duration(milliseconds: 120), |
| ), |
| ), |
| ); |
| |
| // The hint's opacity animates from 1.0 to 0.0. |
| // The animation's default duration is 20ms. |
| await tester.pump(const Duration(milliseconds: 50)); |
| hintOpacity50ms = getHintOpacity(tester); |
| expect(hintOpacity50ms, inExclusiveRange(0.0, 1.0)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| hintOpacity100ms = getHintOpacity(tester); |
| expect(hintOpacity100ms, inExclusiveRange(0.0, hintOpacity50ms)); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect(getHintOpacity(tester), 0.0); |
| }); |
| }); |
| |
| group('InputDecoration.alignLabelWithHint', () { |
| testWidgets('positions InputDecoration.labelText vertically aligned with the hint', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isEmpty: true, |
| decoration: const InputDecoration( |
| labelText: labelText, |
| alignLabelWithHint: true, |
| hintText: hintText, |
| ), |
| ), |
| ); |
| |
| // Label and hint should be vertically aligned. |
| expect(getLabelCenter(tester).dy, getHintCenter(tester).dy); |
| }); |
| |
| testWidgets('positions InputDecoration.label vertically aligned with the hint', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isEmpty: true, |
| decoration: const InputDecoration( |
| label: customLabel, |
| alignLabelWithHint: true, |
| hintText: hintText, |
| ), |
| ), |
| ); |
| |
| // Label and hint should be vertically aligned. |
| expect(getCustomLabelCenter(tester).dy, getHintCenter(tester).dy); |
| }); |
| |
| group('in non-expanded multiline TextField', () { |
| testWidgets('positions the label correctly when strut is disabled', (WidgetTester tester) async { |
| final FocusNode focusNode = FocusNode(); |
| final TextEditingController controller = TextEditingController(); |
| addTearDown(() { focusNode.dispose(); controller.dispose();}); |
| |
| Widget buildFrame(bool alignLabelWithHint) { |
| return MaterialApp( |
| home: Material( |
| child: Align( |
| alignment: Alignment.topLeft, |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: TextField( |
| controller: controller, |
| focusNode: focusNode, |
| maxLines: 8, |
| decoration: InputDecoration( |
| labelText: labelText, |
| alignLabelWithHint: alignLabelWithHint, |
| hintText: hintText, |
| ), |
| strutStyle: StrutStyle.disabled, |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| // `alignLabelWithHint: false` centers the label vertically in the TextField. |
| await tester.pumpWidget(buildFrame(false)); |
| await tester.pumpAndSettle(); |
| expect(getLabelCenter(tester).dy, getDecoratorCenter(tester).dy); |
| |
| // Entering text still happens at the top. |
| await tester.enterText(find.byType(TextField), inputText); |
| expect(getInputRect(tester).top, 24.0); |
| controller.clear(); |
| focusNode.unfocus(); |
| |
| // `alignLabelWithHint: true` aligns the label vertically with the hint. |
| await tester.pumpWidget(buildFrame(true)); |
| await tester.pumpAndSettle(); |
| expect(getLabelCenter(tester).dy, getHintCenter(tester).dy); |
| |
| // Entering text still happens at the top. |
| await tester.enterText(find.byType(TextField), inputText); |
| expect(getInputRect(tester).top, 24.0); |
| controller.clear(); |
| focusNode.unfocus(); |
| }); |
| |
| testWidgets('positions the label correctly when strut style is set to default', (WidgetTester tester) async { |
| final FocusNode focusNode = FocusNode(); |
| final TextEditingController controller = TextEditingController(); |
| addTearDown(() { focusNode.dispose(); controller.dispose();}); |
| |
| Widget buildFrame(bool alignLabelWithHint) { |
| return MaterialApp( |
| home: Material( |
| child: Align( |
| alignment: Alignment.topLeft, |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: TextField( |
| controller: controller, |
| focusNode: focusNode, |
| maxLines: 8, |
| decoration: InputDecoration( |
| labelText: labelText, |
| alignLabelWithHint: alignLabelWithHint, |
| hintText: hintText, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| // `alignLabelWithHint: false` centers the label vertically in the TextField. |
| await tester.pumpWidget(buildFrame(false)); |
| await tester.pumpAndSettle(); |
| expect(getLabelCenter(tester).dy, getDecoratorCenter(tester).dy); |
| |
| // Entering text still happens at the top. |
| await tester.enterText(find.byType(InputDecorator), inputText); |
| expect(getInputRect(tester).top, 24.0); |
| controller.clear(); |
| focusNode.unfocus(); |
| |
| // `alignLabelWithHint: true` aligns the label vertically with the hint. |
| await tester.pumpWidget(buildFrame(true)); |
| await tester.pumpAndSettle(); |
| expect(getLabelCenter(tester).dy, getHintCenter(tester).dy); |
| |
| // Entering text still happens at the top. |
| await tester.enterText(find.byType(InputDecorator), inputText); |
| expect(getInputRect(tester).top, 24.0); |
| controller.clear(); |
| focusNode.unfocus(); |
| }); |
| }); |
| |
| group('in expanded multiline TextField', () { |
| testWidgets('positions the label correctly', (WidgetTester tester) async { |
| final FocusNode focusNode = FocusNode(); |
| final TextEditingController controller = TextEditingController(); |
| addTearDown(() { focusNode.dispose(); controller.dispose();}); |
| |
| Widget buildFrame(bool alignLabelWithHint) { |
| return MaterialApp( |
| home: Material( |
| child: Align( |
| alignment: Alignment.topLeft, |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: TextField( |
| controller: controller, |
| focusNode: focusNode, |
| maxLines: null, |
| expands: true, |
| decoration: InputDecoration( |
| labelText: labelText, |
| alignLabelWithHint: alignLabelWithHint, |
| hintText: hintText, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| // `alignLabelWithHint: false` centers the label vertically in the TextField. |
| await tester.pumpWidget(buildFrame(false)); |
| await tester.pumpAndSettle(); |
| expect(getLabelCenter(tester).dy, getDecoratorCenter(tester).dy); |
| |
| // Entering text still happens at the top. |
| await tester.enterText(find.byType(InputDecorator), inputText); |
| expect(getInputRect(tester).top, 24.0); |
| controller.clear(); |
| focusNode.unfocus(); |
| |
| // alignLabelWithHint: true aligns the label vertically with the hint at the top. |
| await tester.pumpWidget(buildFrame(true)); |
| await tester.pumpAndSettle(); |
| expect(getLabelCenter(tester).dy, getHintCenter(tester).dy); |
| |
| // Entering text still happens at the top. |
| await tester.enterText(find.byType(InputDecorator), inputText); |
| expect(getInputRect(tester).top, 24.0); |
| controller.clear(); |
| focusNode.unfocus(); |
| }); |
| |
| testWidgets('positions the label correctly when border is outlined', (WidgetTester tester) async { |
| final FocusNode focusNode = FocusNode(); |
| final TextEditingController controller = TextEditingController(); |
| addTearDown(() { focusNode.dispose(); controller.dispose();}); |
| |
| Widget buildFrame(bool alignLabelWithHint) { |
| return MaterialApp( |
| home: Material( |
| child: Align( |
| alignment: Alignment.topLeft, |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: TextField( |
| controller: controller, |
| focusNode: focusNode, |
| maxLines: null, |
| expands: true, |
| decoration: InputDecoration( |
| labelText: labelText, |
| alignLabelWithHint: alignLabelWithHint, |
| hintText: hintText, |
| border: const OutlineInputBorder( |
| borderRadius: BorderRadius.zero, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| // `alignLabelWithHint: false` centers the label vertically in the TextField. |
| await tester.pumpWidget(buildFrame(false)); |
| await tester.pumpAndSettle(); |
| expect(getLabelCenter(tester).dy, getDecoratorCenter(tester).dy); |
| |
| // Entering text happens in the center as well. |
| await tester.enterText(find.byType(InputDecorator), inputText); |
| expect(getInputCenter(tester).dy, getDecoratorCenter(tester).dy); |
| controller.clear(); |
| focusNode.unfocus(); |
| |
| // `alignLabelWithHint: true` aligns keeps the label in the center because |
| // that's where the hint is. |
| await tester.pumpWidget(buildFrame(true)); |
| await tester.pumpAndSettle(); |
| |
| // On M3, hint centering is slightly wrong. |
| // TODO(bleroux): remove closeTo usage when this is fixed. |
| expect(getHintCenter(tester).dy, closeTo(getDecoratorCenter(tester).dy, 2.0)); |
| expect(getLabelCenter(tester).dy, getHintCenter(tester).dy); |
| |
| // Entering text still happens in the center. |
| await tester.enterText(find.byType(InputDecorator), inputText); |
| expect(getInputCenter(tester).dy, getDecoratorCenter(tester).dy); |
| controller.clear(); |
| focusNode.unfocus(); |
| }); |
| }); |
| |
| group('Horizontal alignment', () { |
| testWidgets('Label for outlined decoration aligns horizontally with prefixIcon by default', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/113537. |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| prefixIcon: Icon(Icons.ac_unit), |
| labelText: labelText, |
| border: OutlineInputBorder(), |
| ), |
| isFocused: true, |
| ), |
| ); |
| |
| // 12 is the left padding. |
| // TODO(bleroux): consider changing this padding because from M3 soec this should be 16. |
| expect(getLabelRect(tester).left, 12.0); |
| // TODO(bleroux): consider changing the input text position because, based on M3 spec, |
| // the expected horizontal position is 52 (12 padding, 24 icon, 16 gap between icon and input). |
| // See https://m3.material.io/components/text-fields/specs#1ad2798c-ab41-4f0c-9a97-295ab9b37f33 |
| // (Note that the diagrams on the spec for outlined text field are wrong but the table for |
| // outlined text fields and the diagrams for filled text field point to these values). |
| // The 48.0 value come from icon min interactive width and height. |
| expect(getInputRect(tester).left, 48.0); |
| }); |
| |
| testWidgets('Label for outlined decoration aligns horizontally with input when alignLabelWithHint is true', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/113537. |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| prefixIcon: Icon(Icons.ac_unit), |
| labelText: labelText, |
| border: OutlineInputBorder(), |
| alignLabelWithHint: true, |
| ), |
| isFocused: true, |
| ), |
| ); |
| |
| expect(getLabelRect(tester).left, getInputRect(tester).left); |
| }); |
| |
| testWidgets('Label for filled decoration is horizontally aligned with text by default', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/113537. |
| // See https://github.com/flutter/flutter/pull/115540. |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| prefixIcon: Icon(Icons.ac_unit), |
| labelText: labelText, |
| filled: true, |
| ), |
| isFocused: true, |
| ), |
| ); |
| |
| // Label and input are horizontally aligned despite `alignLabelWithHint` being false (default value). |
| // The reason is that `alignLabelWithHint` was initially intended for vertical alignement only. |
| // See https://github.com/flutter/flutter/pull/24993 which introduced `alignLabelWithHint` parameter. |
| // See https://github.com/flutter/flutter/pull/115409 which used `alignLabelWithHint` for |
| // horizontal alignment in outlined text field. |
| expect(getLabelRect(tester).left, getInputRect(tester).left); |
| }); |
| }); |
| }); |
| }); |
| }); |
| |
| group('Material3 - InputDecoration helper/counter/error', () { |
| // Overall height for InputDecorator (filled or outlined) is 80dp on mobile: |
| // 8 - top padding |
| // 12 - floating label (font size = 16 * 0.75, line height is forced to 1.0) |
| // 4 - gap between label and input |
| // 24 - input text (font size = 16, line height = 1.5) |
| // 8 - bottom padding |
| // 8 - gap above supporting text |
| // 16 - helper/counter (font size = 12, line height is 1.5) |
| const double topPadding = 8.0; |
| const double floatingLabelHeight = 12.0; |
| const double labelInputGap = 4.0; |
| const double inputHeight = 24.0; |
| const double bottomPadding = 8.0; |
| // TODO(bleroux): make the InputDecorator implementation compliant with M3 spec by changing |
| // the helperGap to 4.0 instead of 8.0. |
| // See https://github.com/flutter/flutter/issues/144984. |
| const double helperGap = 8.0; |
| const double helperHeight = 16.0; |
| const double containerHeight = topPadding + floatingLabelHeight + labelInputGap + inputHeight + bottomPadding; // 56.0 |
| const double fullHeight = containerHeight + helperGap + helperHeight; // 80.0 (should be 76.0 based on M3 spec) |
| const double errorHeight = helperHeight; |
| // TODO(bleroux): consider changing this padding because, from the M3 specification, it should be 16. |
| const double helperStartPadding = 12.0; |
| const double counterEndPadding = 12.0; |
| |
| // Actual size varies a little on web platforms with HTML renderer. |
| // TODO(bleroux): remove closeTo usage when https://github.com/flutter/flutter/issues/99933 is fixed. |
| final Matcher closeToFullHeight = closeTo(fullHeight, 0.1); |
| final Matcher closeToHelperHeight = closeTo(helperHeight, 0.1); |
| final Matcher closeToErrorHeight = closeTo(errorHeight, 0.1); |
| |
| group('for filled text field', () { |
| group('when field is enabled', () { |
| testWidgets('Helper and counter are correctly positioned', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| filled: true, |
| labelText: labelText, |
| helperText: helperText, |
| counterText: counterText, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).height, closeToFullHeight); |
| expect(getBorderBottom(tester), containerHeight); |
| expect(getHelperRect(tester).top, containerHeight + helperGap); |
| expect(getHelperRect(tester).height, closeToHelperHeight); |
| expect(getHelperRect(tester).left, helperStartPadding); |
| expect(getCounterRect(tester).top, containerHeight + helperGap); |
| expect(getCounterRect(tester).height, closeToHelperHeight); |
| expect(getCounterRect(tester).right, 800 - counterEndPadding); |
| }); |
| |
| testWidgets('Helper and counter are correctly styled', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| filled: true, |
| labelText: labelText, |
| helperText: helperText, |
| counterText: counterText, |
| ), |
| ), |
| ); |
| |
| final ThemeData theme = Theme.of(tester.element(findDecorator())); |
| final Color expectedColor = theme.colorScheme.onSurfaceVariant; |
| final TextStyle expectedStyle = theme.textTheme.bodySmall!.copyWith(color: expectedColor); |
| expect(getHelperStyle(tester), expectedStyle); |
| expect(getCounterStyle(tester), expectedStyle); |
| }); |
| }); |
| |
| group('when field is disabled', () { |
| testWidgets('Helper and counter are correctly positioned', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| filled: true, |
| enabled: false, |
| labelText: labelText, |
| helperText: helperText, |
| counterText: counterText, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).height, closeToFullHeight); |
| expect(getBorderBottom(tester), containerHeight); |
| expect(getHelperRect(tester).top, containerHeight + helperGap); |
| expect(getHelperRect(tester).height, closeToHelperHeight); |
| expect(getHelperRect(tester).left, helperStartPadding); |
| expect(getCounterRect(tester).top, containerHeight + helperGap); |
| expect(getCounterRect(tester).height, closeToHelperHeight); |
| expect(getCounterRect(tester).right, 800 - counterEndPadding); |
| }); |
| |
| testWidgets('Helper and counter are correctly styled', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| filled: true, |
| enabled: false, |
| labelText: labelText, |
| helperText: helperText, |
| counterText: counterText, |
| ), |
| ), |
| ); |
| |
| final ThemeData theme = Theme.of(tester.element(findDecorator())); |
| final Color expectedColor = theme.colorScheme.onSurface.withOpacity(0.38); |
| final TextStyle expectedStyle = theme.textTheme.bodySmall!.copyWith(color: expectedColor); |
| expect(getHelperStyle(tester), expectedStyle); |
| expect(getCounterStyle(tester), expectedStyle); |
| }); |
| }); |
| |
| group('when field is hovered', () { |
| testWidgets('Helper and counter are correctly positioned', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isHovering: true, |
| decoration: const InputDecoration( |
| filled: true, |
| labelText: labelText, |
| helperText: helperText, |
| counterText: counterText, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).height, closeToFullHeight); |
| expect(getBorderBottom(tester), containerHeight); |
| expect(getHelperRect(tester).top, containerHeight + helperGap); |
| expect(getHelperRect(tester).height, closeToHelperHeight); |
| expect(getHelperRect(tester).left, helperStartPadding); |
| expect(getCounterRect(tester).top, containerHeight + helperGap); |
| expect(getCounterRect(tester).height, closeToHelperHeight); |
| expect(getCounterRect(tester).right, 800 - counterEndPadding); |
| }); |
| |
| testWidgets('Helper and counter are correctly styled', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isHovering: true, |
| decoration: const InputDecoration( |
| filled: true, |
| labelText: labelText, |
| helperText: helperText, |
| counterText: counterText, |
| ), |
| ), |
| ); |
| |
| final ThemeData theme = Theme.of(tester.element(findDecorator())); |
| final Color expectedColor = theme.colorScheme.onSurfaceVariant; |
| final TextStyle expectedStyle = theme.textTheme.bodySmall!.copyWith(color: expectedColor); |
| expect(getHelperStyle(tester), expectedStyle); |
| expect(getCounterStyle(tester), expectedStyle); |
| }); |
| }); |
| |
| group('when field is focused', () { |
| testWidgets('Helper and counter are correctly positioned', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isFocused: true, |
| decoration: const InputDecoration( |
| filled: true, |
| labelText: labelText, |
| helperText: helperText, |
| counterText: counterText, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).height, closeToFullHeight); |
| expect(getBorderBottom(tester), containerHeight); |
| expect(getHelperRect(tester).top, containerHeight + helperGap); |
| expect(getHelperRect(tester).height, closeToHelperHeight); |
| expect(getHelperRect(tester).left, helperStartPadding); |
| expect(getCounterRect(tester).top, containerHeight + helperGap); |
| expect(getCounterRect(tester).height, closeToHelperHeight); |
| expect(getCounterRect(tester).right, 800 - counterEndPadding); |
| }); |
| |
| testWidgets('Helper and counter are correctly styled', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isFocused: true, |
| decoration: const InputDecoration( |
| filled: true, |
| labelText: labelText, |
| helperText: helperText, |
| counterText: counterText, |
| ), |
| ), |
| ); |
| |
| final ThemeData theme = Theme.of(tester.element(findDecorator())); |
| final Color expectedColor = theme.colorScheme.onSurfaceVariant; |
| final TextStyle expectedStyle = theme.textTheme.bodySmall!.copyWith(color: expectedColor); |
| expect(getHelperStyle(tester), expectedStyle); |
| expect(getCounterStyle(tester), expectedStyle); |
| }); |
| }); |
| |
| group('when field is in error', () { |
| testWidgets('Error and counter are visible, helper is not visible', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isFocused: true, |
| decoration: const InputDecoration( |
| filled: true, |
| labelText: labelText, |
| helperText: helperText, |
| counterText: counterText, |
| errorText: errorText, |
| ), |
| ), |
| ); |
| |
| expect(findError(), findsOneWidget); |
| expect(findCounter(), findsOneWidget); |
| expect(findHelper(), findsNothing); |
| }); |
| |
| testWidgets('Error and counter are correctly positioned', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isFocused: true, |
| decoration: const InputDecoration( |
| filled: true, |
| labelText: labelText, |
| helperText: helperText, |
| counterText: counterText, |
| errorText: errorText, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).height, closeToFullHeight); |
| expect(getBorderBottom(tester), containerHeight); |
| expect(getErrorRect(tester).top, containerHeight + helperGap); |
| expect(getErrorRect(tester).height, closeToErrorHeight); |
| expect(getErrorRect(tester).left, helperStartPadding); |
| expect(getCounterRect(tester).top, containerHeight + helperGap); |
| expect(getCounterRect(tester).height, closeToErrorHeight); |
| expect(getCounterRect(tester).right, 800 - counterEndPadding); |
| }); |
| |
| testWidgets('Error and counter are correctly styled', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isFocused: true, |
| decoration: const InputDecoration( |
| filled: true, |
| labelText: labelText, |
| helperText: helperText, |
| counterText: counterText, |
| errorText: errorText, |
| ), |
| ), |
| ); |
| |
| final ThemeData theme = Theme.of(tester.element(findDecorator())); |
| final Color expectedColor = theme.colorScheme.error; |
| final TextStyle expectedStyle = theme.textTheme.bodySmall!.copyWith(color: expectedColor); |
| expect(getErrorStyle(tester), expectedStyle); |
| final Color expectedCounterColor = theme.colorScheme.onSurfaceVariant; |
| final TextStyle expectedCounterStyle = theme.textTheme.bodySmall!.copyWith(color: expectedCounterColor); |
| expect(getCounterStyle(tester), expectedCounterStyle); |
| }); |
| }); |
| }); |
| |
| group('for outlined text field', () { |
| group('when field is enabled', () { |
| testWidgets('Helper and counter are correctly positioned', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| border: OutlineInputBorder(), |
| labelText: labelText, |
| helperText: helperText, |
| counterText: counterText, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).height, closeToFullHeight); |
| expect(getBorderBottom(tester), containerHeight); |
| expect(getHelperRect(tester).top, containerHeight + helperGap); |
| expect(getHelperRect(tester).height, closeToHelperHeight); |
| expect(getHelperRect(tester).left, helperStartPadding); |
| expect(getCounterRect(tester).top, containerHeight + helperGap); |
| expect(getCounterRect(tester).height, closeToHelperHeight); |
| expect(getCounterRect(tester).right, 800 - counterEndPadding); |
| }); |
| |
| testWidgets('Helper and counter are correctly styled', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| border: OutlineInputBorder(), |
| labelText: labelText, |
| helperText: helperText, |
| counterText: counterText, |
| ), |
| ), |
| ); |
| |
| final ThemeData theme = Theme.of(tester.element(findDecorator())); |
| final Color expectedColor = theme.colorScheme.onSurfaceVariant; |
| final TextStyle expectedStyle = theme.textTheme.bodySmall!.copyWith(color: expectedColor); |
| expect(getHelperStyle(tester), expectedStyle); |
| expect(getCounterStyle(tester), expectedStyle); |
| }); |
| }); |
| |
| group('when field is disabled', () { |
| testWidgets('Helper and counter are correctly positioned', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| border: OutlineInputBorder(), |
| enabled: false, |
| labelText: labelText, |
| helperText: helperText, |
| counterText: counterText, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).height, closeToFullHeight); |
| expect(getBorderBottom(tester), containerHeight); |
| expect(getHelperRect(tester).top, containerHeight + helperGap); |
| expect(getHelperRect(tester).height, closeToHelperHeight); |
| expect(getHelperRect(tester).left, helperStartPadding); |
| expect(getCounterRect(tester).top, containerHeight + helperGap); |
| expect(getCounterRect(tester).height, closeToHelperHeight); |
| expect(getCounterRect(tester).right, 800 - counterEndPadding); |
| }); |
| |
| testWidgets('Helper and counter are correctly styled', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| border: OutlineInputBorder(), |
| enabled: false, |
| labelText: labelText, |
| helperText: helperText, |
| counterText: counterText, |
| ), |
| ), |
| ); |
| |
| final ThemeData theme = Theme.of(tester.element(findDecorator())); |
| final Color expectedColor = theme.colorScheme.onSurface.withOpacity(0.38); |
| final TextStyle expectedStyle = theme.textTheme.bodySmall!.copyWith(color: expectedColor); |
| expect(getHelperStyle(tester), expectedStyle); |
| expect(getCounterStyle(tester), expectedStyle); |
| }); |
| }); |
| |
| group('when field is hovered', () { |
| testWidgets('Helper and counter are correctly positioned', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isHovering: true, |
| decoration: const InputDecoration( |
| border: OutlineInputBorder(), |
| labelText: labelText, |
| helperText: helperText, |
| counterText: counterText, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).height, closeToFullHeight); |
| expect(getBorderBottom(tester), containerHeight); |
| expect(getHelperRect(tester).top, containerHeight + helperGap); |
| expect(getHelperRect(tester).height, closeToHelperHeight); |
| expect(getHelperRect(tester).left, helperStartPadding); |
| expect(getCounterRect(tester).top, containerHeight + helperGap); |
| expect(getCounterRect(tester).height, closeToHelperHeight); |
| expect(getCounterRect(tester).right, 800 - counterEndPadding); |
| }); |
| |
| testWidgets('Helper and counter are correctly styled', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isHovering: true, |
| decoration: const InputDecoration( |
| border: OutlineInputBorder(), |
| labelText: labelText, |
| helperText: helperText, |
| counterText: counterText, |
| ), |
| ), |
| ); |
| |
| final ThemeData theme = Theme.of(tester.element(findDecorator())); |
| final Color expectedColor = theme.colorScheme.onSurfaceVariant; |
| final TextStyle expectedStyle = theme.textTheme.bodySmall!.copyWith(color: expectedColor); |
| expect(getHelperStyle(tester), expectedStyle); |
| expect(getCounterStyle(tester), expectedStyle); |
| }); |
| }); |
| |
| group('when field is focused', () { |
| testWidgets('Helper and counter are correctly positioned', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isFocused: true, |
| decoration: const InputDecoration( |
| border: OutlineInputBorder(), |
| labelText: labelText, |
| helperText: helperText, |
| counterText: counterText, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).height, closeToFullHeight); |
| expect(getBorderBottom(tester), containerHeight); |
| expect(getHelperRect(tester).top, containerHeight + helperGap); |
| expect(getHelperRect(tester).height, closeToHelperHeight); |
| expect(getHelperRect(tester).left, helperStartPadding); |
| expect(getCounterRect(tester).top, containerHeight + helperGap); |
| expect(getCounterRect(tester).height, closeToHelperHeight); |
| expect(getCounterRect(tester).right, 800 - counterEndPadding); |
| }); |
| |
| testWidgets('Helper and counter are correctly styled', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isFocused: true, |
| decoration: const InputDecoration( |
| border: OutlineInputBorder(), |
| labelText: labelText, |
| helperText: helperText, |
| counterText: counterText, |
| ), |
| ), |
| ); |
| |
| final ThemeData theme = Theme.of(tester.element(findDecorator())); |
| final Color expectedColor = theme.colorScheme.onSurfaceVariant; |
| final TextStyle expectedStyle = theme.textTheme.bodySmall!.copyWith(color: expectedColor); |
| expect(getHelperStyle(tester), expectedStyle); |
| expect(getCounterStyle(tester), expectedStyle); |
| }); |
| }); |
| |
| group('when field is in error', () { |
| testWidgets('Error and counter are visible, helper is not visible', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isFocused: true, |
| decoration: const InputDecoration( |
| border: OutlineInputBorder(), |
| labelText: labelText, |
| helperText: helperText, |
| counterText: counterText, |
| errorText: errorText, |
| ), |
| ), |
| ); |
| |
| expect(findHelper(), findsNothing); |
| expect(findError(), findsOneWidget); |
| expect(findCounter(), findsOneWidget); |
| }); |
| |
| testWidgets('Error and counter are correctly positioned', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isFocused: true, |
| decoration: const InputDecoration( |
| border: OutlineInputBorder(), |
| labelText: labelText, |
| helperText: helperText, |
| counterText: counterText, |
| errorText: errorText, |
| ), |
| ), |
| ); |
| |
| expect(getDecoratorRect(tester).height, closeToFullHeight); |
| expect(getBorderBottom(tester), containerHeight); |
| expect(getErrorRect(tester).top, containerHeight + helperGap); |
| expect(getErrorRect(tester).height, closeToErrorHeight); |
| expect(getErrorRect(tester).left, helperStartPadding); |
| expect(getCounterRect(tester).top, containerHeight + helperGap); |
| expect(getCounterRect(tester).height, closeToErrorHeight); |
| expect(getCounterRect(tester).right, 800 - counterEndPadding); |
| }); |
| |
| testWidgets('Error and counter are correctly styled', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| isFocused: true, |
| decoration: const InputDecoration( |
| border: OutlineInputBorder(), |
| labelText: labelText, |
| helperText: helperText, |
| counterText: counterText, |
| errorText: errorText, |
| ), |
| ), |
| ); |
| |
| final ThemeData theme = Theme.of(tester.element(findDecorator())); |
| final Color expectedColor = theme.colorScheme.error; |
| final TextStyle expectedStyle = theme.textTheme.bodySmall!.copyWith(color: expectedColor); |
| expect(getErrorStyle(tester), expectedStyle); |
| final Color expectedCounterColor = theme.colorScheme.onSurfaceVariant; |
| final TextStyle expectedCounterStyle = theme.textTheme.bodySmall!.copyWith(color: expectedCounterColor); |
| expect(getCounterStyle(tester), expectedCounterStyle); |
| }); |
| }); |
| }); |
| |
| group('Multiline error/helper', () { |
| testWidgets('Error height grows to accommodate error text', (WidgetTester tester) async { |
| const int maxLines = 3; |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| labelText: 'label', |
| errorText: threeLines, |
| errorMaxLines: maxLines, |
| filled: true, |
| ), |
| ), |
| ); |
| |
| final Rect errorRect = tester.getRect(find.text(threeLines)); |
| expect(errorRect.height, closeTo(errorHeight * maxLines, 0.25)); |
| expect(getDecoratorRect(tester).height, closeTo(containerHeight + helperGap + errorHeight * maxLines, 0.25)); |
| }); |
| |
| testWidgets('Error height is correct when errorMaxLines is restricted', (WidgetTester tester) async { |
| const int maxLines = 2; |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| labelText: 'label', |
| errorText: threeLines, |
| errorMaxLines: maxLines, |
| filled: true, |
| ), |
| ), |
| ); |
| |
| final Rect errorRect = tester.getRect(find.text(threeLines)); |
| expect(errorRect.height, closeTo(errorHeight * maxLines, 0.25)); |
| expect(getDecoratorRect(tester).height, closeTo(containerHeight + helperGap + errorHeight * maxLines, 0.25)); |
| }); |
| |
| testWidgets('Error height is correct when errorMaxLines is bigger than the number of lines in errorText', (WidgetTester tester) async { |
| const int numberOfLines = 2; |
| const int maxLines = 3; |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| labelText: 'label', |
| errorText: twoLines, |
| errorMaxLines: maxLines, |
| filled: true, |
| ), |
| ), |
| ); |
| |
| final Rect errorRect = tester.getRect(find.text(twoLines)); |
| expect(errorRect.height, closeTo(errorHeight * numberOfLines, 0.25)); |
| expect(getDecoratorRect(tester).height, closeTo(containerHeight + helperGap + errorHeight * numberOfLines, 0.25)); |
| }); |
| |
| testWidgets('Helper height grows to accommodate helper text', (WidgetTester tester) async { |
| const int maxLines = 3; |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| labelText: 'label', |
| helperText: threeLines, |
| helperMaxLines: maxLines, |
| filled: true, |
| ), |
| ), |
| ); |
| |
| final Rect helperRect = tester.getRect(find.text(threeLines)); |
| expect(helperRect.height, closeTo(helperHeight * maxLines, 0.25)); |
| expect(getDecoratorRect(tester).height, closeTo(containerHeight + helperGap + helperHeight * maxLines, 0.25)); |
| }); |
| |
| testWidgets('Helper height is correct when maxLines is restricted', (WidgetTester tester) async { |
| const int maxLines = 2; |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| labelText: 'label', |
| helperText: threeLines, |
| helperMaxLines: maxLines, |
| filled: true, |
| ), |
| ), |
| ); |
| |
| final Rect helperRect = tester.getRect(find.text(threeLines)); |
| expect(helperRect.height, closeTo(helperHeight * maxLines, 0.25)); |
| expect(getDecoratorRect(tester).height, closeTo(containerHeight + helperGap + helperHeight * maxLines, 0.25)); |
| }); |
| |
| testWidgets('Helper height is correct when helperMaxLines is bigger than the number of lines in helperText', (WidgetTester tester) async { |
| const int numberOfLines = 2; |
| const int maxLines = 3; |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| labelText: 'label', |
| helperText: twoLines, |
| helperMaxLines: maxLines, |
| filled: true, |
| ), |
| ), |
| ); |
| |
| final Rect helperRect = tester.getRect(find.text(twoLines)); |
| expect(helperRect.height, closeTo(helperHeight * numberOfLines, 0.25)); |
| expect(getDecoratorRect(tester).height, closeTo(containerHeight + helperGap + helperHeight * numberOfLines, 0.25)); |
| }); |
| }); |
| |
| group('Error widget', () { |
| testWidgets('InputDecorator shows error widget', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| error: Text('error', style: TextStyle(fontSize: 20.0)), |
| ), |
| ), |
| ); |
| |
| expect(find.text('error'), findsOneWidget); |
| }); |
| |
| testWidgets('InputDecorator throws when error text and error widget are provided', (WidgetTester tester) async { |
| expect( |
| () { |
| buildInputDecorator( |
| decoration: InputDecoration( |
| errorText: 'errorText', |
| error: const Text('error', style: TextStyle(fontSize: 20.0)), |
| ), |
| ); |
| }, |
| throwsAssertionError, |
| ); |
| }); |
| }); |
| }); |
| |
| testWidgets('Material3 - Default height is 56dp on mobile', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| labelText: 'label', |
| ), |
| ), |
| ); |
| |
| // Overall height for this InputDecorator is 56dp on mobile: |
| // 8 - top padding |
| // 12 - floating label (font size = 16 * 0.75, line height is forced to 1.0) |
| // 4 - gap between label and input |
| // 24 - input text (font size = 16, line height = 1.5) |
| // 8 - bottom padding |
| // TODO(bleroux): fix input decorator to not rely on a 4 pixels gap between the label and the input, |
| // this gap is not compliant with the M3 spec (M3 spec uses line height for this purpose). |
| expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 56.0)); |
| }, variant: TargetPlatformVariant.mobile()); |
| |
| testWidgets('Material3 - Default height is 48dp on desktop', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| labelText: 'label', |
| ), |
| ), |
| ); |
| |
| // Overall height for this InputDecorator is 48dp on desktop: |
| // 4 - top padding |
| // 12 - floating label (font size = 16 * 0.75, line height is forced to 1.0) |
| // 4 - gap between label and input |
| // 24 - input text (font size = 16, line height = 1.5) |
| // 4 - bottom padding |
| expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 48.0)); |
| }, variant: TargetPlatformVariant.desktop()); |
| |
| testWidgets('Material3 - Default height is 56dp on mobile', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildInputDecorator( |
| decoration: const InputDecoration( |
| labelText: 'label', |
| ), |
| ), |
| ); |
| |
| // Overall height for this InputDecorator is 56dp on mobile: |
| // 8 - top padding |
| // 12 - floating label (font size = 16 * 0.75, line height is forced to 1.0) |
| // 4 - gap between label and input |
| // 24 - input text (font size = 16, line height = 1.5) |
| // 8 - bottom padding |
| // TODO(bleroux): fix input decorator to not rely on a 4 pixels gap between the label and the input, |
| // this gap is not compliant with the M3 spec (M3 spec uses line height for this purpose). |
| expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 56.0)); |
| }, variant: TargetPlatformVariant.mobile()); |
| |
| // This is a regression test for https://github.com/flutter/flutter/issues/139916. |
| testWidgets('Prefix ignores pointer when hidden', (WidgetTester tester) async { |
| bool tapped = false; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setState) { |
| return TextField( |
| decoration: InputDecoration( |
| labelText: 'label', |
| prefix: GestureDetector( |
| onTap: () { |
| setState(() { |
| tapped = true; |
| }); |
| }, |
| child: const Icon(Icons.search), |
| ), |
| ), |
| ); |
| } |
| ), |
| ), |
| ), |
| ); |
| |
| expect(tapped, isFalse); |
| |
| double prefixOpacity = tester.widget<AnimatedOpacity>(find.ancestor( |
| of: find.byType(Icon), |
| matching: find.byType(AnimatedOpacity), |
| )).opacity; |
| |
| // Initially the prefix icon should be hidden. |
| expect(prefixOpacity, 0.0); |
| |
| await tester.tap(find.byType(Icon), warnIfMissed: false); // Not expected to find the target. |
| await tester.pump(); |
| |
| // The suffix icon should ignore pointer events when hidden. |
| expect(tapped, isFalse); |
| |
| // Tap the text field to show the prefix icon. |
| await tester.tap(find.byType(TextField)); |
| await tester.pump(); |
| |
| prefixOpacity = tester.widget<AnimatedOpacity>(find.ancestor( |
| of: find.byType(Icon), |
| matching: find.byType(AnimatedOpacity), |
| )).opacity; |
| |
| // The prefix icon should be visible. |
| expect(prefixOpacity, 1.0); |
| |
| // Tap the prefix icon. |
| await tester.tap(find.byType(Icon)); |
| await tester.pump(); |
| |
| // The prefix icon should be tapped. |
| expect(tapped, isTrue); |
| }); |
| |
| // This is a regression test for https://github.com/flutter/flutter/issues/139916. |
| testWidgets('Suffix ignores pointer when hidden', (WidgetTester tester) async { |
| bool tapped = false; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setState) { |
| return TextField( |
| decoration: InputDecoration( |
| labelText: 'label', |
| suffix: GestureDetector( |
| onTap: () { |
| setState(() { |
| tapped = true; |
| }); |
| }, |
| child: const Icon(Icons.search), |
| ), |
| ), |
| ); |
| } |
| ), |
| ), |
| ), |
| ); |
| |
| expect(tapped, isFalse); |
| |
| double suffixOpacity = tester.widget<AnimatedOpacity>(find.ancestor( |
| of: find.byType(Icon), |
| matching: find.byType(AnimatedOpacity), |
| )).opacity; |
| |
| // Initially the suffix icon should be hidden. |
| expect(suffixOpacity, 0.0); |
| |
| await tester.tap(find.byType(Icon), warnIfMissed: false); // Not expected to find the target. |
| await tester.pump(); |
| |
| // The suffix icon should ignore pointer events when hidden. |
| expect(tapped, isFalse); |
| |
| // Tap the text field to show the suffix icon. |
| await tester.tap(find.byType(TextField)); |
| await tester.pump(); |
| |
| suffixOpacity = tester.widget<AnimatedOpacity>(find.ancestor( |
| of: find.byType(Icon), |
| matching: find.byType(AnimatedOpacity), |
| )).opacity; |
| |
| // The suffix icon should be visible. |
| expect(suffixOpacity, 1.0); |
| |
| // Tap the suffix icon. |
| await tester.tap(find.byType(Icon)); |
| await tester.pump(); |
| |
| // The suffix icon should be tapped. |
| expect(tapped, isTrue); |
| }); |
| |
| testWidgets('InputDecorator counter text, widget, and null', (WidgetTester tester) async { |
| Widget buildFrame({ |
| InputCounterWidgetBuilder? buildCounter, |
| String? counterText, |
| Widget? counter, |
| int? maxLength, |
| }) { |
| return MaterialApp( |
| home: Scaffold( |
| body: Center( |
| child: Column( |
| mainAxisAlignment: MainAxisAlignment.center, |
| children: <Widget>[ |
| TextFormField( |
| buildCounter: buildCounter, |
| maxLength: maxLength, |
| decoration: InputDecoration( |
| counterText: counterText, |
| counter: counter, |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| // When counter, counterText, and buildCounter are null, defaults to showing |
| // the built-in counter. |
| int? maxLength = 10; |
| await tester.pumpWidget(buildFrame(maxLength: maxLength)); |
| Finder counterFinder = find.byType(Text); |
| expect(counterFinder, findsOneWidget); |
| final Text counterWidget = tester.widget(counterFinder); |
| expect(counterWidget.data, '0/$maxLength'); |
| |
| // When counter, counterText, and buildCounter are set, shows the counter |
| // widget. |
| final Key counterKey = UniqueKey(); |
| final Key buildCounterKey = UniqueKey(); |
| const String counterText = 'I show instead of count'; |
| final Widget counter = Text('hello', key: counterKey); |
| Widget buildCounter( |
| BuildContext context, { |
| required int currentLength, |
| required int? maxLength, |
| required bool isFocused, |
| }) { |
| return Text( |
| '$currentLength of $maxLength', |
| key: buildCounterKey, |
| ); |
| } |
| |
| await tester.pumpWidget(buildFrame( |
| counterText: counterText, |
| counter: counter, |
| buildCounter: buildCounter, |
| maxLength: maxLength, |
| )); |
| counterFinder = find.byKey(counterKey); |
| expect(counterFinder, findsOneWidget); |
| expect(find.text(counterText), findsNothing); |
| expect(find.byKey(buildCounterKey), findsNothing); |
| |
| // When counter is null but counterText and buildCounter are set, shows the |
| // counterText. |
| await tester.pumpWidget(buildFrame( |
| counterText: counterText, |
| buildCounter: buildCounter, |
| maxLength: maxLength, |
| )); |
| expect(find.text(counterText), findsOneWidget); |
| counterFinder = find.byKey(counterKey); |
| expect(counterFinder, findsNothing); |
| expect(find.byKey(buildCounterKey), findsNothing); |
| |
| // When counter and counterText are null but buildCounter is set, shows the |
| // generated widget. |
| await tester.pumpWidget(buildFrame( |
| buildCounter: buildCounter, |
| maxLength: maxLength, |
| )); |
| expect(find.byKey(buildCounterKey), findsOneWidget); |
| expect(counterFinder, findsNothing); |
| expect(find.text(counterText), findsNothing); |
| |
| // When counterText is empty string and counter and buildCounter are null, |
| // shows nothing. |
| await tester.pumpWidget(buildFrame(counterText: '', maxLength: maxLength)); |
| expect(find.byType(Text), findsNothing); |
| |
| // When no maxLength, can still show a counter |
| maxLength = null; |
| await tester.pumpWidget(buildFrame( |
| buildCounter: buildCounter, |
| maxLength: maxLength, |
| )); |
| expect(find.byKey(buildCounterKey), findsOneWidget); |
| }); |
| |
| testWidgets('InputDecorator iconColor/prefixIconColor/suffixIconColor', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: TextField( |
| decoration: InputDecoration( |
| icon: Icon(Icons.cabin), |
| prefixIcon: Icon(Icons.sailing), |
| suffixIcon: Icon(Icons.close), |
| iconColor: Colors.amber, |
| prefixIconColor: Colors.green, |
| suffixIconColor: Colors.red, |
| filled: true, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(tester.widget<IconTheme>(find.widgetWithIcon(IconTheme,Icons.cabin).first).data.color, Colors.amber); |
| expect(tester.widget<IconTheme>(find.widgetWithIcon(IconTheme,Icons.sailing).first).data.color, Colors.green); |
| expect(tester.widget<IconTheme>(find.widgetWithIcon(IconTheme,Icons.close).first).data.color, Colors.red); |
| }); |
| |
| testWidgets('InputDecorator suffixIconColor in error state', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| decoration: InputDecoration( |
| suffixIcon: IconButton(icon: const Icon(Icons.close), onPressed: () {}), |
| errorText: 'error state', |
| filled: true, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final ThemeData theme = Theme.of(tester.element(find.byType(TextField))); |
| expect(getIconStyle(tester, Icons.close)?.color, theme.colorScheme.error); |
| }); |
| |
| testWidgets('InputDecoration default floatingLabelStyle resolves hovered/focused states', (WidgetTester tester) async { |
| final FocusNode focusNode = FocusNode(); |
| addTearDown(focusNode.dispose); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: TextField( |
| focusNode: focusNode, |
| decoration: const InputDecoration( |
| labelText: 'label', |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Focused. |
| focusNode.requestFocus(); |
| await tester.pumpAndSettle(); |
| final ThemeData theme = Theme.of(tester.element(find.byType(TextField))); |
| expect(getLabelStyle(tester).color, theme.colorScheme.primary); |
| |
| // Hovered. |
| final Offset center = tester.getCenter(find.byType(TextField)); |
| final TestGesture gesture = await tester.createGesture( |
| kind: PointerDeviceKind.mouse, |
| ); |
| await gesture.addPointer(); |
| await gesture.moveTo(center); |
| await tester.pumpAndSettle(); |
| expect(getLabelStyle(tester).color, theme.colorScheme.onSurfaceVariant); |
| }); |
| |
| testWidgets('FloatingLabelAlignment.toString()', (WidgetTester tester) async { |
| expect(FloatingLabelAlignment.start.toString(), 'FloatingLabelAlignment.start'); |
| expect(FloatingLabelAlignment.center.toString(), 'FloatingLabelAlignment.center'); |
| }); |
| |
| testWidgets('InputDecorator.toString()', (WidgetTester tester) async { |
| const Widget child = InputDecorator( |
| key: Key('key'), |
| decoration: InputDecoration(), |
| baseStyle: TextStyle(), |
| textAlign: TextAlign.center, |
| child: Placeholder(), |
| ); |
| expect( |
| child.toString(), |
| "InputDecorator-[<'key'>](decoration: InputDecoration(), baseStyle: TextStyle(<all styles inherited>), isFocused: false, isEmpty: false)", |
| ); |
| }); |
| |
| testWidgets('InputDecorationTheme.inputDecoration', (WidgetTester tester) async { |
| const TextStyle themeStyle = TextStyle(color: Color(0xFF00FFFF)); |
| const Color themeColor = Color(0xFF00FF00); |
| const InputBorder themeInputBorder = OutlineInputBorder( |
| borderSide: BorderSide( |
| color: Color(0xFF0000FF), |
| ), |
| ); |
| const TextStyle decorationStyle = TextStyle(color: Color(0xFFFFFF00)); |
| const Color decorationColor = Color(0xFF0000FF); |
| const InputBorder decorationInputBorder = OutlineInputBorder( |
| borderSide: BorderSide( |
| color: Color(0xFFFF00FF), |
| ), |
| ); |
| |
| // InputDecorationTheme arguments define InputDecoration properties. |
| InputDecoration decoration = const InputDecoration().applyDefaults( |
| const InputDecorationTheme( |
| labelStyle: themeStyle, |
| floatingLabelStyle: themeStyle, |
| helperStyle: themeStyle, |
| helperMaxLines: 2, |
| hintStyle: themeStyle, |
| errorStyle: themeStyle, |
| errorMaxLines: 2, |
| floatingLabelBehavior: FloatingLabelBehavior.never, |
| floatingLabelAlignment: FloatingLabelAlignment.center, |
| isDense: true, |
| contentPadding: EdgeInsets.all(1.0), |
| iconColor: themeColor, |
| prefixStyle: themeStyle, |
| prefixIconColor: themeColor, |
| suffixStyle: themeStyle, |
| suffixIconColor: themeColor, |
| counterStyle: themeStyle, |
| filled: true, |
| fillColor: themeColor, |
| focusColor: themeColor, |
| hoverColor: themeColor, |
| errorBorder: themeInputBorder, |
| focusedBorder: themeInputBorder, |
| focusedErrorBorder: themeInputBorder, |
| disabledBorder: themeInputBorder, |
| enabledBorder: themeInputBorder, |
| border: InputBorder.none, |
| alignLabelWithHint: true, |
| constraints: BoxConstraints(minWidth: 10, maxWidth: 20, minHeight: 30, maxHeight: 40), |
| ), |
| ); |
| |
| expect(decoration.labelStyle, themeStyle); |
| expect(decoration.floatingLabelStyle, themeStyle); |
| expect(decoration.helperStyle, themeStyle); |
| expect(decoration.helperMaxLines, 2); |
| expect(decoration.hintStyle, themeStyle); |
| expect(decoration.errorStyle, themeStyle); |
| expect(decoration.errorMaxLines, 2); |
| expect(decoration.floatingLabelBehavior, FloatingLabelBehavior.never); |
| expect(decoration.floatingLabelAlignment, FloatingLabelAlignment.center); |
| expect(decoration.isDense, true); |
| expect(decoration.contentPadding, const EdgeInsets.all(1.0)); |
| expect(decoration.iconColor, themeColor); |
| expect(decoration.prefixStyle, themeStyle); |
| expect(decoration.prefixIconColor, themeColor); |
| expect(decoration.suffixStyle, themeStyle); |
| expect(decoration.suffixIconColor, themeColor); |
| expect(decoration.counterStyle, themeStyle); |
| expect(decoration.filled, true); |
| expect(decoration.fillColor, themeColor); |
| expect(decoration.focusColor, themeColor); |
| expect(decoration.hoverColor, themeColor); |
| expect(decoration.errorBorder, themeInputBorder); |
| expect(decoration.focusedBorder, themeInputBorder); |
| expect(decoration.focusedErrorBorder, themeInputBorder); |
| expect(decoration.disabledBorder, themeInputBorder); |
| expect(decoration.enabledBorder, themeInputBorder); |
| expect(decoration.border, InputBorder.none); |
| expect(decoration.alignLabelWithHint, true); |
| expect(decoration.constraints, const BoxConstraints(minWidth: 10, maxWidth: 20, minHeight: 30, maxHeight: 40)); |
| |
| // InputDecoration (baseDecoration) defines InputDecoration properties |
| decoration = const InputDecoration( |
| labelStyle: decorationStyle, |
| floatingLabelStyle: decorationStyle, |
| helperStyle: decorationStyle, |
| helperMaxLines: 3, |
| hintStyle: decorationStyle, |
| errorStyle: decorationStyle, |
| errorMaxLines: 3, |
| floatingLabelBehavior: FloatingLabelBehavior.always, |
| floatingLabelAlignment: FloatingLabelAlignment.start, |
| isDense: false, |
| contentPadding: EdgeInsets.all(4.0), |
| iconColor: decorationColor, |
| prefixStyle: decorationStyle, |
| prefixIconColor: decorationColor, |
| suffixStyle: decorationStyle, |
| suffixIconColor: decorationColor, |
| counterStyle: decorationStyle, |
| filled: false, |
| fillColor: decorationColor, |
| focusColor: decorationColor, |
| hoverColor: decorationColor, |
| errorBorder: decorationInputBorder, |
| focusedBorder: decorationInputBorder, |
| focusedErrorBorder: decorationInputBorder, |
| disabledBorder: decorationInputBorder, |
| enabledBorder: decorationInputBorder, |
| border: OutlineInputBorder(), |
| alignLabelWithHint: false, |
| constraints: BoxConstraints(minWidth: 40, maxWidth: 50, minHeight: 60, maxHeight: 70), |
| ).applyDefaults( |
| const InputDecorationTheme( |
| labelStyle: themeStyle, |
| floatingLabelStyle: themeStyle, |
| helperStyle: themeStyle, |
| helperMaxLines: 2, |
| hintStyle: themeStyle, |
| errorStyle: themeStyle, |
| errorMaxLines: 2, |
| floatingLabelBehavior: FloatingLabelBehavior.never, |
| floatingLabelAlignment: FloatingLabelAlignment.center, |
| isDense: true, |
| contentPadding: EdgeInsets.all(1.0), |
| iconColor: themeColor, |
| prefixStyle: themeStyle, |
| prefixIconColor: themeColor, |
| suffixStyle: themeStyle, |
| suffixIconColor: themeColor, |
| counterStyle: themeStyle, |
| filled: true, |
| fillColor: themeColor, |
| focusColor: themeColor, |
| hoverColor: themeColor, |
| errorBorder: themeInputBorder, |
| focusedBorder: themeInputBorder, |
| focusedErrorBorder: themeInputBorder, |
| disabledBorder: themeInputBorder, |
| enabledBorder: themeInputBorder, |
| border: InputBorder.none, |
| alignLabelWithHint: true, |
| constraints: BoxConstraints(minWidth: 10, maxWidth: 20, minHeight: 30, maxHeight: 40), |
| ), |
| ); |
| |
| expect(decoration.labelStyle, decorationStyle); |
| expect(decoration.floatingLabelStyle, decorationStyle); |
| expect(decoration.helperStyle, decorationStyle); |
| expect(decoration.helperMaxLines, 3); |
| expect(decoration.hintStyle, decorationStyle); |
| expect(decoration.errorStyle, decorationStyle); |
| expect(decoration.errorMaxLines, 3); |
| expect(decoration.floatingLabelBehavior, FloatingLabelBehavior.always); |
| expect(decoration.floatingLabelAlignment, FloatingLabelAlignment.start); |
| expect(decoration.isDense, false); |
| expect(decoration.contentPadding, const EdgeInsets.all(4.0)); |
| expect(decoration.iconColor, decorationColor); |
| expect(decoration.prefixStyle, decorationStyle); |
| expect(decoration.prefixIconColor, decorationColor); |
| expect(decoration.suffixStyle, decorationStyle); |
| expect(decoration.suffixIconColor, decorationColor); |
| expect(decoration.counterStyle, decorationStyle); |
| expect(decoration.filled, false); |
| expect(decoration.fillColor, decorationColor); |
| expect(decoration.focusColor, decorationColor); |
| expect(decoration.hoverColor, decorationColor); |
| expect(decoration.errorBorder, decorationInputBorder); |
| expect(decoration.focusedBorder, decorationInputBorder); |
| expect(decoration.focusedErrorBorder, decorationInputBorder); |
| expect(decoration.disabledBorder, decorationInputBorder); |
| expect(decoration.enabledBorder, decorationInputBorder); |
| expect(decoration.border, const OutlineInputBorder()); |
| expect(decoration.alignLabelWithHint, false); |
| expect(decoration.constraints, const BoxConstraints(minWidth: 40, maxWidth: 50, minHeight: 60, maxHeight: 70)); |
| }); |
| |
| testWidgets('InputDecorationTheme.inputDecoration with MaterialState', (WidgetTester tester) async { |
| final MaterialStateTextStyle themeStyle = MaterialStateTextStyle.resolveWith((Set<MaterialState> states) { |
| return const TextStyle(color: Colors.green); |
| }); |
| |
| final MaterialStateTextStyle decorationStyle = MaterialStateTextStyle.resolveWith((Set<MaterialState> states) { |
| return const TextStyle(color: Colors.blue); |
| }); |
| |
| // InputDecorationTheme arguments define InputDecoration properties. |
| InputDecoration decoration = const InputDecoration().applyDefaults( |
| InputDecorationTheme( |
| labelStyle: themeStyle, |
| helperStyle: themeStyle, |
| hintStyle: themeStyle, |
| errorStyle: themeStyle, |
| isDense: true, |
| contentPadding: const EdgeInsets.all(1.0), |
| prefixStyle: themeStyle, |
| suffixStyle: themeStyle, |
| counterStyle: themeStyle, |
| filled: true, |
| fillColor: Colors.red, |
| focusColor: Colors.blue, |
| border: InputBorder.none, |
| alignLabelWithHint: true, |
| constraints: const BoxConstraints(minWidth: 10, maxWidth: 20, minHeight: 30, maxHeight: 40), |
| ), |
| ); |
| |
| expect(decoration.labelStyle, themeStyle); |
| expect(decoration.helperStyle, themeStyle); |
| expect(decoration.hintStyle, themeStyle); |
| expect(decoration.errorStyle, themeStyle); |
| expect(decoration.isDense, true); |
| expect(decoration.contentPadding, const EdgeInsets.all(1.0)); |
| expect(decoration.prefixStyle, themeStyle); |
| expect(decoration.suffixStyle, themeStyle); |
| expect(decoration.counterStyle, themeStyle); |
| expect(decoration.filled, true); |
| expect(decoration.fillColor, Colors.red); |
| expect(decoration.border, InputBorder.none); |
| expect(decoration.alignLabelWithHint, true); |
| expect(decoration.constraints, const BoxConstraints(minWidth: 10, maxWidth: 20, minHeight: 30, maxHeight: 40)); |
| |
| // InputDecoration (baseDecoration) defines InputDecoration properties |
| final MaterialStateOutlineInputBorder border = MaterialStateOutlineInputBorder.resolveWith((Set<MaterialState> states) { |
| return const OutlineInputBorder(); |
| }); |
| decoration = InputDecoration( |
| labelStyle: decorationStyle, |
| helperStyle: decorationStyle, |
| hintStyle: decorationStyle, |
| errorStyle: decorationStyle, |
| isDense: false, |
| contentPadding: const EdgeInsets.all(4.0), |
| prefixStyle: decorationStyle, |
| suffixStyle: decorationStyle, |
| counterStyle: decorationStyle, |
| filled: false, |
| fillColor: Colors.blue, |
| border: border, |
| alignLabelWithHint: false, |
| constraints: const BoxConstraints(minWidth: 10, maxWidth: 20, minHeight: 30, maxHeight: 40), |
| ).applyDefaults( |
| InputDecorationTheme( |
| labelStyle: themeStyle, |
| helperStyle: themeStyle, |
| helperMaxLines: 5, |
| hintStyle: themeStyle, |
| errorStyle: themeStyle, |
| errorMaxLines: 4, |
| isDense: true, |
| contentPadding: const EdgeInsets.all(1.0), |
| prefixStyle: themeStyle, |
| suffixStyle: themeStyle, |
| counterStyle: themeStyle, |
| filled: true, |
| fillColor: Colors.red, |
| focusColor: Colors.blue, |
| border: InputBorder.none, |
| alignLabelWithHint: true, |
| constraints: const BoxConstraints(minWidth: 40, maxWidth: 30, minHeight: 20, maxHeight: 10), |
| ), |
| ); |
| |
| expect(decoration.labelStyle, decorationStyle); |
| expect(decoration.helperStyle, decorationStyle); |
| expect(decoration.helperMaxLines, 5); |
| expect(decoration.hintStyle, decorationStyle); |
| expect(decoration.errorStyle, decorationStyle); |
| expect(decoration.errorMaxLines, 4); |
| expect(decoration.isDense, false); |
| expect(decoration.contentPadding, const EdgeInsets.all(4.0)); |
| expect(decoration.prefixStyle, decorationStyle); |
| expect(decoration.suffixStyle, decorationStyle); |
| expect(decoration.counterStyle, decorationStyle); |
| expect(decoration.filled, false); |
| expect(decoration.fillColor, Colors.blue); |
| expect(decoration.border, isA<MaterialStateOutlineInputBorder>()); |
| expect(decoration.alignLabelWithHint, false); |
| expect(decoration.constraints, const BoxConstraints(minWidth: 10, maxWidth: 20, minHeight: 30, maxHeight: 40)); |
| }); |
| |
| testWidgets('InputDecorator constrained to 0x0', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/17710 |
| await tester.pumpWidget( |
| Material( |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: UnconstrainedBox(child: ConstrainedBox( |
| constraints: BoxConstraints.tight(Size.zero), |
| child: const InputDecorator( |
| decoration: InputDecoration( |
| labelText: 'XP', |
| border: OutlineInputBorder(), |
| ), |
| ), |
| )), |
| ), |
| ), |
| ); |
| }); |
| |
| testWidgets('InputDecorationTheme.toString()', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/19305 |
| expect( |
| const InputDecorationTheme( |
| contentPadding: EdgeInsetsDirectional.only(start: 5.0), |
| ).toString(), |
| contains('contentPadding: EdgeInsetsDirectional(5.0, 0.0, 0.0, 0.0)'), |
| ); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/20374 |
| expect( |
| const InputDecorationTheme( |
| contentPadding: EdgeInsets.only(left: 5.0), |
| ).toString(), |
| contains('contentPadding: EdgeInsets(5.0, 0.0, 0.0, 0.0)'), |
| ); |
| |
| // Verify that the toString() method succeeds. |
| final String debugString = const InputDecorationTheme( |
| labelStyle: TextStyle(height: 1.0), |
| helperStyle: TextStyle(height: 2.0), |
| helperMaxLines: 5, |
| hintStyle: TextStyle(height: 3.0), |
| errorStyle: TextStyle(height: 4.0), |
| errorMaxLines: 5, |
| isDense: true, |
| contentPadding: EdgeInsets.only(right: 6.0), |
| isCollapsed: true, |
| prefixStyle: TextStyle(height: 7.0), |
| suffixStyle: TextStyle(height: 8.0), |
| counterStyle: TextStyle(height: 9.0), |
| filled: true, |
| fillColor: Color(0x00000010), |
| focusColor: Color(0x00000020), |
| errorBorder: UnderlineInputBorder(), |
| focusedBorder: OutlineInputBorder(), |
| focusedErrorBorder: UnderlineInputBorder(), |
| disabledBorder: OutlineInputBorder(), |
| enabledBorder: UnderlineInputBorder(), |
| border: OutlineInputBorder(), |
| ).toString(); |
| |
| // Spot check |
| expect(debugString, contains('labelStyle: TextStyle(inherit: true, height: 1.0x)')); |
| expect(debugString, contains('isDense: true')); |
| expect(debugString, contains('fillColor: Color(0x00000010)')); |
| expect(debugString, contains('focusColor: Color(0x00000020)')); |
| expect(debugString, contains('errorBorder: UnderlineInputBorder()')); |
| expect(debugString, contains('focusedBorder: OutlineInputBorder()')); |
| }); |
| |
| testWidgets('InputDecorationTheme implements debugFillDescription', (WidgetTester tester) async { |
|