blob: 2f5405592d7029387996809eef4de9d38dc967f3 [file] [log] [blame] [edit]
// 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 {