4901 lines
170 KiB
Dart
4901 lines
170 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
// no-shuffle:
|
|
// //TODO(gspencergoog): Remove this tag once this test's state leaks/test
|
|
// dependencies have been fixed.
|
|
// https://github.com/flutter/flutter/issues/85160
|
|
// Fails with "flutter test --test-randomize-ordering-seed=456"
|
|
// reduced-test-set:
|
|
// This file is run as part of a reduced test set in CI on Mac and Windows
|
|
// machines.
|
|
@Tags(<String>['reduced-test-set', 'no-shuffle'])
|
|
library;
|
|
|
|
import 'dart:math' as math;
|
|
import 'dart:ui';
|
|
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import '../widgets/feedback_tester.dart';
|
|
import '../widgets/semantics_tester.dart';
|
|
|
|
const List<String> menuItems = <String>['one', 'two', 'three', 'four'];
|
|
void onChanged<T>(T _) {}
|
|
|
|
final Type dropdownButtonType = DropdownButton<String>(
|
|
onChanged: (_) {},
|
|
items: const <DropdownMenuItem<String>>[],
|
|
).runtimeType;
|
|
|
|
Finder _iconRichText(Key iconKey) {
|
|
return find.descendant(of: find.byKey(iconKey), matching: find.byType(RichText));
|
|
}
|
|
|
|
Widget buildDropdown({
|
|
required bool isFormField,
|
|
Key? buttonKey,
|
|
String? initialValue = 'two',
|
|
ValueChanged<String?>? onChanged,
|
|
VoidCallback? onTap,
|
|
Widget? icon,
|
|
Color? iconDisabledColor,
|
|
Color? iconEnabledColor,
|
|
double iconSize = 24.0,
|
|
bool isDense = false,
|
|
bool isExpanded = false,
|
|
Widget? hint,
|
|
Widget? disabledHint,
|
|
Widget? underline,
|
|
List<String>? items = menuItems,
|
|
List<Widget> Function(BuildContext)? selectedItemBuilder,
|
|
double? itemHeight = kMinInteractiveDimension,
|
|
double? menuWidth,
|
|
AlignmentDirectional alignment = AlignmentDirectional.centerStart,
|
|
TextDirection textDirection = TextDirection.ltr,
|
|
Size? mediaSize,
|
|
FocusNode? focusNode,
|
|
bool autofocus = false,
|
|
Color? focusColor,
|
|
Color? dropdownColor,
|
|
double? menuMaxHeight,
|
|
EdgeInsetsGeometry? padding,
|
|
InputDecoration? decoration,
|
|
}) {
|
|
final List<DropdownMenuItem<String>>? listItems = items?.map<DropdownMenuItem<String>>((
|
|
String item,
|
|
) {
|
|
return DropdownMenuItem<String>(
|
|
key: ValueKey<String>(item),
|
|
value: item,
|
|
child: Text(item, key: ValueKey<String>('${item}Text')),
|
|
);
|
|
}).toList();
|
|
|
|
if (isFormField) {
|
|
return Form(
|
|
child: DropdownButtonFormField<String>(
|
|
key: buttonKey,
|
|
initialValue: initialValue,
|
|
hint: hint,
|
|
disabledHint: disabledHint,
|
|
onChanged: onChanged,
|
|
onTap: onTap,
|
|
icon: icon,
|
|
iconSize: iconSize,
|
|
iconDisabledColor: iconDisabledColor,
|
|
iconEnabledColor: iconEnabledColor,
|
|
isDense: isDense,
|
|
isExpanded: isExpanded,
|
|
// No underline attribute
|
|
focusNode: focusNode,
|
|
autofocus: autofocus,
|
|
focusColor: focusColor,
|
|
dropdownColor: dropdownColor,
|
|
items: listItems,
|
|
selectedItemBuilder: selectedItemBuilder,
|
|
itemHeight: itemHeight,
|
|
alignment: alignment,
|
|
menuMaxHeight: menuMaxHeight,
|
|
padding: padding,
|
|
decoration: decoration,
|
|
),
|
|
);
|
|
}
|
|
return DropdownButton<String>(
|
|
key: buttonKey,
|
|
value: initialValue,
|
|
hint: hint,
|
|
disabledHint: disabledHint,
|
|
onChanged: onChanged,
|
|
onTap: onTap,
|
|
icon: icon,
|
|
iconSize: iconSize,
|
|
iconDisabledColor: iconDisabledColor,
|
|
iconEnabledColor: iconEnabledColor,
|
|
isDense: isDense,
|
|
isExpanded: isExpanded,
|
|
underline: underline,
|
|
focusNode: focusNode,
|
|
autofocus: autofocus,
|
|
focusColor: focusColor,
|
|
dropdownColor: dropdownColor,
|
|
items: listItems,
|
|
selectedItemBuilder: selectedItemBuilder,
|
|
itemHeight: itemHeight,
|
|
menuWidth: menuWidth,
|
|
alignment: alignment,
|
|
menuMaxHeight: menuMaxHeight,
|
|
padding: padding,
|
|
);
|
|
}
|
|
|
|
Widget buildFrame({
|
|
Key? buttonKey,
|
|
String? initialValue = 'two',
|
|
ValueChanged<String?>? onChanged,
|
|
VoidCallback? onTap,
|
|
Widget? icon,
|
|
Color? iconDisabledColor,
|
|
Color? iconEnabledColor,
|
|
double iconSize = 24.0,
|
|
bool isDense = false,
|
|
bool isExpanded = false,
|
|
Widget? hint,
|
|
Widget? disabledHint,
|
|
Widget? underline,
|
|
List<String>? items = menuItems,
|
|
List<Widget> Function(BuildContext)? selectedItemBuilder,
|
|
double? itemHeight = kMinInteractiveDimension,
|
|
double? menuWidth,
|
|
AlignmentDirectional alignment = AlignmentDirectional.centerStart,
|
|
TextDirection textDirection = TextDirection.ltr,
|
|
Size? mediaSize,
|
|
FocusNode? focusNode,
|
|
bool autofocus = false,
|
|
Color? focusColor,
|
|
Color? dropdownColor,
|
|
bool isFormField = false,
|
|
double? menuMaxHeight,
|
|
EdgeInsetsGeometry? padding,
|
|
Alignment dropdownAlignment = Alignment.center,
|
|
bool? useMaterial3,
|
|
InputDecoration? decoration,
|
|
InputDecorationThemeData? localInputDecorationTheme,
|
|
}) {
|
|
return Theme(
|
|
data: ThemeData(useMaterial3: useMaterial3),
|
|
child: TestApp(
|
|
textDirection: textDirection,
|
|
mediaSize: mediaSize,
|
|
child: Material(
|
|
child: Align(
|
|
alignment: dropdownAlignment,
|
|
child: RepaintBoundary(
|
|
child: InputDecorationTheme(
|
|
data: localInputDecorationTheme,
|
|
child: buildDropdown(
|
|
isFormField: isFormField,
|
|
buttonKey: buttonKey,
|
|
initialValue: initialValue,
|
|
hint: hint,
|
|
disabledHint: disabledHint,
|
|
onChanged: onChanged,
|
|
onTap: onTap,
|
|
icon: icon,
|
|
iconSize: iconSize,
|
|
iconDisabledColor: iconDisabledColor,
|
|
iconEnabledColor: iconEnabledColor,
|
|
isDense: isDense,
|
|
isExpanded: isExpanded,
|
|
underline: underline,
|
|
focusNode: focusNode,
|
|
autofocus: autofocus,
|
|
focusColor: focusColor,
|
|
dropdownColor: dropdownColor,
|
|
items: items,
|
|
selectedItemBuilder: selectedItemBuilder,
|
|
itemHeight: itemHeight,
|
|
menuWidth: menuWidth,
|
|
alignment: alignment,
|
|
menuMaxHeight: menuMaxHeight,
|
|
padding: padding,
|
|
decoration: decoration,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget buildDropdownWithHint({
|
|
required AlignmentDirectional alignment,
|
|
required bool isExpanded,
|
|
bool enableSelectedItemBuilder = false,
|
|
}) {
|
|
return buildFrame(
|
|
useMaterial3: false,
|
|
mediaSize: const Size(800, 600),
|
|
itemHeight: 100.0,
|
|
alignment: alignment,
|
|
isExpanded: isExpanded,
|
|
selectedItemBuilder: enableSelectedItemBuilder
|
|
? (BuildContext context) {
|
|
return menuItems.map<Widget>((String item) {
|
|
return ColoredBox(color: const Color(0xff00ff00), child: Text(item));
|
|
}).toList();
|
|
}
|
|
: null,
|
|
hint: const Text('hint'),
|
|
);
|
|
}
|
|
|
|
class TestApp extends StatefulWidget {
|
|
const TestApp({super.key, required this.textDirection, required this.child, this.mediaSize});
|
|
|
|
final TextDirection textDirection;
|
|
final Widget child;
|
|
final Size? mediaSize;
|
|
|
|
@override
|
|
State<TestApp> createState() => _TestAppState();
|
|
}
|
|
|
|
class _TestAppState extends State<TestApp> {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Localizations(
|
|
locale: const Locale('en', 'US'),
|
|
delegates: const <LocalizationsDelegate<dynamic>>[
|
|
DefaultWidgetsLocalizations.delegate,
|
|
DefaultMaterialLocalizations.delegate,
|
|
],
|
|
child: MediaQuery(
|
|
data: MediaQueryData.fromView(View.of(context)).copyWith(size: widget.mediaSize),
|
|
child: Directionality(
|
|
textDirection: widget.textDirection,
|
|
child: Navigator(
|
|
onGenerateRoute: (RouteSettings settings) {
|
|
assert(settings.name == '/');
|
|
return MaterialPageRoute<void>(
|
|
settings: settings,
|
|
builder: (BuildContext context) => widget.child,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// When the dropdown's menu is popped up, a RenderParagraph for the selected
|
|
// menu's text item will appear both in the dropdown button and in the menu.
|
|
// The RenderParagraphs should be aligned, i.e. they should have the same
|
|
// size and location.
|
|
void checkSelectedItemTextGeometry(WidgetTester tester, String value) {
|
|
final List<RenderBox> boxes = tester
|
|
.renderObjectList<RenderBox>(find.byKey(ValueKey<String>('${value}Text')))
|
|
.toList();
|
|
expect(boxes.length, equals(2));
|
|
final RenderBox box0 = boxes[0];
|
|
final RenderBox box1 = boxes[1];
|
|
expect(box0.localToGlobal(Offset.zero), equals(box1.localToGlobal(Offset.zero)));
|
|
expect(box0.size, equals(box1.size));
|
|
}
|
|
|
|
// The dropdown menu isn't readily accessible. To find it we're assuming that it
|
|
// contains a ListView and that it's an instance of _DropdownMenu.
|
|
Rect getMenuRect(WidgetTester tester) {
|
|
late Rect menuRect;
|
|
tester.element(find.byType(ListView)).visitAncestorElements((Element element) {
|
|
if (element.toString().startsWith('_DropdownMenu')) {
|
|
final box = element.findRenderObject()! as RenderBox;
|
|
menuRect = box.localToGlobal(Offset.zero) & box.size;
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
return menuRect;
|
|
}
|
|
|
|
Future<void> checkDropdownColor(
|
|
WidgetTester tester, {
|
|
Color? color,
|
|
bool isFormField = false,
|
|
}) async {
|
|
const text = 'foo';
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(useMaterial3: false),
|
|
home: Material(
|
|
child: isFormField
|
|
? Form(
|
|
child: DropdownButtonFormField<String>(
|
|
dropdownColor: color,
|
|
initialValue: text,
|
|
items: const <DropdownMenuItem<String>>[
|
|
DropdownMenuItem<String>(value: text, child: Text(text)),
|
|
],
|
|
onChanged: (_) {},
|
|
),
|
|
)
|
|
: DropdownButton<String>(
|
|
dropdownColor: color,
|
|
value: text,
|
|
items: const <DropdownMenuItem<String>>[
|
|
DropdownMenuItem<String>(value: text, child: Text(text)),
|
|
],
|
|
onChanged: (_) {},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.tap(find.text(text));
|
|
await tester.pump();
|
|
|
|
expect(
|
|
find.ancestor(of: find.text(text).last, matching: find.byType(CustomPaint)).at(2),
|
|
paints
|
|
..save()
|
|
..rrect()
|
|
..rrect()
|
|
..rrect()
|
|
..rrect(color: color ?? Colors.grey[50], hasMaskFilter: false),
|
|
);
|
|
}
|
|
|
|
void main() {
|
|
testWidgets('Default dropdown golden', (WidgetTester tester) async {
|
|
final Key buttonKey = UniqueKey();
|
|
Widget build() => buildFrame(buttonKey: buttonKey, onChanged: onChanged, useMaterial3: false);
|
|
await tester.pumpWidget(build());
|
|
final Finder buttonFinder = find.byKey(buttonKey);
|
|
assert(tester.renderObject(buttonFinder).attached);
|
|
await expectLater(
|
|
find.ancestor(of: buttonFinder, matching: find.byType(RepaintBoundary)).first,
|
|
matchesGoldenFile('dropdown_test.default.png'),
|
|
);
|
|
});
|
|
|
|
testWidgets('Expanded dropdown golden', (WidgetTester tester) async {
|
|
final Key buttonKey = UniqueKey();
|
|
Widget build() => buildFrame(
|
|
buttonKey: buttonKey,
|
|
isExpanded: true,
|
|
onChanged: onChanged,
|
|
useMaterial3: false,
|
|
);
|
|
await tester.pumpWidget(build());
|
|
final Finder buttonFinder = find.byKey(buttonKey);
|
|
assert(tester.renderObject(buttonFinder).attached);
|
|
await expectLater(
|
|
find.ancestor(of: buttonFinder, matching: find.byType(RepaintBoundary)).first,
|
|
matchesGoldenFile('dropdown_test.expanded.png'),
|
|
);
|
|
});
|
|
|
|
testWidgets('Dropdown button control test', (WidgetTester tester) async {
|
|
String? value = 'one';
|
|
void didChangeValue(String? newValue) {
|
|
value = newValue;
|
|
}
|
|
|
|
Widget build() => buildFrame(initialValue: value, onChanged: didChangeValue);
|
|
|
|
await tester.pumpWidget(build());
|
|
|
|
await tester.tap(find.text('one'));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
|
|
|
|
expect(value, equals('one'));
|
|
|
|
await tester.tap(find.text('three').last);
|
|
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
|
|
|
|
expect(value, equals('three'));
|
|
|
|
await tester.tap(find.text('three', skipOffstage: false), warnIfMissed: false);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
|
|
|
|
expect(value, equals('three'));
|
|
|
|
await tester.pumpWidget(build());
|
|
|
|
await tester.tap(find.text('two').last);
|
|
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
|
|
|
|
expect(value, equals('two'));
|
|
});
|
|
|
|
testWidgets('Dropdown button with no app', (WidgetTester tester) async {
|
|
String? value = 'one';
|
|
void didChangeValue(String? newValue) {
|
|
value = newValue;
|
|
}
|
|
|
|
Widget build() {
|
|
return Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: MediaQueryData.fromView(tester.view),
|
|
child: Navigator(
|
|
initialRoute: '/',
|
|
onGenerateRoute: (RouteSettings settings) {
|
|
return MaterialPageRoute<void>(
|
|
settings: settings,
|
|
builder: (BuildContext context) {
|
|
return Material(
|
|
child: buildFrame(initialValue: 'one', onChanged: didChangeValue),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(build());
|
|
|
|
await tester.tap(find.text('one'));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
|
|
|
|
expect(value, equals('one'));
|
|
|
|
await tester.tap(find.text('three').last);
|
|
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
|
|
|
|
expect(value, equals('three'));
|
|
|
|
await tester.tap(find.text('three', skipOffstage: false), warnIfMissed: false);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
|
|
|
|
expect(value, equals('three'));
|
|
|
|
await tester.pumpWidget(build());
|
|
|
|
await tester.tap(find.text('two').last);
|
|
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
|
|
|
|
expect(value, equals('two'));
|
|
});
|
|
|
|
testWidgets('DropdownButton does not allow duplicate item values', (WidgetTester tester) async {
|
|
final List<DropdownMenuItem<String>> itemsWithDuplicateValues = <String>['a', 'b', 'c', 'c']
|
|
.map<DropdownMenuItem<String>>((String value) {
|
|
return DropdownMenuItem<String>(value: value, child: Text(value));
|
|
})
|
|
.toList();
|
|
|
|
await expectLater(
|
|
() => tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: DropdownButton<String>(
|
|
value: 'c',
|
|
onChanged: (String? newValue) {},
|
|
items: itemsWithDuplicateValues,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
throwsA(
|
|
isAssertionError.having(
|
|
(AssertionError error) => error.toString(),
|
|
'.toString()',
|
|
contains("There should be exactly one item with [DropdownButton]'s value"),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('DropdownButton value should only appear in one menu item', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final List<DropdownMenuItem<String>> itemsWithDuplicateValues = <String>['a', 'b', 'c', 'd']
|
|
.map<DropdownMenuItem<String>>((String value) {
|
|
return DropdownMenuItem<String>(value: value, child: Text(value));
|
|
})
|
|
.toList();
|
|
|
|
await expectLater(
|
|
() => tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: DropdownButton<String>(
|
|
value: 'e',
|
|
onChanged: (String? newValue) {},
|
|
items: itemsWithDuplicateValues,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
throwsA(
|
|
isAssertionError.having(
|
|
(AssertionError error) => error.toString(),
|
|
'.toString()',
|
|
contains("There should be exactly one item with [DropdownButton]'s value"),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('Dropdown form field uses form field state', (WidgetTester tester) async {
|
|
final Key buttonKey = UniqueKey();
|
|
final formKey = GlobalKey<FormState>();
|
|
String? value;
|
|
await tester.pumpWidget(
|
|
StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return MaterialApp(
|
|
home: Material(
|
|
child: Form(
|
|
key: formKey,
|
|
child: DropdownButtonFormField<String>(
|
|
key: buttonKey,
|
|
initialValue: value,
|
|
hint: const Text('Select Value'),
|
|
decoration: const InputDecoration(prefixIcon: Icon(Icons.fastfood)),
|
|
items: menuItems.map((String val) {
|
|
return DropdownMenuItem<String>(value: val, child: Text(val));
|
|
}).toList(),
|
|
validator: (String? v) => v == null ? 'Must select value' : null,
|
|
onChanged: (String? newValue) {},
|
|
onSaved: (String? v) {
|
|
setState(() {
|
|
value = v;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
int getIndex() {
|
|
final stack = tester.element(find.byType(IndexedStack)).widget as IndexedStack;
|
|
return stack.index!;
|
|
}
|
|
|
|
// Initial value of null displays hint
|
|
expect(value, equals(null));
|
|
expect(getIndex(), 4);
|
|
await tester.tap(find.text('Select Value', skipOffstage: false), warnIfMissed: false);
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('three').last);
|
|
await tester.pumpAndSettle();
|
|
expect(getIndex(), 2);
|
|
// Changes only made to FormField state until form saved
|
|
expect(value, equals(null));
|
|
final FormState form = formKey.currentState!;
|
|
form.save();
|
|
expect(value, equals('three'));
|
|
});
|
|
|
|
testWidgets(
|
|
'Dropdown form field only uses initialValue parameter when first built and when reset',
|
|
(WidgetTester tester) async {
|
|
final fieldKey = GlobalKey<FormFieldState<String>>();
|
|
await tester.pumpWidget(
|
|
StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return MaterialApp(
|
|
home: Material(
|
|
child: DropdownButtonFormField<String>(
|
|
key: fieldKey,
|
|
initialValue: 'one',
|
|
hint: const Text('Select Value'),
|
|
items: menuItems.map((String val) {
|
|
return DropdownMenuItem<String>(value: val, child: Text(val));
|
|
}).toList(),
|
|
onChanged: (String? newValue) {
|
|
setState(() {
|
|
// Do nothing, just to trigger a rebuild.
|
|
});
|
|
},
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
expect(fieldKey.currentState!.value, 'one');
|
|
|
|
// Open the dropdown menu.
|
|
await tester.tap(find.text('one'));
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('three').last);
|
|
await tester.pumpAndSettle();
|
|
|
|
// The value should update to selected, not the initial value.
|
|
expect(find.text('three'), findsOneWidget);
|
|
expect(fieldKey.currentState!.value, 'three');
|
|
|
|
fieldKey.currentState!.reset();
|
|
await tester.pump();
|
|
|
|
// Reset to the initial value.
|
|
expect(find.text('one'), findsOneWidget);
|
|
expect(fieldKey.currentState!.value, 'one');
|
|
},
|
|
);
|
|
|
|
testWidgets('Dropdown in ListView', (WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/12053
|
|
// Positions a DropdownButton at the left and right edges of the screen,
|
|
// forcing it to be sized down to the viewport width
|
|
const value = 'foo';
|
|
final itemKey = UniqueKey();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: ListView(
|
|
children: <Widget>[
|
|
DropdownButton<String>(
|
|
value: value,
|
|
items: <DropdownMenuItem<String>>[
|
|
DropdownMenuItem<String>(key: itemKey, value: value, child: const Text(value)),
|
|
],
|
|
onChanged: (_) {},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.tap(find.text(value));
|
|
await tester.pump();
|
|
final List<RenderBox> itemBoxes = tester
|
|
.renderObjectList<RenderBox>(find.byKey(itemKey))
|
|
.toList();
|
|
expect(itemBoxes[0].localToGlobal(Offset.zero).dx, equals(0.0));
|
|
expect(itemBoxes[1].localToGlobal(Offset.zero).dx, equals(16.0));
|
|
expect(itemBoxes[1].size.width, equals(800.0 - 16.0 * 2));
|
|
});
|
|
|
|
testWidgets('Dropdown menu can position correctly inside a nested navigator', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/66870
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(useMaterial3: false),
|
|
home: Scaffold(
|
|
appBar: AppBar(),
|
|
body: Column(
|
|
children: <Widget>[
|
|
ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 500, maxHeight: 200),
|
|
child: Navigator(
|
|
onGenerateRoute: (RouteSettings s) {
|
|
return MaterialPageRoute<void>(
|
|
builder: (BuildContext context) {
|
|
return Center(
|
|
child: DropdownButton<int>(
|
|
value: 1,
|
|
items: const <DropdownMenuItem<int>>[
|
|
DropdownMenuItem<int>(value: 1, child: Text('First Item')),
|
|
DropdownMenuItem<int>(value: 2, child: Text('Second Item')),
|
|
],
|
|
onChanged: (_) {},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.tap(find.text('First Item'));
|
|
await tester.pump();
|
|
final RenderBox secondItem = tester
|
|
.renderObjectList<RenderBox>(find.text('Second Item', skipOffstage: false))
|
|
.toList()[1];
|
|
expect(secondItem.localToGlobal(Offset.zero).dx, equals(150.0));
|
|
expect(secondItem.localToGlobal(Offset.zero).dy, equals(176.0));
|
|
});
|
|
|
|
testWidgets('Dropdown screen edges', (WidgetTester tester) async {
|
|
int? value = 4;
|
|
final items = <DropdownMenuItem<int>>[
|
|
for (int i = 0; i < 20; ++i) DropdownMenuItem<int>(value: i, child: Text('$i')),
|
|
];
|
|
|
|
void handleChanged(int? newValue) {
|
|
value = newValue;
|
|
}
|
|
|
|
final button = DropdownButton<int>(value: value, onChanged: handleChanged, items: items);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: Align(alignment: Alignment.topCenter, child: button),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('4'));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
|
|
|
|
// We should have two copies of item 5, one in the menu and one in the
|
|
// button itself.
|
|
expect(tester.elementList(find.text('5', skipOffstage: false)), hasLength(2));
|
|
|
|
expect(value, 4);
|
|
await tester.tap(find.byWidget(button, skipOffstage: false), warnIfMissed: false);
|
|
expect(value, 4);
|
|
// this waits for the route's completer to complete, which calls handleChanged
|
|
await tester.idle();
|
|
expect(value, 4);
|
|
});
|
|
|
|
for (final TextDirection textDirection in TextDirection.values) {
|
|
testWidgets('Dropdown button aligns selected menu item ($textDirection)', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final Key buttonKey = UniqueKey();
|
|
|
|
Widget build() => buildFrame(
|
|
buttonKey: buttonKey,
|
|
textDirection: textDirection,
|
|
onChanged: onChanged,
|
|
useMaterial3: false,
|
|
);
|
|
|
|
await tester.pumpWidget(build());
|
|
final RenderBox buttonBox = tester.renderObject<RenderBox>(find.byKey(buttonKey));
|
|
assert(buttonBox.attached);
|
|
final Offset buttonOriginBeforeTap = buttonBox.localToGlobal(Offset.zero);
|
|
|
|
await tester.tap(find.text('two'));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
|
|
|
|
// Tapping the dropdown button should not cause it to move.
|
|
expect(buttonBox.localToGlobal(Offset.zero), equals(buttonOriginBeforeTap));
|
|
|
|
// The selected dropdown item is both in menu we just popped up, and in
|
|
// the IndexedStack contained by the dropdown button. Both of them should
|
|
// have the same origin and height as the dropdown button.
|
|
final List<RenderBox> itemBoxes = tester
|
|
.renderObjectList<RenderBox>(find.byKey(const ValueKey<String>('two')))
|
|
.toList();
|
|
expect(itemBoxes.length, equals(2));
|
|
for (final itemBox in itemBoxes) {
|
|
assert(itemBox.attached);
|
|
switch (textDirection) {
|
|
case TextDirection.rtl:
|
|
expect(
|
|
buttonBox.localToGlobal(buttonBox.size.bottomRight(Offset.zero)),
|
|
equals(itemBox.localToGlobal(itemBox.size.bottomRight(Offset.zero))),
|
|
);
|
|
case TextDirection.ltr:
|
|
expect(
|
|
buttonBox.localToGlobal(Offset.zero),
|
|
equals(itemBox.localToGlobal(Offset.zero)),
|
|
);
|
|
}
|
|
expect(buttonBox.size.height, equals(itemBox.size.height));
|
|
}
|
|
|
|
// The two RenderParagraph objects, for the 'two' items' Text children,
|
|
// should have the same size and location.
|
|
checkSelectedItemTextGeometry(tester, 'two');
|
|
|
|
await tester.pumpWidget(Container()); // reset test
|
|
});
|
|
}
|
|
|
|
testWidgets('Arrow icon aligns with the edge of button when expanded', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final Key buttonKey = UniqueKey();
|
|
|
|
Widget build() => buildFrame(buttonKey: buttonKey, isExpanded: true, onChanged: onChanged);
|
|
|
|
await tester.pumpWidget(build());
|
|
final RenderBox buttonBox = tester.renderObject<RenderBox>(find.byKey(buttonKey));
|
|
assert(buttonBox.attached);
|
|
|
|
final RenderBox arrowIcon = tester.renderObject<RenderBox>(find.byIcon(Icons.arrow_drop_down));
|
|
assert(arrowIcon.attached);
|
|
|
|
// Arrow icon should be aligned with far right of button when expanded
|
|
expect(
|
|
arrowIcon.localToGlobal(Offset.zero).dx,
|
|
buttonBox.size.centerRight(Offset(-arrowIcon.size.width, 0.0)).dx,
|
|
);
|
|
});
|
|
|
|
testWidgets('Dropdown button icon will accept widgets as icons', (WidgetTester tester) async {
|
|
final Widget customWidget = Container(
|
|
decoration: ShapeDecoration(
|
|
shape: CircleBorder(side: BorderSide(width: 5.0, color: Colors.grey.shade700)),
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(buildFrame(icon: customWidget, onChanged: onChanged));
|
|
|
|
expect(find.byWidget(customWidget), findsOneWidget);
|
|
expect(find.byIcon(Icons.arrow_drop_down), findsNothing);
|
|
|
|
await tester.pumpWidget(buildFrame(icon: const Icon(Icons.assessment), onChanged: onChanged));
|
|
|
|
expect(find.byIcon(Icons.assessment), findsOneWidget);
|
|
expect(find.byIcon(Icons.arrow_drop_down), findsNothing);
|
|
});
|
|
|
|
testWidgets('Dropdown button icon should have default size and colors when not defined', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final Key iconKey = UniqueKey();
|
|
final customIcon = Icon(Icons.assessment, key: iconKey);
|
|
|
|
await tester.pumpWidget(buildFrame(icon: customIcon, onChanged: onChanged));
|
|
|
|
// test for size
|
|
final RenderBox icon = tester.renderObject(find.byKey(iconKey));
|
|
expect(icon.size, const Size(24.0, 24.0));
|
|
|
|
// test for enabled color
|
|
final RichText enabledRichText = tester.widget<RichText>(_iconRichText(iconKey));
|
|
expect(enabledRichText.text.style!.color, Colors.grey.shade700);
|
|
|
|
// test for disabled color
|
|
await tester.pumpWidget(buildFrame(icon: customIcon));
|
|
|
|
final RichText disabledRichText = tester.widget<RichText>(_iconRichText(iconKey));
|
|
expect(disabledRichText.text.style!.color, Colors.grey.shade400);
|
|
});
|
|
|
|
testWidgets('Dropdown button icon should have the passed in size and color instead of defaults', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final Key iconKey = UniqueKey();
|
|
final customIcon = Icon(Icons.assessment, key: iconKey);
|
|
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
icon: customIcon,
|
|
iconSize: 30.0,
|
|
iconEnabledColor: Colors.pink,
|
|
iconDisabledColor: Colors.orange,
|
|
onChanged: onChanged,
|
|
),
|
|
);
|
|
|
|
// test for size
|
|
final RenderBox icon = tester.renderObject(find.byKey(iconKey));
|
|
expect(icon.size, const Size(30.0, 30.0));
|
|
|
|
// test for enabled color
|
|
final RichText enabledRichText = tester.widget<RichText>(_iconRichText(iconKey));
|
|
expect(enabledRichText.text.style!.color, Colors.pink);
|
|
|
|
// test for disabled color
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
icon: customIcon,
|
|
iconSize: 30.0,
|
|
iconEnabledColor: Colors.pink,
|
|
iconDisabledColor: Colors.orange,
|
|
),
|
|
);
|
|
|
|
final RichText disabledRichText = tester.widget<RichText>(_iconRichText(iconKey));
|
|
expect(disabledRichText.text.style!.color, Colors.orange);
|
|
});
|
|
|
|
testWidgets(
|
|
'Dropdown button should use its own size and color properties over those defined by the theme',
|
|
(WidgetTester tester) async {
|
|
final Key iconKey = UniqueKey();
|
|
|
|
final customIcon = Icon(Icons.assessment, key: iconKey, size: 40.0, color: Colors.yellow);
|
|
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
icon: customIcon,
|
|
iconSize: 30.0,
|
|
iconEnabledColor: Colors.pink,
|
|
iconDisabledColor: Colors.orange,
|
|
onChanged: onChanged,
|
|
),
|
|
);
|
|
|
|
// test for size
|
|
final RenderBox icon = tester.renderObject(find.byKey(iconKey));
|
|
expect(icon.size, const Size(40.0, 40.0));
|
|
|
|
// test for enabled color
|
|
final RichText enabledRichText = tester.widget<RichText>(_iconRichText(iconKey));
|
|
expect(enabledRichText.text.style!.color, Colors.yellow);
|
|
|
|
// test for disabled color
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
icon: customIcon,
|
|
iconSize: 30.0,
|
|
iconEnabledColor: Colors.pink,
|
|
iconDisabledColor: Colors.orange,
|
|
),
|
|
);
|
|
|
|
final RichText disabledRichText = tester.widget<RichText>(_iconRichText(iconKey));
|
|
expect(disabledRichText.text.style!.color, Colors.yellow);
|
|
},
|
|
);
|
|
|
|
testWidgets('Dropdown button with isDense:true aligns selected menu item', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final Key buttonKey = UniqueKey();
|
|
|
|
Widget build() => buildFrame(buttonKey: buttonKey, isDense: true, onChanged: onChanged);
|
|
|
|
await tester.pumpWidget(build());
|
|
final RenderBox buttonBox = tester.renderObject<RenderBox>(find.byKey(buttonKey));
|
|
assert(buttonBox.attached);
|
|
|
|
await tester.tap(find.text('two'));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
|
|
|
|
// The selected dropdown item is both in menu we just popped up, and in
|
|
// the IndexedStack contained by the dropdown button. Both of them should
|
|
// have the same vertical center as the button.
|
|
final List<RenderBox> itemBoxes = tester
|
|
.renderObjectList<RenderBox>(find.byKey(const ValueKey<String>('two')))
|
|
.toList();
|
|
expect(itemBoxes.length, equals(2));
|
|
|
|
// When isDense is true, the button's height is reduced. The menu items'
|
|
// heights are not.
|
|
final double menuItemHeight = itemBoxes
|
|
.map<double>((RenderBox box) => box.size.height)
|
|
.reduce(math.max);
|
|
expect(menuItemHeight, greaterThan(buttonBox.size.height));
|
|
|
|
for (final itemBox in itemBoxes) {
|
|
assert(itemBox.attached);
|
|
final Offset buttonBoxCenter = buttonBox.size.center(buttonBox.localToGlobal(Offset.zero));
|
|
final Offset itemBoxCenter = itemBox.size.center(itemBox.localToGlobal(Offset.zero));
|
|
expect(buttonBoxCenter.dy, equals(itemBoxCenter.dy));
|
|
}
|
|
|
|
// The two RenderParagraph objects, for the 'two' items' Text children,
|
|
// should have the same size and location.
|
|
checkSelectedItemTextGeometry(tester, 'two');
|
|
});
|
|
|
|
testWidgets('Dropdown button can have a text style with no fontSize specified', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/33425
|
|
const value = 'foo';
|
|
final itemKey = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
TestApp(
|
|
textDirection: TextDirection.ltr,
|
|
child: Material(
|
|
child: DropdownButton<String>(
|
|
value: value,
|
|
items: <DropdownMenuItem<String>>[
|
|
DropdownMenuItem<String>(key: itemKey, value: 'foo', child: const Text(value)),
|
|
],
|
|
isDense: true,
|
|
onChanged: (_) {},
|
|
style: const TextStyle(color: Colors.blue),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('Dropdown menu scrolls to first item in long lists', (WidgetTester tester) async {
|
|
// Open the dropdown menu
|
|
final Key buttonKey = UniqueKey();
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
buttonKey: buttonKey,
|
|
initialValue: null, // nothing selected
|
|
items: List<String>.generate(/*length=*/ 100, (int index) => index.toString()),
|
|
onChanged: onChanged,
|
|
),
|
|
);
|
|
await tester.tap(find.byKey(buttonKey));
|
|
await tester.pump();
|
|
await tester.pumpAndSettle(); // finish the menu animation
|
|
|
|
// Find the first item in the scrollable dropdown list
|
|
final Finder menuItemFinder = find.byType(Scrollable);
|
|
final RenderBox menuItemContainer = tester.renderObject<RenderBox>(menuItemFinder);
|
|
final RenderBox firstItem = tester.renderObject<RenderBox>(
|
|
find.descendant(of: menuItemFinder, matching: find.byKey(const ValueKey<String>('0'))),
|
|
);
|
|
|
|
// List should be scrolled so that the first item is at the top. Menu items
|
|
// are offset 8.0 from the top edge of the scrollable menu.
|
|
const selectedItemOffset = Offset(0.0, -8.0);
|
|
expect(
|
|
firstItem.size.topCenter(firstItem.localToGlobal(selectedItemOffset)).dy,
|
|
equals(menuItemContainer.size.topCenter(menuItemContainer.localToGlobal(Offset.zero)).dy),
|
|
);
|
|
});
|
|
|
|
testWidgets('Dropdown menu aligns selected item with button in long lists', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// Open the dropdown menu
|
|
final Key buttonKey = UniqueKey();
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
buttonKey: buttonKey,
|
|
initialValue: '50',
|
|
items: List<String>.generate(/*length=*/ 100, (int index) => index.toString()),
|
|
onChanged: onChanged,
|
|
),
|
|
);
|
|
final RenderBox buttonBox = tester.renderObject<RenderBox>(find.byKey(buttonKey));
|
|
await tester.tap(find.byKey(buttonKey));
|
|
await tester.pumpAndSettle(); // finish the menu animation
|
|
|
|
// Find the selected item in the scrollable dropdown list
|
|
final RenderBox selectedItem = tester.renderObject<RenderBox>(
|
|
find.descendant(
|
|
of: find.byType(Scrollable),
|
|
matching: find.byKey(const ValueKey<String>('50')),
|
|
),
|
|
);
|
|
|
|
// List should be scrolled so that the selected item is in line with the button
|
|
expect(
|
|
selectedItem.size.center(selectedItem.localToGlobal(Offset.zero)).dy,
|
|
equals(buttonBox.size.center(buttonBox.localToGlobal(Offset.zero)).dy),
|
|
);
|
|
});
|
|
|
|
testWidgets('Dropdown menu scrolls to last item in long lists', (WidgetTester tester) async {
|
|
final Key buttonKey = UniqueKey();
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
buttonKey: buttonKey,
|
|
initialValue: '99',
|
|
items: List<String>.generate(/*length=*/ 100, (int index) => index.toString()),
|
|
onChanged: onChanged,
|
|
),
|
|
);
|
|
await tester.tap(find.byKey(buttonKey));
|
|
await tester.pump();
|
|
|
|
final ScrollController scrollController = PrimaryScrollController.of(
|
|
tester.element(find.byType(ListView)),
|
|
);
|
|
// Make sure there is no overscroll
|
|
expect(scrollController.offset, scrollController.position.maxScrollExtent);
|
|
|
|
// Find the selected item in the scrollable dropdown list
|
|
final Finder menuItemFinder = find.byType(Scrollable);
|
|
final RenderBox menuItemContainer = tester.renderObject<RenderBox>(menuItemFinder);
|
|
final RenderBox selectedItem = tester.renderObject<RenderBox>(
|
|
find.descendant(of: menuItemFinder, matching: find.byKey(const ValueKey<String>('99'))),
|
|
);
|
|
|
|
// kMaterialListPadding.vertical is 8.
|
|
const menuPaddingOffset = Offset(0.0, -8.0);
|
|
final Offset selectedItemOffset = selectedItem.localToGlobal(Offset.zero);
|
|
final Offset menuItemContainerOffset = menuItemContainer.localToGlobal(menuPaddingOffset);
|
|
// Selected item should be aligned to the bottom of the dropdown menu.
|
|
expect(
|
|
selectedItem.size.bottomCenter(selectedItemOffset).dy,
|
|
menuItemContainer.size.bottomCenter(menuItemContainerOffset).dy,
|
|
);
|
|
});
|
|
|
|
testWidgets('Size of DropdownButton with null value', (WidgetTester tester) async {
|
|
final Key buttonKey = UniqueKey();
|
|
String? value;
|
|
|
|
Widget build() => buildFrame(buttonKey: buttonKey, initialValue: value, onChanged: onChanged);
|
|
|
|
await tester.pumpWidget(build());
|
|
final RenderBox buttonBoxNullValue = tester.renderObject<RenderBox>(find.byKey(buttonKey));
|
|
assert(buttonBoxNullValue.attached);
|
|
|
|
value = 'three';
|
|
await tester.pumpWidget(build());
|
|
final RenderBox buttonBox = tester.renderObject<RenderBox>(find.byKey(buttonKey));
|
|
assert(buttonBox.attached);
|
|
|
|
// A Dropdown button with a null value should be the same size as a
|
|
// one with a non-null value.
|
|
expect(
|
|
buttonBox.localToGlobal(Offset.zero),
|
|
equals(buttonBoxNullValue.localToGlobal(Offset.zero)),
|
|
);
|
|
expect(buttonBox.size, equals(buttonBoxNullValue.size));
|
|
});
|
|
|
|
testWidgets('Size of DropdownButton with no items', (WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/26419
|
|
final Key buttonKey = UniqueKey();
|
|
List<String>? items;
|
|
|
|
Widget build() => buildFrame(buttonKey: buttonKey, items: items, onChanged: onChanged);
|
|
|
|
await tester.pumpWidget(build());
|
|
final RenderBox buttonBoxNullItems = tester.renderObject<RenderBox>(find.byKey(buttonKey));
|
|
assert(buttonBoxNullItems.attached);
|
|
|
|
items = <String>[];
|
|
await tester.pumpWidget(build());
|
|
final RenderBox buttonBoxEmptyItems = tester.renderObject<RenderBox>(find.byKey(buttonKey));
|
|
assert(buttonBoxEmptyItems.attached);
|
|
|
|
items = <String>['one', 'two', 'three', 'four'];
|
|
await tester.pumpWidget(build());
|
|
final RenderBox buttonBox = tester.renderObject<RenderBox>(find.byKey(buttonKey));
|
|
assert(buttonBox.attached);
|
|
|
|
// A Dropdown button with a null value should be the same size as a
|
|
// one with a non-null value.
|
|
expect(
|
|
buttonBox.localToGlobal(Offset.zero),
|
|
equals(buttonBoxNullItems.localToGlobal(Offset.zero)),
|
|
);
|
|
expect(buttonBox.size, equals(buttonBoxNullItems.size));
|
|
});
|
|
|
|
testWidgets('Layout of a DropdownButton with null value', (WidgetTester tester) async {
|
|
final Key buttonKey = UniqueKey();
|
|
String? value;
|
|
|
|
void onChanged(String? newValue) {
|
|
value = newValue;
|
|
}
|
|
|
|
Widget build() => buildFrame(buttonKey: buttonKey, initialValue: value, onChanged: onChanged);
|
|
|
|
await tester.pumpWidget(build());
|
|
final RenderBox buttonBox = tester.renderObject<RenderBox>(find.byKey(buttonKey));
|
|
assert(buttonBox.attached);
|
|
|
|
// Show the menu.
|
|
await tester.tap(find.byKey(buttonKey));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
|
|
|
|
// Tap on item 'one', which must appear over the button.
|
|
await tester.tap(find.byKey(buttonKey, skipOffstage: false), warnIfMissed: false);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
|
|
|
|
await tester.pumpWidget(build());
|
|
expect(value, equals('one'));
|
|
});
|
|
|
|
testWidgets('Size of DropdownButton with null value and a hint', (WidgetTester tester) async {
|
|
final Key buttonKey = UniqueKey();
|
|
String? value;
|
|
|
|
// The hint will define the dropdown's width
|
|
Widget build() =>
|
|
buildFrame(buttonKey: buttonKey, initialValue: value, hint: const Text('onetwothree'));
|
|
|
|
await tester.pumpWidget(build());
|
|
expect(find.text('onetwothree'), findsOneWidget);
|
|
final RenderBox buttonBoxHintValue = tester.renderObject<RenderBox>(find.byKey(buttonKey));
|
|
assert(buttonBoxHintValue.attached);
|
|
|
|
value = 'three';
|
|
await tester.pumpWidget(build());
|
|
final RenderBox buttonBox = tester.renderObject<RenderBox>(find.byKey(buttonKey));
|
|
assert(buttonBox.attached);
|
|
|
|
// A Dropdown button with a null value and a hint should be the same size as a
|
|
// one with a non-null value.
|
|
expect(
|
|
buttonBox.localToGlobal(Offset.zero),
|
|
equals(buttonBoxHintValue.localToGlobal(Offset.zero)),
|
|
);
|
|
expect(buttonBox.size, equals(buttonBoxHintValue.size));
|
|
});
|
|
|
|
testWidgets('Dropdown menus must fit within the screen', (WidgetTester tester) async {
|
|
// In all of the tests that follow we're assuming that the dropdown menu
|
|
// is horizontally aligned with the center of the dropdown button and padded
|
|
// on the top, left, and right.
|
|
const buttonPadding = EdgeInsets.only(top: 8.0, left: 16.0, right: 24.0);
|
|
|
|
Rect getExpandedButtonRect() {
|
|
final RenderBox box = tester.renderObject<RenderBox>(find.byType(dropdownButtonType));
|
|
final Rect buttonRect = box.localToGlobal(Offset.zero) & box.size;
|
|
return buttonPadding.inflateRect(buttonRect);
|
|
}
|
|
|
|
late Rect buttonRect;
|
|
late Rect menuRect;
|
|
|
|
Future<void> popUpAndDown(Widget frame) async {
|
|
await tester.pumpWidget(frame);
|
|
await tester.tap(find.byType(dropdownButtonType));
|
|
await tester.pumpAndSettle();
|
|
menuRect = getMenuRect(tester);
|
|
buttonRect = getExpandedButtonRect();
|
|
await tester.tap(find.byType(dropdownButtonType, skipOffstage: false), warnIfMissed: false);
|
|
}
|
|
|
|
// Dropdown button is along the top of the app. The top of the menu is
|
|
// aligned with the top of the expanded button and shifted horizontally
|
|
// so that it fits within the frame.
|
|
|
|
await popUpAndDown(
|
|
buildFrame(
|
|
dropdownAlignment: Alignment.topLeft,
|
|
initialValue: menuItems.last,
|
|
onChanged: onChanged,
|
|
),
|
|
);
|
|
expect(menuRect.topLeft, Offset.zero);
|
|
expect(menuRect.topRight, Offset(menuRect.width, 0.0));
|
|
|
|
await popUpAndDown(
|
|
buildFrame(
|
|
dropdownAlignment: Alignment.topCenter,
|
|
initialValue: menuItems.last,
|
|
onChanged: onChanged,
|
|
),
|
|
);
|
|
expect(menuRect.topLeft, Offset(buttonRect.left, 0.0));
|
|
expect(menuRect.topRight, Offset(buttonRect.right, 0.0));
|
|
|
|
await popUpAndDown(
|
|
buildFrame(
|
|
dropdownAlignment: Alignment.topRight,
|
|
initialValue: menuItems.last,
|
|
onChanged: onChanged,
|
|
),
|
|
);
|
|
expect(menuRect.topLeft, Offset(800.0 - menuRect.width, 0.0));
|
|
expect(menuRect.topRight, const Offset(800.0, 0.0));
|
|
|
|
// Dropdown button is along the middle of the app. The top of the menu is
|
|
// aligned with the top of the expanded button (because the 1st item
|
|
// is selected) and shifted horizontally so that it fits within the frame.
|
|
|
|
await popUpAndDown(
|
|
buildFrame(
|
|
dropdownAlignment: Alignment.centerLeft,
|
|
initialValue: menuItems.first,
|
|
onChanged: onChanged,
|
|
),
|
|
);
|
|
expect(menuRect.topLeft, Offset(0.0, buttonRect.top));
|
|
expect(menuRect.topRight, Offset(menuRect.width, buttonRect.top));
|
|
|
|
await popUpAndDown(buildFrame(initialValue: menuItems.first, onChanged: onChanged));
|
|
expect(menuRect.topLeft, buttonRect.topLeft);
|
|
expect(menuRect.topRight, buttonRect.topRight);
|
|
|
|
await popUpAndDown(
|
|
buildFrame(
|
|
dropdownAlignment: Alignment.centerRight,
|
|
initialValue: menuItems.first,
|
|
onChanged: onChanged,
|
|
),
|
|
);
|
|
expect(menuRect.topLeft, Offset(800.0 - menuRect.width, buttonRect.top));
|
|
expect(menuRect.topRight, Offset(800.0, buttonRect.top));
|
|
|
|
// Dropdown button is along the bottom of the app. The bottom of the menu is
|
|
// aligned with the bottom of the expanded button and shifted horizontally
|
|
// so that it fits within the frame.
|
|
|
|
await popUpAndDown(
|
|
buildFrame(
|
|
dropdownAlignment: Alignment.bottomLeft,
|
|
initialValue: menuItems.first,
|
|
onChanged: onChanged,
|
|
),
|
|
);
|
|
expect(menuRect.bottomLeft, const Offset(0.0, 600.0));
|
|
expect(menuRect.bottomRight, Offset(menuRect.width, 600.0));
|
|
|
|
await popUpAndDown(
|
|
buildFrame(
|
|
dropdownAlignment: Alignment.bottomCenter,
|
|
initialValue: menuItems.first,
|
|
onChanged: onChanged,
|
|
),
|
|
);
|
|
expect(menuRect.bottomLeft, Offset(buttonRect.left, 600.0));
|
|
expect(menuRect.bottomRight, Offset(buttonRect.right, 600.0));
|
|
|
|
await popUpAndDown(
|
|
buildFrame(
|
|
dropdownAlignment: Alignment.bottomRight,
|
|
initialValue: menuItems.first,
|
|
onChanged: onChanged,
|
|
),
|
|
);
|
|
expect(menuRect.bottomLeft, Offset(800.0 - menuRect.width, 600.0));
|
|
expect(menuRect.bottomRight, const Offset(800.0, 600.0));
|
|
});
|
|
|
|
testWidgets(
|
|
'Dropdown menus are dismissed on screen orientation changes, but not on keyboard hide',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(buildFrame(onChanged: onChanged, mediaSize: const Size(800, 600)));
|
|
await tester.tap(find.byType(dropdownButtonType));
|
|
await tester.pumpAndSettle();
|
|
expect(find.byType(ListView), findsOneWidget);
|
|
|
|
// Show a keyboard (simulate by shortening the height).
|
|
await tester.pumpWidget(buildFrame(onChanged: onChanged, mediaSize: const Size(800, 300)));
|
|
await tester.pump();
|
|
expect(find.byType(ListView, skipOffstage: false), findsOneWidget);
|
|
|
|
// Hide a keyboard again (simulate by increasing the height).
|
|
await tester.pumpWidget(buildFrame(onChanged: onChanged, mediaSize: const Size(800, 600)));
|
|
await tester.pump();
|
|
expect(find.byType(ListView, skipOffstage: false), findsOneWidget);
|
|
|
|
// Rotate the device (simulate by changing the aspect ratio).
|
|
await tester.pumpWidget(buildFrame(onChanged: onChanged, mediaSize: const Size(600, 800)));
|
|
await tester.pump();
|
|
expect(find.byType(ListView, skipOffstage: false), findsNothing);
|
|
},
|
|
);
|
|
|
|
testWidgets('Semantics Tree contains only selected element', (WidgetTester tester) async {
|
|
final semantics = SemanticsTester(tester);
|
|
await tester.pumpWidget(buildFrame(onChanged: onChanged));
|
|
|
|
expect(semantics, isNot(includesNodeWith(label: menuItems[0])));
|
|
expect(semantics, includesNodeWith(label: menuItems[1]));
|
|
expect(semantics, isNot(includesNodeWith(label: menuItems[2])));
|
|
expect(semantics, isNot(includesNodeWith(label: menuItems[3])));
|
|
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets('Dropdown button includes semantics', (WidgetTester tester) async {
|
|
final SemanticsHandle handle = tester.ensureSemantics();
|
|
const key = Key('test');
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
buttonKey: key,
|
|
initialValue: null,
|
|
onChanged: (String? _) {},
|
|
hint: const Text('test'),
|
|
),
|
|
);
|
|
|
|
// By default the hint contributes the label.
|
|
expect(
|
|
tester.getSemantics(find.text('test')),
|
|
matchesSemantics(
|
|
isButton: true,
|
|
hasExpandedState: true,
|
|
label: 'test',
|
|
hasTapAction: true,
|
|
hasFocusAction: true,
|
|
isFocusable: true,
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
buttonKey: key,
|
|
initialValue: 'three',
|
|
onChanged: onChanged,
|
|
hint: const Text('test'),
|
|
),
|
|
);
|
|
|
|
// Displays label of select item.
|
|
expect(
|
|
tester.getSemantics(find.text('three')),
|
|
matchesSemantics(
|
|
isButton: true,
|
|
hasExpandedState: true,
|
|
label: 'three',
|
|
hasTapAction: true,
|
|
hasFocusAction: true,
|
|
isFocusable: true,
|
|
),
|
|
);
|
|
handle.dispose();
|
|
});
|
|
|
|
testWidgets('Dropdown menu includes semantics', (WidgetTester tester) async {
|
|
final semantics = SemanticsTester(tester);
|
|
const key = Key('test');
|
|
await tester.pumpWidget(buildFrame(buttonKey: key, initialValue: null, onChanged: onChanged));
|
|
await tester.tap(find.byKey(key));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
semantics,
|
|
hasSemantics(
|
|
TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics.rootChild(
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
role: SemanticsRole.menu,
|
|
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute, SemanticsFlag.namesRoute],
|
|
label: 'Popup menu',
|
|
textDirection: TextDirection.ltr,
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
role: SemanticsRole.menuItem,
|
|
label: 'one',
|
|
textDirection: TextDirection.ltr,
|
|
flags: <SemanticsFlag>[
|
|
SemanticsFlag.isButton,
|
|
SemanticsFlag.isFocused,
|
|
SemanticsFlag.isFocusable,
|
|
],
|
|
tags: <SemanticsTag>[
|
|
const SemanticsTag('RenderViewport.twoPane'),
|
|
],
|
|
actions: <SemanticsAction>[
|
|
SemanticsAction.tap,
|
|
SemanticsAction.focus,
|
|
],
|
|
),
|
|
TestSemantics(
|
|
role: SemanticsRole.menuItem,
|
|
label: 'two',
|
|
textDirection: TextDirection.ltr,
|
|
flags: <SemanticsFlag>[
|
|
SemanticsFlag.isButton,
|
|
SemanticsFlag.isFocusable,
|
|
],
|
|
tags: <SemanticsTag>[
|
|
const SemanticsTag('RenderViewport.twoPane'),
|
|
],
|
|
actions: <SemanticsAction>[
|
|
SemanticsAction.tap,
|
|
SemanticsAction.focus,
|
|
],
|
|
),
|
|
TestSemantics(
|
|
role: SemanticsRole.menuItem,
|
|
label: 'three',
|
|
textDirection: TextDirection.ltr,
|
|
flags: <SemanticsFlag>[
|
|
SemanticsFlag.isButton,
|
|
SemanticsFlag.isFocusable,
|
|
],
|
|
tags: <SemanticsTag>[
|
|
const SemanticsTag('RenderViewport.twoPane'),
|
|
],
|
|
actions: <SemanticsAction>[
|
|
SemanticsAction.tap,
|
|
SemanticsAction.focus,
|
|
],
|
|
),
|
|
TestSemantics(
|
|
role: SemanticsRole.menuItem,
|
|
label: 'four',
|
|
textDirection: TextDirection.ltr,
|
|
flags: <SemanticsFlag>[
|
|
SemanticsFlag.isButton,
|
|
SemanticsFlag.isFocusable,
|
|
],
|
|
tags: <SemanticsTag>[
|
|
const SemanticsTag('RenderViewport.twoPane'),
|
|
],
|
|
actions: <SemanticsAction>[
|
|
SemanticsAction.tap,
|
|
SemanticsAction.focus,
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
TestSemantics(
|
|
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.dismiss],
|
|
label: 'Dismiss',
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
ignoreId: true,
|
|
ignoreRect: true,
|
|
ignoreTransform: true,
|
|
),
|
|
);
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets('disabledHint displays on empty items or onChanged', (WidgetTester tester) async {
|
|
final Key buttonKey = UniqueKey();
|
|
|
|
Widget build({List<String>? items, ValueChanged<String?>? onChanged}) => buildFrame(
|
|
items: items,
|
|
onChanged: onChanged,
|
|
buttonKey: buttonKey,
|
|
initialValue: null,
|
|
hint: const Text('enabled'),
|
|
disabledHint: const Text('disabled'),
|
|
);
|
|
|
|
// [disabledHint] should display when [items] is null
|
|
await tester.pumpWidget(build(onChanged: onChanged));
|
|
expect(find.text('enabled'), findsNothing);
|
|
expect(find.text('disabled'), findsOneWidget);
|
|
|
|
// [disabledHint] should display when [items] is an empty list.
|
|
await tester.pumpWidget(build(items: <String>[], onChanged: onChanged));
|
|
expect(find.text('enabled'), findsNothing);
|
|
expect(find.text('disabled'), findsOneWidget);
|
|
|
|
// [disabledHint] should display when [onChanged] is null
|
|
await tester.pumpWidget(build(items: menuItems));
|
|
expect(find.text('enabled'), findsNothing);
|
|
expect(find.text('disabled'), findsOneWidget);
|
|
final RenderBox disabledHintBox = tester.renderObject<RenderBox>(find.byKey(buttonKey));
|
|
|
|
// A Dropdown button with a disabled hint should be the same size as a
|
|
// one with a regular enabled hint.
|
|
await tester.pumpWidget(build(items: menuItems, onChanged: onChanged));
|
|
expect(find.text('disabled'), findsNothing);
|
|
expect(find.text('enabled'), findsOneWidget);
|
|
final RenderBox enabledHintBox = tester.renderObject<RenderBox>(find.byKey(buttonKey));
|
|
expect(
|
|
enabledHintBox.localToGlobal(Offset.zero),
|
|
equals(disabledHintBox.localToGlobal(Offset.zero)),
|
|
);
|
|
expect(enabledHintBox.size, equals(disabledHintBox.size));
|
|
});
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/70177
|
|
testWidgets('disabledHint behavior test', (WidgetTester tester) async {
|
|
Widget build({
|
|
List<String>? items,
|
|
ValueChanged<String?>? onChanged,
|
|
String? value,
|
|
Widget? hint,
|
|
Widget? disabledHint,
|
|
}) => buildFrame(
|
|
items: items,
|
|
onChanged: onChanged,
|
|
initialValue: value,
|
|
hint: hint,
|
|
disabledHint: disabledHint,
|
|
);
|
|
|
|
// The selected value should be displayed when the button is disabled.
|
|
await tester.pumpWidget(build(items: menuItems, value: 'two'));
|
|
// The dropdown icon and the selected menu item are vertically aligned.
|
|
expect(tester.getCenter(find.text('two')).dy, tester.getCenter(find.byType(Icon)).dy);
|
|
|
|
// If [value] is null, the button is enabled, hint is displayed.
|
|
await tester.pumpWidget(
|
|
build(
|
|
items: menuItems,
|
|
onChanged: onChanged,
|
|
hint: const Text('hint'),
|
|
disabledHint: const Text('disabledHint'),
|
|
),
|
|
);
|
|
expect(tester.getCenter(find.text('hint')).dy, tester.getCenter(find.byType(Icon)).dy);
|
|
|
|
// If [value] is null, the button is disabled, [disabledHint] is displayed when [disabledHint] is non-null.
|
|
await tester.pumpWidget(
|
|
build(items: menuItems, hint: const Text('hint'), disabledHint: const Text('disabledHint')),
|
|
);
|
|
expect(tester.getCenter(find.text('disabledHint')).dy, tester.getCenter(find.byType(Icon)).dy);
|
|
|
|
// If [value] is null, the button is disabled, [hint] is displayed when [disabledHint] is null.
|
|
await tester.pumpWidget(build(items: menuItems, hint: const Text('hint')));
|
|
expect(tester.getCenter(find.text('hint')).dy, tester.getCenter(find.byType(Icon)).dy);
|
|
|
|
int? getIndex() {
|
|
final stack = tester.element(find.byType(IndexedStack)).widget as IndexedStack;
|
|
return stack.index;
|
|
}
|
|
|
|
// If [value], [hint] and [disabledHint] are null, the button is disabled, nothing displayed.
|
|
await tester.pumpWidget(build(items: menuItems));
|
|
expect(getIndex(), null);
|
|
|
|
// If [value], [hint] and [disabledHint] are null, the button is enabled, nothing displayed.
|
|
await tester.pumpWidget(build(items: menuItems, onChanged: onChanged));
|
|
expect(getIndex(), null);
|
|
});
|
|
|
|
testWidgets('DropdownButton selected item color test', (WidgetTester tester) async {
|
|
Widget build({
|
|
ValueChanged<String?>? onChanged,
|
|
String? value,
|
|
Widget? hint,
|
|
Widget? disabledHint,
|
|
}) {
|
|
return MaterialApp(
|
|
theme: ThemeData(disabledColor: Colors.pink),
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: Column(
|
|
children: <Widget>[
|
|
DropdownButtonFormField<String>(
|
|
style: const TextStyle(color: Colors.yellow),
|
|
disabledHint: disabledHint,
|
|
hint: hint,
|
|
items: const <DropdownMenuItem<String>>[
|
|
DropdownMenuItem<String>(value: 'one', child: Text('one')),
|
|
DropdownMenuItem<String>(value: 'two', child: Text('two')),
|
|
],
|
|
initialValue: value,
|
|
onChanged: onChanged,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Color textColor(String text) {
|
|
return tester.renderObject<RenderParagraph>(find.text(text)).text.style!.color!;
|
|
}
|
|
|
|
// The selected value should be displayed when the button is enabled.
|
|
await tester.pumpWidget(build(onChanged: onChanged, value: 'two'));
|
|
// The dropdown icon and the selected menu item are vertically aligned.
|
|
expect(tester.getCenter(find.text('two')).dy, tester.getCenter(find.byType(Icon)).dy);
|
|
// Selected item has a normal color from [DropdownButtonFormField.style]
|
|
// when the button is enabled.
|
|
expect(textColor('two'), Colors.yellow);
|
|
|
|
// The selected value should be displayed when the button is disabled.
|
|
await tester.pumpWidget(build(value: 'two'));
|
|
expect(tester.getCenter(find.text('two')).dy, tester.getCenter(find.byType(Icon)).dy);
|
|
// Selected item has a disabled color from [theme.disabledColor]
|
|
// when the button is disable.
|
|
expect(textColor('two'), Colors.pink);
|
|
});
|
|
|
|
testWidgets('DropdownButton hint displays when the items list is empty, '
|
|
'items is null, and disabledHint is null', (WidgetTester tester) async {
|
|
final Key buttonKey = UniqueKey();
|
|
|
|
Widget build({List<String>? items}) {
|
|
return buildFrame(
|
|
items: items,
|
|
buttonKey: buttonKey,
|
|
initialValue: null,
|
|
hint: const Text('hint used when disabled'),
|
|
);
|
|
}
|
|
|
|
// [hint] should display when [items] is null and [disabledHint] is not defined
|
|
await tester.pumpWidget(build());
|
|
expect(find.text('hint used when disabled'), findsOneWidget);
|
|
|
|
// [hint] should display when [items] is an empty list and [disabledHint] is not defined.
|
|
await tester.pumpWidget(build(items: <String>[]));
|
|
expect(find.text('hint used when disabled'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('DropdownButton disabledHint is null by default', (WidgetTester tester) async {
|
|
final Key buttonKey = UniqueKey();
|
|
|
|
Widget build({List<String>? items}) {
|
|
return buildFrame(
|
|
items: items,
|
|
buttonKey: buttonKey,
|
|
initialValue: null,
|
|
hint: const Text('hint used when disabled'),
|
|
);
|
|
}
|
|
|
|
// [hint] should display when [items] is null and [disabledHint] is not defined
|
|
await tester.pumpWidget(build());
|
|
expect(find.text('hint used when disabled'), findsOneWidget);
|
|
|
|
// [hint] should display when [items] is an empty list and [disabledHint] is not defined.
|
|
await tester.pumpWidget(build(items: <String>[]));
|
|
expect(find.text('hint used when disabled'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets(
|
|
'Size of largest widget is used DropdownButton when selectedItemBuilder is non-null',
|
|
(WidgetTester tester) async {
|
|
final items = <String>['25', '50', '100'];
|
|
const selectedItem = '25';
|
|
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
// To test the size constraints, the selected item should not be the
|
|
// largest item. This validates that the button sizes itself according
|
|
// to the largest item regardless of which one is selected.
|
|
initialValue: selectedItem,
|
|
items: items,
|
|
itemHeight: null,
|
|
selectedItemBuilder: (BuildContext context) {
|
|
return items.map<Widget>((String item) {
|
|
return SizedBox(
|
|
height: double.parse(item),
|
|
width: double.parse(item),
|
|
child: Center(child: Text(item)),
|
|
);
|
|
}).toList();
|
|
},
|
|
onChanged: (String? newValue) {},
|
|
),
|
|
);
|
|
|
|
final RenderBox dropdownButtonRenderBox = tester.renderObject<RenderBox>(
|
|
find.widgetWithText(Row, '25'),
|
|
);
|
|
// DropdownButton should be the height of the largest item
|
|
expect(dropdownButtonRenderBox.size.height, 100);
|
|
// DropdownButton should be width of largest item added to the icon size
|
|
expect(dropdownButtonRenderBox.size.width, 100 + 24.0);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'Enabled button - Size of largest widget is used DropdownButton when selectedItemBuilder '
|
|
'is non-null and hint is defined, but smaller than largest selected item widget',
|
|
(WidgetTester tester) async {
|
|
final items = <String>['25', '50', '100'];
|
|
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
initialValue: null,
|
|
// [hint] widget is smaller than largest selected item widget
|
|
hint: const SizedBox(height: 50, width: 50, child: Text('hint')),
|
|
items: items,
|
|
itemHeight: null,
|
|
selectedItemBuilder: (BuildContext context) {
|
|
return items.map<Widget>((String item) {
|
|
return SizedBox(
|
|
height: double.parse(item),
|
|
width: double.parse(item),
|
|
child: Center(child: Text(item)),
|
|
);
|
|
}).toList();
|
|
},
|
|
onChanged: (String? newValue) {},
|
|
),
|
|
);
|
|
|
|
final RenderBox dropdownButtonRenderBox = tester.renderObject<RenderBox>(
|
|
find.widgetWithText(Row, 'hint'),
|
|
);
|
|
// DropdownButton should be the height of the largest item
|
|
expect(dropdownButtonRenderBox.size.height, 100);
|
|
// DropdownButton should be width of largest item added to the icon size
|
|
expect(dropdownButtonRenderBox.size.width, 100 + 24.0);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'Enabled button - Size of largest widget is used DropdownButton when selectedItemBuilder '
|
|
'is non-null and hint is defined, but larger than largest selected item widget',
|
|
(WidgetTester tester) async {
|
|
final items = <String>['25', '50', '100'];
|
|
const selectedItem = '25';
|
|
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
// To test the size constraints, the selected item should not be the
|
|
// largest item. This validates that the button sizes itself according
|
|
// to the largest item regardless of which one is selected.
|
|
initialValue: selectedItem,
|
|
// [hint] widget is larger than largest selected item widget
|
|
hint: const SizedBox(height: 125, width: 125, child: Text('hint')),
|
|
items: items,
|
|
itemHeight: null,
|
|
selectedItemBuilder: (BuildContext context) {
|
|
return items.map<Widget>((String item) {
|
|
return SizedBox(
|
|
height: double.parse(item),
|
|
width: double.parse(item),
|
|
child: Center(child: Text(item)),
|
|
);
|
|
}).toList();
|
|
},
|
|
onChanged: (String? newValue) {},
|
|
),
|
|
);
|
|
|
|
final RenderBox dropdownButtonRenderBox = tester.renderObject<RenderBox>(
|
|
find.widgetWithText(Row, '25'),
|
|
);
|
|
// DropdownButton should be the height of the largest item (hint inclusive)
|
|
expect(dropdownButtonRenderBox.size.height, 125);
|
|
// DropdownButton should be width of largest item (hint inclusive) added to the icon size
|
|
expect(dropdownButtonRenderBox.size.width, 125 + 24.0);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'Disabled button - Size of largest widget is used DropdownButton when selectedItemBuilder '
|
|
'is non-null, and hint is defined, but smaller than largest selected item widget',
|
|
(WidgetTester tester) async {
|
|
final items = <String>['25', '50', '100'];
|
|
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
initialValue: null,
|
|
// [hint] widget is smaller than largest selected item widget
|
|
hint: const SizedBox(height: 50, width: 50, child: Text('hint')),
|
|
items: items,
|
|
itemHeight: null,
|
|
selectedItemBuilder: (BuildContext context) {
|
|
return items.map<Widget>((String item) {
|
|
return SizedBox(
|
|
height: double.parse(item),
|
|
width: double.parse(item),
|
|
child: Center(child: Text(item)),
|
|
);
|
|
}).toList();
|
|
},
|
|
),
|
|
);
|
|
|
|
final RenderBox dropdownButtonRenderBox = tester.renderObject<RenderBox>(
|
|
find.widgetWithText(Row, 'hint'),
|
|
);
|
|
// DropdownButton should be the height of the largest item
|
|
expect(dropdownButtonRenderBox.size.height, 100);
|
|
// DropdownButton should be width of largest item added to the icon size
|
|
expect(dropdownButtonRenderBox.size.width, 100 + 24.0);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'Disabled button - Size of largest widget is used DropdownButton when selectedItemBuilder '
|
|
'is non-null and hint is defined, but larger than largest selected item widget',
|
|
(WidgetTester tester) async {
|
|
final items = <String>['25', '50', '100'];
|
|
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
initialValue: null,
|
|
// [hint] widget is larger than largest selected item widget
|
|
hint: const SizedBox(height: 125, width: 125, child: Text('hint')),
|
|
items: items,
|
|
itemHeight: null,
|
|
selectedItemBuilder: (BuildContext context) {
|
|
return items.map<Widget>((String item) {
|
|
return SizedBox(
|
|
height: double.parse(item),
|
|
width: double.parse(item),
|
|
child: Center(child: Text(item)),
|
|
);
|
|
}).toList();
|
|
},
|
|
),
|
|
);
|
|
|
|
final RenderBox dropdownButtonRenderBox = tester.renderObject<RenderBox>(
|
|
find.widgetWithText(Row, '25', skipOffstage: false),
|
|
);
|
|
// DropdownButton should be the height of the largest item (hint inclusive)
|
|
expect(dropdownButtonRenderBox.size.height, 125);
|
|
// DropdownButton should be width of largest item (hint inclusive) added to the icon size
|
|
expect(dropdownButtonRenderBox.size.width, 125 + 24.0);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'Disabled button - Size of largest widget is used DropdownButton when selectedItemBuilder '
|
|
'is non-null, and disabledHint is defined, but smaller than largest selected item widget',
|
|
(WidgetTester tester) async {
|
|
final items = <String>['25', '50', '100'];
|
|
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
initialValue: null,
|
|
// [hint] widget is smaller than largest selected item widget
|
|
disabledHint: const SizedBox(height: 50, width: 50, child: Text('hint')),
|
|
items: items,
|
|
itemHeight: null,
|
|
selectedItemBuilder: (BuildContext context) {
|
|
return items.map<Widget>((String item) {
|
|
return SizedBox(
|
|
height: double.parse(item),
|
|
width: double.parse(item),
|
|
child: Center(child: Text(item)),
|
|
);
|
|
}).toList();
|
|
},
|
|
),
|
|
);
|
|
|
|
final RenderBox dropdownButtonRenderBox = tester.renderObject<RenderBox>(
|
|
find.widgetWithText(Row, 'hint'),
|
|
);
|
|
// DropdownButton should be the height of the largest item
|
|
expect(dropdownButtonRenderBox.size.height, 100);
|
|
// DropdownButton should be width of largest item added to the icon size
|
|
expect(dropdownButtonRenderBox.size.width, 100 + 24.0);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'Disabled button - Size of largest widget is used DropdownButton when selectedItemBuilder '
|
|
'is non-null and disabledHint is defined, but larger than largest selected item widget',
|
|
(WidgetTester tester) async {
|
|
final items = <String>['25', '50', '100'];
|
|
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
initialValue: null,
|
|
// [hint] widget is larger than largest selected item widget
|
|
disabledHint: const SizedBox(height: 125, width: 125, child: Text('hint')),
|
|
items: items,
|
|
itemHeight: null,
|
|
selectedItemBuilder: (BuildContext context) {
|
|
return items.map<Widget>((String item) {
|
|
return SizedBox(
|
|
height: double.parse(item),
|
|
width: double.parse(item),
|
|
child: Center(child: Text(item)),
|
|
);
|
|
}).toList();
|
|
},
|
|
),
|
|
);
|
|
|
|
final RenderBox dropdownButtonRenderBox = tester.renderObject<RenderBox>(
|
|
find.widgetWithText(Row, '25', skipOffstage: false),
|
|
);
|
|
// DropdownButton should be the height of the largest item (hint inclusive)
|
|
expect(dropdownButtonRenderBox.size.height, 125);
|
|
// DropdownButton should be width of largest item (hint inclusive) added to the icon size
|
|
expect(dropdownButtonRenderBox.size.width, 125 + 24.0);
|
|
},
|
|
);
|
|
|
|
testWidgets('Menu width is correct when set', (WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/133267.
|
|
final items = <String>['25', '50', '100'];
|
|
const selectedItem = '25';
|
|
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
initialValue: selectedItem,
|
|
items: items,
|
|
menuWidth: 200,
|
|
selectedItemBuilder: (BuildContext context) {
|
|
return items.map<Widget>((String item) {
|
|
return SizedBox(
|
|
height: double.parse(item),
|
|
width: double.parse(item),
|
|
child: Center(child: Text(item)),
|
|
);
|
|
}).toList();
|
|
},
|
|
onChanged: (String? newValue) {},
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('25'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(getMenuRect(tester).width, 200);
|
|
});
|
|
|
|
testWidgets('Dropdown in middle showing middle item', (WidgetTester tester) async {
|
|
final items = List<DropdownMenuItem<int>>.generate(
|
|
100,
|
|
(int i) => DropdownMenuItem<int>(value: i, child: Text('$i')),
|
|
);
|
|
|
|
final button = DropdownButton<int>(value: 50, onChanged: (int? newValue) {}, items: items);
|
|
|
|
double getMenuScroll() {
|
|
double scrollPosition;
|
|
final ScrollController scrollController = PrimaryScrollController.of(
|
|
tester.element(find.byType(ListView)),
|
|
);
|
|
scrollPosition = scrollController.position.pixels;
|
|
return scrollPosition;
|
|
}
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(child: Align(child: button)),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('50'));
|
|
await tester.pumpAndSettle();
|
|
expect(getMenuScroll(), 2180.0);
|
|
});
|
|
|
|
testWidgets('Dropdown in top showing bottom item', (WidgetTester tester) async {
|
|
final items = List<DropdownMenuItem<int>>.generate(
|
|
100,
|
|
(int i) => DropdownMenuItem<int>(value: i, child: Text('$i')),
|
|
);
|
|
|
|
final button = DropdownButton<int>(value: 99, onChanged: (int? newValue) {}, items: items);
|
|
|
|
double getMenuScroll() {
|
|
double scrollPosition;
|
|
final ScrollController scrollController = PrimaryScrollController.of(
|
|
tester.element(find.byType(ListView)),
|
|
);
|
|
scrollPosition = scrollController.position.pixels;
|
|
return scrollPosition;
|
|
}
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: Align(alignment: Alignment.topCenter, child: button),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('99'));
|
|
await tester.pumpAndSettle();
|
|
expect(getMenuScroll(), 4312.0);
|
|
});
|
|
|
|
testWidgets('Dropdown in bottom showing top item', (WidgetTester tester) async {
|
|
final items = List<DropdownMenuItem<int>>.generate(
|
|
100,
|
|
(int i) => DropdownMenuItem<int>(value: i, child: Text('$i')),
|
|
);
|
|
|
|
final button = DropdownButton<int>(value: 0, onChanged: (int? newValue) {}, items: items);
|
|
|
|
double getMenuScroll() {
|
|
double scrollPosition;
|
|
final ScrollController scrollController = PrimaryScrollController.of(
|
|
tester.element(find.byType(ListView)),
|
|
);
|
|
scrollPosition = scrollController.position.pixels;
|
|
return scrollPosition;
|
|
}
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: Align(alignment: Alignment.bottomCenter, child: button),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('0'));
|
|
await tester.pumpAndSettle();
|
|
expect(getMenuScroll(), 0.0);
|
|
});
|
|
|
|
testWidgets('Dropdown in center showing bottom item', (WidgetTester tester) async {
|
|
final items = List<DropdownMenuItem<int>>.generate(
|
|
100,
|
|
(int i) => DropdownMenuItem<int>(value: i, child: Text('$i')),
|
|
);
|
|
|
|
final button = DropdownButton<int>(value: 99, onChanged: (int? newValue) {}, items: items);
|
|
|
|
double getMenuScroll() {
|
|
double scrollPosition;
|
|
final ScrollController scrollController = PrimaryScrollController.of(
|
|
tester.element(find.byType(ListView)),
|
|
);
|
|
scrollPosition = scrollController.position.pixels;
|
|
return scrollPosition;
|
|
}
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(child: Align(child: button)),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('99'));
|
|
await tester.pumpAndSettle();
|
|
expect(getMenuScroll(), 4312.0);
|
|
});
|
|
|
|
testWidgets('Dropdown menu respects parent size limits', (WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/24417
|
|
int? selectedIndex;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
bottomNavigationBar: const SizedBox(height: 200),
|
|
body: Navigator(
|
|
onGenerateRoute: (RouteSettings settings) {
|
|
return MaterialPageRoute<void>(
|
|
builder: (BuildContext context) {
|
|
return SafeArea(
|
|
child: Container(
|
|
alignment: Alignment.topLeft,
|
|
// From material/dropdown.dart (menus are unaligned by default):
|
|
// _kUnalignedMenuMargin = EdgeInsetsDirectional.only(start: 16.0, end: 24.0)
|
|
// This padding ensures that the entire menu will be visible
|
|
padding: const EdgeInsetsDirectional.only(start: 16.0, end: 24.0),
|
|
child: DropdownButton<int>(
|
|
value: 12,
|
|
onChanged: (int? i) {
|
|
selectedIndex = i;
|
|
},
|
|
items: List<DropdownMenuItem<int>>.generate(100, (int i) {
|
|
return DropdownMenuItem<int>(value: i, child: Text('$i'));
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('12'));
|
|
await tester.pumpAndSettle();
|
|
expect(selectedIndex, null);
|
|
|
|
await tester.tap(find.text('13').last);
|
|
await tester.pumpAndSettle();
|
|
expect(selectedIndex, 13);
|
|
});
|
|
|
|
testWidgets('Dropdown button will accept widgets as its underline', (WidgetTester tester) async {
|
|
const decoration = BoxDecoration(
|
|
border: Border(bottom: BorderSide(color: Color(0xFFCCBB00), width: 4.0)),
|
|
);
|
|
const defaultDecoration = BoxDecoration(
|
|
border: Border(bottom: BorderSide(color: Color(0xFFBDBDBD), width: 0.0)),
|
|
);
|
|
|
|
final Widget customUnderline = Container(height: 4.0, decoration: decoration);
|
|
final Key buttonKey = UniqueKey();
|
|
|
|
final Finder decoratedBox = find.descendant(
|
|
of: find.byKey(buttonKey),
|
|
matching: find.byType(DecoratedBox),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
buildFrame(buttonKey: buttonKey, underline: customUnderline, onChanged: onChanged),
|
|
);
|
|
expect(tester.widgetList<DecoratedBox>(decoratedBox).last.decoration, decoration);
|
|
|
|
await tester.pumpWidget(buildFrame(buttonKey: buttonKey, onChanged: onChanged));
|
|
expect(tester.widgetList<DecoratedBox>(decoratedBox).last.decoration, defaultDecoration);
|
|
});
|
|
|
|
testWidgets('DropdownButton selectedItemBuilder builds custom buttons', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const items = <String>['One', 'Two', 'Three'];
|
|
String? selectedItem = items[0];
|
|
|
|
await tester.pumpWidget(
|
|
StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return MaterialApp(
|
|
home: Scaffold(
|
|
body: DropdownButton<String>(
|
|
value: selectedItem,
|
|
onChanged: (String? string) {
|
|
setState(() => selectedItem = string);
|
|
},
|
|
selectedItemBuilder: (BuildContext context) {
|
|
var index = 0;
|
|
return items.map((String string) {
|
|
index += 1;
|
|
return Text('$string as an Arabic numeral: $index');
|
|
}).toList();
|
|
},
|
|
items: items.map((String string) {
|
|
return DropdownMenuItem<String>(value: string, child: Text(string));
|
|
}).toList(),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(find.text('One as an Arabic numeral: 1'), findsOneWidget);
|
|
await tester.tap(find.text('One as an Arabic numeral: 1'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('Two'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Two as an Arabic numeral: 2'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('DropdownButton uses default color when expanded', (WidgetTester tester) async {
|
|
await checkDropdownColor(tester);
|
|
});
|
|
|
|
testWidgets('DropdownButton uses dropdownColor when expanded', (WidgetTester tester) async {
|
|
await checkDropdownColor(tester, color: const Color.fromRGBO(120, 220, 70, 0.8));
|
|
});
|
|
|
|
testWidgets('DropdownButtonFormField uses dropdownColor when expanded', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await checkDropdownColor(
|
|
tester,
|
|
color: const Color.fromRGBO(120, 220, 70, 0.8),
|
|
isFormField: true,
|
|
);
|
|
});
|
|
|
|
testWidgets('DropdownButton hint displays properly when selectedItemBuilder is defined', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/42340
|
|
final items = <String>['1', '2', '3'];
|
|
String? selectedItem;
|
|
|
|
await tester.pumpWidget(
|
|
StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return MaterialApp(
|
|
home: Scaffold(
|
|
body: DropdownButton<String>(
|
|
hint: const Text('Please select an item'),
|
|
value: selectedItem,
|
|
onChanged: (String? string) {
|
|
setState(() {
|
|
selectedItem = string;
|
|
});
|
|
},
|
|
selectedItemBuilder: (BuildContext context) {
|
|
return items.map((String item) {
|
|
return Text('You have selected: $item');
|
|
}).toList();
|
|
},
|
|
items: items.map((String item) {
|
|
return DropdownMenuItem<String>(value: item, child: Text(item));
|
|
}).toList(),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
// Initially shows the hint text
|
|
expect(find.text('Please select an item'), findsOneWidget);
|
|
await tester.tap(find.text('Please select an item', skipOffstage: false), warnIfMissed: false);
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('1'));
|
|
await tester.pumpAndSettle();
|
|
// Selecting an item should display its corresponding item builder
|
|
expect(find.text('You have selected: 1'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('Variable size and oversized menu items', (WidgetTester tester) async {
|
|
final itemHeights = <double>[30, 40, 50, 60];
|
|
double? dropdownValue = itemHeights[0];
|
|
|
|
Widget buildFrame() {
|
|
return MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return DropdownButton<double>(
|
|
onChanged: (double? value) {
|
|
setState(() {
|
|
dropdownValue = value;
|
|
});
|
|
},
|
|
value: dropdownValue,
|
|
itemHeight: null,
|
|
items: itemHeights.map<DropdownMenuItem<double>>((double value) {
|
|
return DropdownMenuItem<double>(
|
|
key: ValueKey<double>(value),
|
|
value: value,
|
|
child: Center(
|
|
child: Container(width: 100, height: value, color: Colors.blue),
|
|
),
|
|
);
|
|
}).toList(),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
final Finder dropdownIcon = find.byType(Icon);
|
|
final Finder item30 = find.byKey(const ValueKey<double>(30), skipOffstage: false);
|
|
final Finder item40 = find.byKey(const ValueKey<double>(40), skipOffstage: false);
|
|
final Finder item50 = find.byKey(const ValueKey<double>(50), skipOffstage: false);
|
|
final Finder item60 = find.byKey(const ValueKey<double>(60), skipOffstage: false);
|
|
|
|
// Only the DropdownButton is visible. It contains the selected item
|
|
// and a dropdown arrow icon.
|
|
await tester.pumpWidget(buildFrame());
|
|
expect(dropdownIcon, findsOneWidget);
|
|
expect(item30, findsOneWidget);
|
|
|
|
// All menu items have a minimum height of 48. The centers of the
|
|
// dropdown icon and the selected menu item are vertically aligned
|
|
// and horizontally adjacent.
|
|
expect(tester.getSize(item30), const Size(100, 48));
|
|
expect(tester.getCenter(item30).dy, tester.getCenter(dropdownIcon).dy);
|
|
expect(tester.getTopRight(item30).dx, tester.getTopLeft(dropdownIcon).dx);
|
|
|
|
// Show the popup menu.
|
|
await tester.tap(item30);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Each item appears twice, once in the menu and once
|
|
// in the dropdown button's IndexedStack.
|
|
expect(item30.evaluate().length, 2);
|
|
expect(item40.evaluate().length, 2);
|
|
expect(item50.evaluate().length, 2);
|
|
expect(item60.evaluate().length, 2);
|
|
|
|
// Verify that the items have the expected sizes. The width of the items
|
|
// that appear in the menu is padded by 16 on the left and right.
|
|
expect(tester.getSize(item30.first), const Size(100, 48));
|
|
expect(tester.getSize(item40.first), const Size(100, 48));
|
|
expect(tester.getSize(item50.first), const Size(100, 50));
|
|
expect(tester.getSize(item60.first), const Size(100, 60));
|
|
expect(tester.getSize(item30.last), const Size(132, 48));
|
|
expect(tester.getSize(item40.last), const Size(132, 48));
|
|
expect(tester.getSize(item50.last), const Size(132, 50));
|
|
expect(tester.getSize(item60.last), const Size(132, 60));
|
|
|
|
// The vertical center of the selectedItem (item30) should
|
|
// line up with its button counterpart.
|
|
expect(tester.getCenter(item30.first).dy, tester.getCenter(item30.last).dy);
|
|
|
|
// The menu items should be arranged in a column.
|
|
expect(tester.getBottomLeft(item30.last), tester.getTopLeft(item40.last));
|
|
expect(tester.getBottomLeft(item40.last), tester.getTopLeft(item50.last));
|
|
expect(tester.getBottomLeft(item50.last), tester.getTopLeft(item60.last));
|
|
|
|
// Dismiss the menu by selecting item40 and then show the menu again.
|
|
await tester.tap(item40.last);
|
|
await tester.pumpAndSettle();
|
|
expect(dropdownValue, 40);
|
|
await tester.tap(item40.first);
|
|
await tester.pumpAndSettle();
|
|
|
|
// The vertical center of the selectedItem (item40) should
|
|
// line up with its button counterpart.
|
|
expect(tester.getCenter(item40.first).dy, tester.getCenter(item40.last).dy);
|
|
});
|
|
|
|
testWidgets('DropdownButton menu items do not resize when its route is popped', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/44877.
|
|
const items = <String>['one', 'two', 'three'];
|
|
String? item = items[0];
|
|
late double textScale;
|
|
|
|
await tester.pumpWidget(
|
|
StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return MaterialApp(
|
|
builder: (BuildContext context, Widget? child) {
|
|
textScale = MediaQuery.of(context).textScaler.scale(14) / 14;
|
|
return MediaQuery(
|
|
data: MediaQueryData(textScaler: TextScaler.linear(textScale)),
|
|
child: child!,
|
|
);
|
|
},
|
|
home: Scaffold(
|
|
body: DropdownButton<String>(
|
|
value: item,
|
|
items: items
|
|
.map((String item) => DropdownMenuItem<String>(value: item, child: Text(item)))
|
|
.toList(),
|
|
onChanged: (String? newItem) {
|
|
setState(() {
|
|
item = newItem;
|
|
textScale += 0.1;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
// Verify that the first item is showing.
|
|
expect(find.text('one'), findsOneWidget);
|
|
|
|
// Select a different item to trigger setState, which updates mediaQuery
|
|
// and forces a performLayout on the popped _DropdownRoute. This operation
|
|
// should not cause an exception.
|
|
await tester.tap(find.text('one'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('two').last);
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('two'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('DropdownButton hint is selected item', (WidgetTester tester) async {
|
|
const double hintPaddingOffset = 8;
|
|
const itemValues = <String>['item0', 'item1', 'item2', 'item3'];
|
|
String? selectedItem = 'item0';
|
|
|
|
Widget buildFrame() {
|
|
return MaterialApp(
|
|
home: Scaffold(
|
|
body: ButtonTheme(
|
|
alignedDropdown: true,
|
|
child: DropdownButtonHideUnderline(
|
|
child: Center(
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
// The pretzel below is from an actual app. The price
|
|
// of limited configurability is keeping this working.
|
|
return DropdownButton<String>(
|
|
isExpanded: true,
|
|
elevation: 2,
|
|
hint: LayoutBuilder(
|
|
builder: (BuildContext context, BoxConstraints constraints) {
|
|
// Stack with a positioned widget is used to override the
|
|
// hard coded 16px margin in the dropdown code, so that
|
|
// this hint aligns "properly" with the menu.
|
|
return Stack(
|
|
clipBehavior: Clip.none,
|
|
alignment: Alignment.topCenter,
|
|
children: <Widget>[
|
|
PositionedDirectional(
|
|
width: constraints.maxWidth + hintPaddingOffset,
|
|
start: -hintPaddingOffset,
|
|
top: 4.0,
|
|
child: Text('-$selectedItem-'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
onChanged: (String? value) {
|
|
setState(() {
|
|
selectedItem = value;
|
|
});
|
|
},
|
|
icon: Container(),
|
|
items: itemValues.map<DropdownMenuItem<String>>((String value) {
|
|
return DropdownMenuItem<String>(value: value, child: Text(value));
|
|
}).toList(),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(buildFrame());
|
|
expect(tester.getTopLeft(find.text('-item0-')).dx, 8);
|
|
|
|
// Show the popup menu.
|
|
await tester.tap(find.text('-item0-', skipOffstage: false), warnIfMissed: false);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(tester.getTopLeft(find.text('-item0-')).dx, 8);
|
|
});
|
|
|
|
Finder findInputDecoratorBorderPainter() {
|
|
return find.descendant(
|
|
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_BorderContainer'),
|
|
matching: find.byWidgetPredicate((Widget w) => w is CustomPaint),
|
|
);
|
|
}
|
|
|
|
testWidgets('DropdownButton can be focused, and has focusColor', (WidgetTester tester) async {
|
|
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
|
final buttonKey = UniqueKey();
|
|
final focusNode = FocusNode(debugLabel: 'DropdownButton');
|
|
addTearDown(focusNode.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
buttonKey: buttonKey,
|
|
onChanged: onChanged,
|
|
focusNode: focusNode,
|
|
autofocus: true,
|
|
useMaterial3: false,
|
|
),
|
|
);
|
|
await tester.pumpAndSettle(); // Pump a frame for autofocus to take effect.
|
|
expect(focusNode.hasPrimaryFocus, isTrue);
|
|
expect(
|
|
find.byType(Material),
|
|
paints..rect(
|
|
rect: const Rect.fromLTRB(348.0, 276.0, 452.0, 324.0),
|
|
color: const Color(0x1f000000),
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
buttonKey: buttonKey,
|
|
onChanged: onChanged,
|
|
focusNode: focusNode,
|
|
focusColor: const Color(0xff00ff00),
|
|
useMaterial3: false,
|
|
),
|
|
);
|
|
await tester.pumpAndSettle(); // Pump a frame for autofocus to take effect.
|
|
expect(
|
|
find.byType(Material),
|
|
paints..rect(
|
|
rect: const Rect.fromLTRB(348.0, 276.0, 452.0, 324.0),
|
|
color: const Color(0x1f00ff00),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('DropdownButtonFormField can be focused, and has focusColor', (
|
|
WidgetTester tester,
|
|
) async {
|
|
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
|
final buttonKey = UniqueKey();
|
|
final focusNode = FocusNode(debugLabel: 'DropdownButtonFormField');
|
|
addTearDown(focusNode.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
isFormField: true,
|
|
buttonKey: buttonKey,
|
|
onChanged: onChanged,
|
|
focusNode: focusNode,
|
|
autofocus: true,
|
|
decoration: const InputDecoration(filled: true),
|
|
),
|
|
);
|
|
|
|
await tester.pump(); // Pump a frame for autofocus to take effect.
|
|
expect(focusNode.hasPrimaryFocus, isTrue);
|
|
|
|
// Default focus Color from InputDecorator defaults.
|
|
final ThemeData theme = Theme.of(tester.element(find.byType(InputDecorator)));
|
|
expect(
|
|
findInputDecoratorBorderPainter(),
|
|
paints..path(style: PaintingStyle.fill, color: theme.colorScheme.surfaceContainerHighest),
|
|
);
|
|
|
|
// Focus color from Decoration.
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
isFormField: true,
|
|
buttonKey: buttonKey,
|
|
onChanged: onChanged,
|
|
focusNode: focusNode,
|
|
decoration: const InputDecoration(filled: true, focusColor: Color(0xff00ffff)),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
findInputDecoratorBorderPainter(),
|
|
paints..path(style: PaintingStyle.fill, color: const Color(0xff00ffff)),
|
|
);
|
|
|
|
// Focus color from focusColor property.
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
isFormField: true,
|
|
buttonKey: buttonKey,
|
|
onChanged: onChanged,
|
|
focusNode: focusNode,
|
|
decoration: const InputDecoration(filled: true, focusColor: Color(0xff00ffff)),
|
|
focusColor: const Color(0xff00ff00),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
findInputDecoratorBorderPainter(),
|
|
paints..path(style: PaintingStyle.fill, color: const Color(0xff00ff00)),
|
|
);
|
|
});
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/166642.
|
|
testWidgets('DropdownButtonFormField can replace focusNode properly', (
|
|
WidgetTester tester,
|
|
) async {
|
|
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
|
final buttonKey = UniqueKey();
|
|
var focusNode = FocusNode(debugLabel: 'DropdownButtonFormField');
|
|
addTearDown(() => focusNode.dispose());
|
|
|
|
Widget buildFormField() => buildFrame(
|
|
isFormField: true,
|
|
buttonKey: buttonKey,
|
|
onChanged: onChanged,
|
|
focusNode: focusNode,
|
|
decoration: const InputDecoration(filled: true),
|
|
focusColor: const Color(0xff00ff00),
|
|
);
|
|
|
|
await tester.pumpWidget(buildFormField());
|
|
final Color defaultBorderColor = Theme.of(
|
|
tester.element(find.byType(InputDecorator)),
|
|
).colorScheme.surfaceContainerHighest;
|
|
expect(
|
|
findInputDecoratorBorderPainter(),
|
|
paints..path(style: PaintingStyle.fill, color: defaultBorderColor),
|
|
);
|
|
|
|
// Replace focusNode and request focus.
|
|
focusNode.dispose();
|
|
focusNode = FocusNode(debugLabel: 'DropdownButtonFormField');
|
|
focusNode.requestFocus();
|
|
|
|
await tester.pumpWidget(buildFormField());
|
|
await tester.pump(); // Wait for requestFocus to take effect.
|
|
expect(
|
|
findInputDecoratorBorderPainter(),
|
|
paints..path(style: PaintingStyle.fill, color: const Color(0xff00ff00)),
|
|
);
|
|
|
|
// Replace focusNode and request focus.
|
|
focusNode.dispose();
|
|
focusNode = FocusNode(debugLabel: 'DropdownButtonFormField');
|
|
focusNode.requestFocus();
|
|
|
|
await tester.pumpWidget(buildFormField());
|
|
FocusManager.instance.primaryFocus?.unfocus();
|
|
await tester.pump(); // Wait for unfocus to take effect.
|
|
expect(
|
|
findInputDecoratorBorderPainter(),
|
|
paints..path(style: PaintingStyle.fill, color: defaultBorderColor),
|
|
);
|
|
});
|
|
|
|
testWidgets('DropdownButtonFormField should properly dispose its internal FocusNode '
|
|
'when replaced by an external FocusNode', (WidgetTester tester) async {
|
|
final buttonKey = UniqueKey();
|
|
FocusNode? focusNode;
|
|
addTearDown(() => focusNode?.dispose());
|
|
|
|
Widget buildFormField() => buildFrame(
|
|
isFormField: true,
|
|
buttonKey: buttonKey,
|
|
onChanged: onChanged,
|
|
focusNode: focusNode,
|
|
);
|
|
|
|
await tester.pumpWidget(buildFormField());
|
|
final FocusNode internalNode = tester
|
|
.widget<Focus>(
|
|
find
|
|
.descendant(of: find.byType(DropdownButton<String>), matching: find.byType(Focus))
|
|
.first,
|
|
)
|
|
.focusNode!;
|
|
|
|
// Replace internal FocusNode with external FocusNode.
|
|
focusNode = FocusNode(debugLabel: 'DropdownButtonFormField');
|
|
await tester.pumpWidget(buildFormField());
|
|
|
|
expect(
|
|
internalNode.dispose,
|
|
throwsA(
|
|
isA<FlutterError>().having(
|
|
(FlutterError error) => error.message,
|
|
'message',
|
|
startsWith('A FocusNode was used after being disposed.'),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/147069.
|
|
testWidgets('DropdownButtonFormField can be hovered', (WidgetTester tester) async {
|
|
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
|
final buttonKey = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
buildFrame(isFormField: true, buttonKey: buttonKey, onChanged: onChanged),
|
|
);
|
|
await tester.pump();
|
|
|
|
// Check inputDecorator.isHovering value because DropdownButtonFormField
|
|
// delegates to the InputDecorator which manages hover overlay.
|
|
InputDecorator inputDecorator = tester.widget(find.byType(InputDecorator));
|
|
expect(inputDecorator.isHovering, false);
|
|
|
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
await gesture.moveTo(tester.getCenter(find.byKey(buttonKey)));
|
|
await tester.pump();
|
|
|
|
inputDecorator = tester.widget(find.byType(InputDecorator));
|
|
expect(inputDecorator.isHovering, true);
|
|
});
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/151460.
|
|
testWidgets('DropdownButtonFormField has hover color', (WidgetTester tester) async {
|
|
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
|
final buttonKey = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
isFormField: true,
|
|
buttonKey: buttonKey,
|
|
onChanged: onChanged,
|
|
// Setting InputDecoration.filled to true is required to get overlay showing.
|
|
decoration: const InputDecoration(filled: true),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
await gesture.moveTo(tester.getCenter(find.byKey(buttonKey)));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 15)); // Hover animation.
|
|
|
|
// Default hover color.
|
|
final ThemeData theme = Theme.of(tester.element(find.byType(InputDecorator)));
|
|
expect(
|
|
findInputDecoratorBorderPainter(),
|
|
paints..path(
|
|
style: PaintingStyle.fill,
|
|
color: Color.alphaBlend(theme.hoverColor, theme.colorScheme.surfaceContainerHighest),
|
|
),
|
|
);
|
|
|
|
// Custom hover color.
|
|
const hoverColor = Color(0xaa00ff00);
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
isFormField: true,
|
|
buttonKey: buttonKey,
|
|
onChanged: onChanged,
|
|
decoration: const InputDecoration(filled: true, hoverColor: hoverColor),
|
|
),
|
|
);
|
|
expect(
|
|
findInputDecoratorBorderPainter(),
|
|
paints..path(
|
|
style: PaintingStyle.fill,
|
|
color: Color.alphaBlend(hoverColor, theme.colorScheme.surfaceContainerHighest),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets("DropdownButton won't be focused if not enabled", (WidgetTester tester) async {
|
|
final buttonKey = UniqueKey();
|
|
final focusNode = FocusNode(debugLabel: 'DropdownButton');
|
|
addTearDown(focusNode.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
buttonKey: buttonKey,
|
|
focusNode: focusNode,
|
|
autofocus: true,
|
|
focusColor: const Color(0xff00ff00),
|
|
),
|
|
);
|
|
await tester.pump(); // Pump a frame for autofocus to take effect (although it shouldn't).
|
|
expect(focusNode.hasPrimaryFocus, isFalse);
|
|
expect(
|
|
find.byKey(buttonKey),
|
|
isNot(
|
|
paints..rrect(
|
|
rrect: const RRect.fromLTRBXY(0.0, 0.0, 104.0, 48.0, 4.0, 4.0),
|
|
color: const Color(0xff00ff00),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('DropdownButton is activated with the enter key', (WidgetTester tester) async {
|
|
final focusNode = FocusNode(debugLabel: 'DropdownButton');
|
|
addTearDown(focusNode.dispose);
|
|
String? value = 'one';
|
|
|
|
Widget buildFrame() {
|
|
return MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return DropdownButton<String>(
|
|
focusNode: focusNode,
|
|
autofocus: true,
|
|
onChanged: (String? newValue) {
|
|
setState(() {
|
|
value = newValue;
|
|
});
|
|
},
|
|
value: value,
|
|
itemHeight: null,
|
|
items: menuItems.map<DropdownMenuItem<String>>((String item) {
|
|
return DropdownMenuItem<String>(
|
|
key: ValueKey<String>(item),
|
|
value: item,
|
|
child: Text(item, key: ValueKey<String>('${item}Text')),
|
|
);
|
|
}).toList(),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(buildFrame());
|
|
await tester.pump(); // Pump a frame for autofocus to take effect.
|
|
expect(focusNode.hasPrimaryFocus, isTrue);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
|
|
expect(value, equals('one'));
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab); // Focus 'two'
|
|
await tester.pump();
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter); // Select 'two'.
|
|
await tester.pump();
|
|
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
|
|
|
|
expect(value, equals('two'));
|
|
});
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/77655.
|
|
testWidgets('DropdownButton selecting a null valued item should be selected', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final items = <MapEntry<String?, String>>[
|
|
const MapEntry<String?, String>(null, 'None'),
|
|
const MapEntry<String?, String>('one', 'One'),
|
|
const MapEntry<String?, String>('two', 'Two'),
|
|
];
|
|
String? selectedItem = 'one';
|
|
|
|
await tester.pumpWidget(
|
|
StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return MaterialApp(
|
|
home: Scaffold(
|
|
body: DropdownButton<String>(
|
|
value: selectedItem,
|
|
onChanged: (String? string) {
|
|
setState(() {
|
|
selectedItem = string;
|
|
});
|
|
},
|
|
items: items.map((MapEntry<String?, String> item) {
|
|
return DropdownMenuItem<String>(value: item.key, child: Text(item.value));
|
|
}).toList(),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('One'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('None').last);
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('None'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('DropdownButton is activated with the space key', (WidgetTester tester) async {
|
|
final focusNode = FocusNode(debugLabel: 'DropdownButton');
|
|
addTearDown(focusNode.dispose);
|
|
String? value = 'one';
|
|
|
|
Widget buildFrame() {
|
|
return MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return DropdownButton<String>(
|
|
focusNode: focusNode,
|
|
autofocus: true,
|
|
onChanged: (String? newValue) {
|
|
setState(() {
|
|
value = newValue;
|
|
});
|
|
},
|
|
value: value,
|
|
itemHeight: null,
|
|
items: menuItems.map<DropdownMenuItem<String>>((String item) {
|
|
return DropdownMenuItem<String>(
|
|
key: ValueKey<String>(item),
|
|
value: item,
|
|
child: Text(item, key: ValueKey<String>('${item}Text')),
|
|
);
|
|
}).toList(),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(buildFrame());
|
|
await tester.pump(); // Pump a frame for autofocus to take effect.
|
|
expect(focusNode.hasPrimaryFocus, isTrue);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
|
|
expect(value, equals('one'));
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab); // Focus 'two'
|
|
await tester.pump();
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.space); // Select 'two'.
|
|
await tester.pump();
|
|
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
|
|
|
|
expect(value, equals('two'));
|
|
});
|
|
|
|
testWidgets('Selected element is focused when dropdown is opened', (WidgetTester tester) async {
|
|
final focusNode = FocusNode(debugLabel: 'DropdownButton');
|
|
addTearDown(focusNode.dispose);
|
|
String? value = 'one';
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return DropdownButton<String>(
|
|
focusNode: focusNode,
|
|
autofocus: true,
|
|
onChanged: (String? newValue) {
|
|
setState(() {
|
|
value = newValue;
|
|
});
|
|
},
|
|
value: value,
|
|
itemHeight: null,
|
|
items: menuItems.map<DropdownMenuItem<String>>((String item) {
|
|
return DropdownMenuItem<String>(
|
|
key: ValueKey<String>(item),
|
|
value: item,
|
|
child: Text(item, key: ValueKey<String>('Text $item')),
|
|
);
|
|
}).toList(),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pump(); // Pump a frame for autofocus to take effect.
|
|
expect(focusNode.hasPrimaryFocus, isTrue);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu open animation
|
|
expect(value, equals('one'));
|
|
expect(
|
|
Focus.of(tester.element(find.byKey(const ValueKey<String>('one')).last)).hasPrimaryFocus,
|
|
isTrue,
|
|
);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab); // Focus 'two'
|
|
await tester.pump();
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter); // Select 'two' and close the dropdown.
|
|
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu close animation
|
|
|
|
expect(value, equals('two'));
|
|
|
|
// Now make sure that "two" is focused when we re-open the dropdown.
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu open animation
|
|
expect(value, equals('two'));
|
|
final Element element = tester.element(find.byKey(const ValueKey<String>('two')).last);
|
|
final FocusNode node = Focus.of(element);
|
|
expect(node.hasFocus, isTrue);
|
|
});
|
|
|
|
testWidgets(
|
|
'Selected element is correctly focused with dropdown that more items than fit on the screen',
|
|
(WidgetTester tester) async {
|
|
final focusNode = FocusNode(debugLabel: 'DropdownButton');
|
|
addTearDown(focusNode.dispose);
|
|
int? value = 1;
|
|
final hugeMenuItems = List<int>.generate(50, (int index) => index);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return DropdownButton<int>(
|
|
focusNode: focusNode,
|
|
autofocus: true,
|
|
onChanged: (int? newValue) {
|
|
setState(() {
|
|
value = newValue;
|
|
});
|
|
},
|
|
value: value,
|
|
itemHeight: null,
|
|
items: hugeMenuItems.map<DropdownMenuItem<int>>((int item) {
|
|
return DropdownMenuItem<int>(
|
|
key: ValueKey<int>(item),
|
|
value: item,
|
|
child: Text(item.toString(), key: ValueKey<String>('Text $item')),
|
|
);
|
|
}).toList(),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pump(); // Pump a frame for autofocus to take effect.
|
|
expect(focusNode.hasPrimaryFocus, isTrue);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu open animation
|
|
expect(value, equals(1));
|
|
expect(
|
|
Focus.of(tester.element(find.byKey(const ValueKey<int>(1)).last)).hasPrimaryFocus,
|
|
isTrue,
|
|
);
|
|
|
|
for (var i = 0; i < 41; ++i) {
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab); // Move to the next one.
|
|
await tester.pumpAndSettle(
|
|
const Duration(milliseconds: 200),
|
|
); // Wait for it to animate the menu.
|
|
}
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter); // Select '42' and close the dropdown.
|
|
await tester.pumpAndSettle(const Duration(seconds: 1)); // Finish the menu close animation
|
|
expect(value, equals(42));
|
|
|
|
// Now make sure that "42" is focused when we re-open the dropdown.
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu open animation
|
|
expect(value, equals(42));
|
|
final Element element = tester.element(find.byKey(const ValueKey<int>(42)).last);
|
|
final FocusNode node = Focus.of(element);
|
|
expect(node.hasFocus, isTrue);
|
|
},
|
|
);
|
|
|
|
testWidgets("Having a focused element doesn't interrupt scroll when flung by touch", (
|
|
WidgetTester tester,
|
|
) async {
|
|
final focusNode = FocusNode(debugLabel: 'DropdownButton');
|
|
addTearDown(focusNode.dispose);
|
|
int? value = 1;
|
|
final hugeMenuItems = List<int>.generate(100, (int index) => index);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(useMaterial3: false),
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return DropdownButton<int>(
|
|
focusNode: focusNode,
|
|
autofocus: true,
|
|
onChanged: (int? newValue) {
|
|
setState(() {
|
|
value = newValue;
|
|
});
|
|
},
|
|
value: value,
|
|
itemHeight: null,
|
|
items: hugeMenuItems.map<DropdownMenuItem<int>>((int item) {
|
|
return DropdownMenuItem<int>(
|
|
key: ValueKey<int>(item),
|
|
value: item,
|
|
child: Text(item.toString(), key: ValueKey<String>('Text $item')),
|
|
);
|
|
}).toList(),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pump(); // Pump a frame for autofocus to take effect.
|
|
expect(focusNode.hasPrimaryFocus, isTrue);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
|
await tester.pumpAndSettle();
|
|
expect(value, equals(1));
|
|
expect(
|
|
Focus.of(tester.element(find.byKey(const ValueKey<int>(1)).last)).hasPrimaryFocus,
|
|
isTrue,
|
|
);
|
|
|
|
// Move to an item very far down the menu.
|
|
for (var i = 0; i < 90; ++i) {
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab); // Move to the next one.
|
|
await tester.pumpAndSettle(); // Wait for it to animate the menu.
|
|
}
|
|
expect(
|
|
Focus.of(tester.element(find.byKey(const ValueKey<int>(91)).last)).hasPrimaryFocus,
|
|
isTrue,
|
|
);
|
|
|
|
// Scroll back to the top using touch, and make sure we end up there.
|
|
final Finder menu = find.byWidgetPredicate((Widget widget) {
|
|
return widget.runtimeType.toString().startsWith('_DropdownMenu<');
|
|
});
|
|
final Rect menuRect = tester.getRect(menu).shift(tester.getTopLeft(menu));
|
|
for (var i = 0; i < 10; ++i) {
|
|
await tester.fling(menu, Offset(0.0, menuRect.height), 10.0);
|
|
}
|
|
await tester.pumpAndSettle();
|
|
|
|
// Make sure that we made it to the top and something didn't stop the
|
|
// scroll.
|
|
expect(find.byKey(const ValueKey<int>(1)), findsNWidgets(2));
|
|
expect(
|
|
tester.getRect(find.byKey(const ValueKey<int>(1)).last),
|
|
equals(const Rect.fromLTRB(372.0, 104.0, 436.0, 152.0)),
|
|
);
|
|
|
|
// Scrolling to the top again has removed the one the focus was on from the
|
|
// tree, causing it to lose focus.
|
|
expect(
|
|
Focus.of(
|
|
tester.element(find.byKey(const ValueKey<int>(91), skipOffstage: false).last),
|
|
).hasPrimaryFocus,
|
|
isFalse,
|
|
);
|
|
});
|
|
|
|
testWidgets('DropdownButton onTap callback can request focus', (WidgetTester tester) async {
|
|
final focusNode = FocusNode(debugLabel: 'DropdownButton')..addListener(() {});
|
|
addTearDown(focusNode.dispose);
|
|
int? value = 1;
|
|
final hugeMenuItems = List<int>.generate(100, (int index) => index);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return DropdownButton<int>(
|
|
focusNode: focusNode,
|
|
onChanged: (int? newValue) {
|
|
setState(() {
|
|
value = newValue;
|
|
});
|
|
},
|
|
value: value,
|
|
itemHeight: null,
|
|
items: hugeMenuItems.map<DropdownMenuItem<int>>((int item) {
|
|
return DropdownMenuItem<int>(
|
|
key: ValueKey<int>(item),
|
|
value: item,
|
|
child: Text(item.toString()),
|
|
);
|
|
}).toList(),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pump(); // Pump a frame for autofocus to take effect.
|
|
expect(focusNode.hasPrimaryFocus, isFalse);
|
|
|
|
await tester.tap(find.text('1'));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Close the dropdown menu.
|
|
await tester.tapAt(const Offset(1.0, 1.0));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(focusNode.hasPrimaryFocus, isTrue);
|
|
});
|
|
|
|
testWidgets('DropdownButton changes selected item with arrow keys', (WidgetTester tester) async {
|
|
final focusNode = FocusNode(debugLabel: 'DropdownButton');
|
|
addTearDown(focusNode.dispose);
|
|
String? value = 'one';
|
|
|
|
Widget buildFrame() {
|
|
return MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return DropdownButton<String>(
|
|
focusNode: focusNode,
|
|
autofocus: true,
|
|
onChanged: (String? newValue) {
|
|
setState(() {
|
|
value = newValue;
|
|
});
|
|
},
|
|
value: value,
|
|
itemHeight: null,
|
|
items: menuItems.map<DropdownMenuItem<String>>((String item) {
|
|
return DropdownMenuItem<String>(
|
|
key: ValueKey<String>(item),
|
|
value: item,
|
|
child: Text(item, key: ValueKey<String>('${item}Text')),
|
|
);
|
|
}).toList(),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(buildFrame());
|
|
await tester.pump(); // Pump a frame for autofocus to take effect.
|
|
expect(focusNode.hasPrimaryFocus, isTrue);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
|
|
expect(value, equals('one'));
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); // Focus 'two'.
|
|
await tester.pump();
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); // Focus 'three'.
|
|
await tester.pump();
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); // Back to 'two'.
|
|
await tester.pump();
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter); // Select 'two'.
|
|
await tester.pump();
|
|
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
|
|
|
|
expect(value, equals('two'));
|
|
});
|
|
|
|
testWidgets('DropdownButton onTap callback is called when defined', (WidgetTester tester) async {
|
|
var dropdownButtonTapCounter = 0;
|
|
String? value = 'one';
|
|
|
|
void onChanged(String? newValue) {
|
|
value = newValue;
|
|
}
|
|
|
|
void onTap() {
|
|
dropdownButtonTapCounter += 1;
|
|
}
|
|
|
|
Widget build() => buildFrame(initialValue: value, onChanged: onChanged, onTap: onTap);
|
|
await tester.pumpWidget(build());
|
|
|
|
expect(dropdownButtonTapCounter, 0);
|
|
|
|
// Tap dropdown button.
|
|
await tester.tap(find.text('one'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(value, equals('one'));
|
|
expect(dropdownButtonTapCounter, 1); // Should update counter.
|
|
|
|
// Tap dropdown menu item.
|
|
await tester.tap(find.text('three').last);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(value, equals('three'));
|
|
expect(dropdownButtonTapCounter, 1); // Should not change.
|
|
|
|
// Tap dropdown button again.
|
|
await tester.tap(find.text('three', skipOffstage: false), warnIfMissed: false);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(value, equals('three'));
|
|
expect(dropdownButtonTapCounter, 2); // Should update counter.
|
|
|
|
// Tap dropdown menu item.
|
|
await tester.tap(find.text('two').last);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(value, equals('two'));
|
|
expect(dropdownButtonTapCounter, 2); // Should not change.
|
|
});
|
|
|
|
testWidgets('DropdownMenuItem onTap callback is called when defined', (
|
|
WidgetTester tester,
|
|
) async {
|
|
String? value = 'one';
|
|
final menuItemTapCounters = <int>[0, 0, 0, 0];
|
|
void onChanged(String? newValue) {
|
|
value = newValue;
|
|
}
|
|
|
|
final onTapCallbacks = <VoidCallback>[
|
|
() {
|
|
menuItemTapCounters[0] += 1;
|
|
},
|
|
() {
|
|
menuItemTapCounters[1] += 1;
|
|
},
|
|
() {
|
|
menuItemTapCounters[2] += 1;
|
|
},
|
|
() {
|
|
menuItemTapCounters[3] += 1;
|
|
},
|
|
];
|
|
|
|
var currentIndex = -1;
|
|
await tester.pumpWidget(
|
|
TestApp(
|
|
textDirection: TextDirection.ltr,
|
|
child: Material(
|
|
child: RepaintBoundary(
|
|
child: DropdownButton<String>(
|
|
value: value,
|
|
onChanged: onChanged,
|
|
items: menuItems.map<DropdownMenuItem<String>>((String item) {
|
|
currentIndex += 1;
|
|
return DropdownMenuItem<String>(
|
|
value: item,
|
|
onTap: onTapCallbacks[currentIndex],
|
|
child: Text(item),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Tap dropdown button.
|
|
await tester.tap(find.text('one'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(value, equals('one'));
|
|
// Counters should still be zero.
|
|
expect(menuItemTapCounters, <int>[0, 0, 0, 0]);
|
|
|
|
// Tap dropdown menu item.
|
|
await tester.tap(find.text('three').last);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Should update the counter for the third item (second index).
|
|
expect(value, equals('three'));
|
|
expect(menuItemTapCounters, <int>[0, 0, 1, 0]);
|
|
|
|
// Tap dropdown button again.
|
|
await tester.tap(find.text('three', skipOffstage: false), warnIfMissed: false);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Should not change.
|
|
expect(value, equals('three'));
|
|
expect(menuItemTapCounters, <int>[0, 0, 1, 0]);
|
|
|
|
// Tap dropdown menu item.
|
|
await tester.tap(find.text('two').last);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Should update the counter for the second item (first index).
|
|
expect(value, equals('two'));
|
|
expect(menuItemTapCounters, <int>[0, 1, 1, 0]);
|
|
|
|
// Tap dropdown button again.
|
|
await tester.tap(find.text('two', skipOffstage: false), warnIfMissed: false);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Should not change.
|
|
expect(value, equals('two'));
|
|
expect(menuItemTapCounters, <int>[0, 1, 1, 0]);
|
|
|
|
// Tap the already selected menu item
|
|
await tester.tap(find.text('two').last);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Should update the counter for the second item (first index), even
|
|
// though it was already selected.
|
|
expect(value, equals('two'));
|
|
expect(menuItemTapCounters, <int>[0, 2, 1, 0]);
|
|
});
|
|
|
|
testWidgets(
|
|
'Does not crash when option is selected without waiting for opening animation to complete',
|
|
(WidgetTester tester) async {
|
|
// Regression test for b/171846624.
|
|
|
|
final options = <String>['first', 'second', 'third'];
|
|
String? value = options.first;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) => DropdownButton<String>(
|
|
value: value,
|
|
items: options
|
|
.map((String s) => DropdownMenuItem<String>(value: s, child: Text(s)))
|
|
.toList(),
|
|
onChanged: (String? v) {
|
|
setState(() {
|
|
value = v;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(find.text('first').hitTestable(), findsOneWidget);
|
|
expect(find.text('second').hitTestable(), findsNothing);
|
|
expect(find.text('third').hitTestable(), findsNothing);
|
|
|
|
// Open dropdown.
|
|
await tester.tap(find.text('first').hitTestable());
|
|
await tester.pump();
|
|
|
|
expect(find.text('third').hitTestable(), findsOneWidget);
|
|
expect(find.text('first').hitTestable(), findsOneWidget);
|
|
expect(find.text('second').hitTestable(), findsOneWidget);
|
|
|
|
// Deliberately not waiting for opening animation to complete!
|
|
|
|
// Select an option in dropdown.
|
|
await tester.tap(find.text('third').hitTestable());
|
|
await tester.pump();
|
|
expect(find.text('third').hitTestable(), findsOneWidget);
|
|
expect(find.text('first').hitTestable(), findsNothing);
|
|
expect(find.text('second').hitTestable(), findsNothing);
|
|
},
|
|
);
|
|
|
|
testWidgets('Dropdown menu should persistently show a scrollbar if it is scrollable', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
initialValue: '0',
|
|
// menu is short enough to fit onto the screen.
|
|
items: List<String>.generate(/*length=*/ 10, (int index) => index.toString()),
|
|
onChanged: onChanged,
|
|
),
|
|
);
|
|
await tester.tap(find.text('0'));
|
|
await tester.pumpAndSettle();
|
|
|
|
ScrollController scrollController = PrimaryScrollController.of(
|
|
tester.element(find.byType(ListView)),
|
|
);
|
|
// The scrollbar shouldn't show if the list fits into the screen.
|
|
expect(scrollController.position.maxScrollExtent, 0);
|
|
expect(find.byType(Scrollbar), isNot(paints..rect()));
|
|
|
|
await tester.tap(find.text('0').last);
|
|
await tester.pumpAndSettle();
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
initialValue: '0',
|
|
// menu is too long to fit onto the screen.
|
|
items: List<String>.generate(/*length=*/ 100, (int index) => index.toString()),
|
|
onChanged: onChanged,
|
|
),
|
|
);
|
|
await tester.tap(find.text('0'));
|
|
await tester.pumpAndSettle();
|
|
|
|
scrollController = PrimaryScrollController.of(tester.element(find.byType(ListView)));
|
|
// The scrollbar is shown when the list is longer than the height of the screen.
|
|
expect(scrollController.position.maxScrollExtent > 0, isTrue);
|
|
expect(find.byType(Scrollbar), paints..rect());
|
|
});
|
|
|
|
testWidgets(
|
|
"Dropdown menu's maximum height should be influenced by DropdownButton.menuMaxHeight.",
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
initialValue: '0',
|
|
items: List<String>.generate(/*length=*/ 64, (int index) => index.toString()),
|
|
onChanged: onChanged,
|
|
),
|
|
);
|
|
await tester.tap(find.text('0'));
|
|
await tester.pumpAndSettle();
|
|
|
|
final Element element = tester.element(find.byType(ListView));
|
|
double menuHeight = element.size!.height;
|
|
// The default maximum height should be one item height from the edge.
|
|
// https://material.io/design/components/menus.html#usage
|
|
final double mediaHeight = MediaQuery.of(element).size.height;
|
|
final double defaultMenuHeight = mediaHeight - (2 * kMinInteractiveDimension);
|
|
expect(menuHeight, defaultMenuHeight);
|
|
|
|
await tester.tap(find.text('0').last);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Set menuMaxHeight which is less than defaultMenuHeight
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
initialValue: '0',
|
|
items: List<String>.generate(/*length=*/ 64, (int index) => index.toString()),
|
|
onChanged: onChanged,
|
|
menuMaxHeight: 7 * kMinInteractiveDimension,
|
|
),
|
|
);
|
|
await tester.tap(find.text('0'));
|
|
await tester.pumpAndSettle();
|
|
|
|
menuHeight = tester.element(find.byType(ListView)).size!.height;
|
|
|
|
expect(menuHeight == defaultMenuHeight, isFalse);
|
|
expect(menuHeight, kMinInteractiveDimension * 7);
|
|
|
|
await tester.tap(find.text('0').last);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Set menuMaxHeight which is greater than defaultMenuHeight
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
initialValue: '0',
|
|
items: List<String>.generate(/*length=*/ 64, (int index) => index.toString()),
|
|
onChanged: onChanged,
|
|
menuMaxHeight: mediaHeight,
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('0'));
|
|
await tester.pumpAndSettle();
|
|
|
|
menuHeight = tester.element(find.byType(ListView)).size!.height;
|
|
expect(menuHeight, defaultMenuHeight);
|
|
},
|
|
);
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/89029
|
|
testWidgets('menu position test with `menuMaxHeight`', (WidgetTester tester) async {
|
|
final Key buttonKey = UniqueKey();
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
buttonKey: buttonKey,
|
|
initialValue: '6',
|
|
items: List<String>.generate(/*length=*/ 64, (int index) => index.toString()),
|
|
onChanged: onChanged,
|
|
menuMaxHeight: 2 * kMinInteractiveDimension,
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('6'));
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderBox menuBox = tester.renderObject(find.byType(ListView));
|
|
final RenderBox buttonBox = tester.renderObject(find.byKey(buttonKey));
|
|
// The menu's bottom should align with the drop-button's bottom.
|
|
expect(
|
|
menuBox.localToGlobal(menuBox.paintBounds.bottomCenter).dy,
|
|
buttonBox.localToGlobal(buttonBox.paintBounds.bottomCenter).dy,
|
|
);
|
|
});
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/76614
|
|
testWidgets('Do not crash if used in very short screen', (WidgetTester tester) async {
|
|
// The default item height is 48.0 pixels and needs two items padding since
|
|
// the menu requires empty space surrounding the menu. Finally, the constraint height
|
|
// is 47.0 pixels for the menu rendering.
|
|
tester.view.physicalSize = const Size(800.0, 48.0 * 3 - 1.0);
|
|
tester.view.devicePixelRatio = 1;
|
|
addTearDown(tester.view.reset);
|
|
|
|
const value = 'foo';
|
|
final itemKey = UniqueKey();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(useMaterial3: false),
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: DropdownButton<String>(
|
|
value: value,
|
|
items: <DropdownMenuItem<String>>[
|
|
DropdownMenuItem<String>(key: itemKey, value: value, child: const Text(value)),
|
|
],
|
|
onChanged: (_) {},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text(value));
|
|
await tester.pumpAndSettle();
|
|
|
|
final List<RenderBox> itemBoxes = tester
|
|
.renderObjectList<RenderBox>(find.byKey(itemKey))
|
|
.toList();
|
|
expect(itemBoxes[0].localToGlobal(Offset.zero).dx, 364.0);
|
|
expect(itemBoxes[0].localToGlobal(Offset.zero).dy, 47.5);
|
|
|
|
expect(itemBoxes[1].localToGlobal(Offset.zero).dx, 364.0);
|
|
expect(itemBoxes[1].localToGlobal(Offset.zero).dy, 47.5);
|
|
|
|
expect(
|
|
find.ancestor(of: find.text(value).last, matching: find.byType(CustomPaint)).at(2),
|
|
paints
|
|
..save()
|
|
..rrect()
|
|
..rrect()
|
|
..rrect()
|
|
// The height of menu is 47.0.
|
|
..rrect(
|
|
rrect: const RRect.fromLTRBXY(0.0, 0.0, 112.0, 47.0, 2.0, 2.0),
|
|
color: Colors.grey[50],
|
|
hasMaskFilter: false,
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('Tapping a disabled item should not close DropdownButton', (
|
|
WidgetTester tester,
|
|
) async {
|
|
String? value = 'first';
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) => DropdownButton<String>(
|
|
value: value,
|
|
items: const <DropdownMenuItem<String>>[
|
|
DropdownMenuItem<String>(enabled: false, child: Text('disabled')),
|
|
DropdownMenuItem<String>(value: 'first', child: Text('first')),
|
|
DropdownMenuItem<String>(value: 'second', child: Text('second')),
|
|
],
|
|
onChanged: (String? newValue) {
|
|
setState(() {
|
|
value = newValue;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open dropdown.
|
|
await tester.tap(find.text('first').hitTestable());
|
|
await tester.pumpAndSettle();
|
|
|
|
// Tap on a disabled item.
|
|
await tester.tap(find.text('disabled').hitTestable());
|
|
await tester.pumpAndSettle();
|
|
|
|
// The dropdown should still be open, i.e., there should be one widget with 'second' text.
|
|
expect(find.text('second').hitTestable(), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('Disabled item should not be focusable', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: DropdownButton<String>(
|
|
value: 'enabled',
|
|
onChanged: onChanged,
|
|
items: const <DropdownMenuItem<String>>[
|
|
DropdownMenuItem<String>(enabled: false, child: Text('disabled')),
|
|
DropdownMenuItem<String>(value: 'enabled', child: Text('enabled')),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open dropdown.
|
|
await tester.tap(find.text('enabled').hitTestable());
|
|
await tester.pumpAndSettle();
|
|
|
|
// The `FocusNode` of [disabledItem] should be `null` as enabled is false.
|
|
final Element disabledItem = tester.element(find.text('disabled').hitTestable());
|
|
expect(
|
|
Focus.maybeOf(disabledItem),
|
|
null,
|
|
reason: 'Disabled menu item should not be able to request focus',
|
|
);
|
|
});
|
|
|
|
testWidgets('alignment test', (WidgetTester tester) async {
|
|
final Key buttonKey = UniqueKey();
|
|
Widget buildFrame({AlignmentGeometry? buttonAlignment, AlignmentGeometry? menuAlignment}) {
|
|
return MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: DropdownButton<String>(
|
|
key: buttonKey,
|
|
alignment: buttonAlignment ?? AlignmentDirectional.centerStart,
|
|
value: 'enabled',
|
|
onChanged: onChanged,
|
|
items: <DropdownMenuItem<String>>[
|
|
DropdownMenuItem<String>(
|
|
alignment: buttonAlignment ?? AlignmentDirectional.centerStart,
|
|
enabled: false,
|
|
child: const Text('disabled'),
|
|
),
|
|
DropdownMenuItem<String>(
|
|
alignment: buttonAlignment ?? AlignmentDirectional.centerStart,
|
|
value: 'enabled',
|
|
child: const Text('enabled'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(buildFrame());
|
|
|
|
final RenderBox buttonBox = tester.renderObject(find.byKey(buttonKey));
|
|
RenderBox selectedItemBox = tester.renderObject(find.text('enabled'));
|
|
// Default to center-start aligned.
|
|
expect(
|
|
buttonBox.localToGlobal(Offset(0.0, buttonBox.size.height / 2.0)),
|
|
selectedItemBox.localToGlobal(Offset(0.0, selectedItemBox.size.height / 2.0)),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
buttonAlignment: AlignmentDirectional.center,
|
|
menuAlignment: AlignmentDirectional.center,
|
|
),
|
|
);
|
|
|
|
selectedItemBox = tester.renderObject(find.text('enabled'));
|
|
// Should be center-center aligned, the icon size is 24.0 pixels.
|
|
expect(
|
|
buttonBox.localToGlobal(
|
|
Offset((buttonBox.size.width - 24.0) / 2.0, buttonBox.size.height / 2.0),
|
|
),
|
|
offsetMoreOrLessEquals(
|
|
selectedItemBox.localToGlobal(
|
|
Offset(selectedItemBox.size.width / 2.0, selectedItemBox.size.height / 2.0),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open dropdown.
|
|
await tester.tap(find.text('enabled').hitTestable());
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderBox selectedItemBoxInMenu = tester
|
|
.renderObjectList<RenderBox>(find.text('enabled'))
|
|
.toList()[1];
|
|
final Finder menu = find.byWidgetPredicate((Widget widget) {
|
|
return widget.runtimeType.toString().startsWith('_DropdownMenu<');
|
|
});
|
|
final Rect menuRect = tester.getRect(menu);
|
|
final Offset center = selectedItemBoxInMenu.localToGlobal(
|
|
Offset(selectedItemBoxInMenu.size.width / 2.0, selectedItemBoxInMenu.size.height / 2.0),
|
|
);
|
|
|
|
expect(center.dx, moreOrLessEquals(menuRect.topCenter.dx));
|
|
expect(
|
|
center.dy,
|
|
moreOrLessEquals(
|
|
selectedItemBox
|
|
.localToGlobal(
|
|
Offset(selectedItemBox.size.width / 2.0, selectedItemBox.size.height / 2.0),
|
|
)
|
|
.dy,
|
|
),
|
|
);
|
|
});
|
|
|
|
group('feedback', () {
|
|
late FeedbackTester feedback;
|
|
|
|
setUp(() {
|
|
feedback = FeedbackTester();
|
|
});
|
|
|
|
tearDown(() {
|
|
feedback.dispose();
|
|
});
|
|
|
|
Widget feedbackBoilerplate({bool? enableFeedback}) {
|
|
return MaterialApp(
|
|
home: Material(
|
|
child: DropdownButton<String>(
|
|
value: 'One',
|
|
enableFeedback: enableFeedback,
|
|
underline: Container(height: 2, color: Colors.deepPurpleAccent),
|
|
onChanged: (String? value) {},
|
|
items: <String>['One', 'Two'].map<DropdownMenuItem<String>>((String value) {
|
|
return DropdownMenuItem<String>(value: value, child: Text(value));
|
|
}).toList(),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
testWidgets('Dropdown with enabled feedback', (WidgetTester tester) async {
|
|
const enableFeedback = true;
|
|
|
|
await tester.pumpWidget(feedbackBoilerplate(enableFeedback: enableFeedback));
|
|
|
|
await tester.tap(find.text('One'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.widgetWithText(InkWell, 'One').last);
|
|
await tester.pumpAndSettle();
|
|
expect(feedback.clickSoundCount, 1);
|
|
expect(feedback.hapticCount, 0);
|
|
});
|
|
|
|
testWidgets('Dropdown with disabled feedback', (WidgetTester tester) async {
|
|
const enableFeedback = false;
|
|
|
|
await tester.pumpWidget(feedbackBoilerplate(enableFeedback: enableFeedback));
|
|
|
|
await tester.tap(find.text('One'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.widgetWithText(InkWell, 'One').last);
|
|
await tester.pumpAndSettle();
|
|
expect(feedback.clickSoundCount, 0);
|
|
expect(feedback.hapticCount, 0);
|
|
});
|
|
|
|
testWidgets('Dropdown with enabled feedback by default', (WidgetTester tester) async {
|
|
await tester.pumpWidget(feedbackBoilerplate());
|
|
|
|
await tester.tap(find.text('One'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.widgetWithText(InkWell, 'Two').last);
|
|
await tester.pumpAndSettle();
|
|
expect(feedback.clickSoundCount, 1);
|
|
expect(feedback.hapticCount, 0);
|
|
});
|
|
});
|
|
|
|
testWidgets('DropdownMenuItem has expected default mouse cursor on hover', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const menuKey = Key('testDropdownMenuButton');
|
|
const itemKey = Key('testDropdownMenuItem');
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: DropdownButton<String>(
|
|
key: menuKey,
|
|
onChanged: (String? value) {},
|
|
items: const <DropdownMenuItem<String>>[
|
|
DropdownMenuItem<String>(
|
|
key: itemKey,
|
|
value: 'testDropdownMenuItem',
|
|
child: Text('TestDropdownMenuItem'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open DropdownButton.
|
|
await tester.tap(find.byKey(menuKey));
|
|
await tester.pump();
|
|
|
|
// Find DropdownMenuItem.
|
|
final Finder menuItemFinder = find.byKey(itemKey);
|
|
final Offset onMenuItem = tester.getCenter(menuItemFinder);
|
|
final Offset offMenuItem = tester.getBottomRight(menuItemFinder) + const Offset(1, 1);
|
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
|
|
await gesture.addPointer(location: onMenuItem);
|
|
await tester.pump();
|
|
|
|
expect(
|
|
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
|
kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic,
|
|
);
|
|
|
|
await gesture.moveTo(offMenuItem);
|
|
|
|
expect(
|
|
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
|
SystemMouseCursors.basic,
|
|
);
|
|
});
|
|
|
|
testWidgets('DropdownButton changes mouse cursor when hovered as expected', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const key = Key('testDropdownButton');
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: DropdownButton<String>(
|
|
key: key,
|
|
onChanged: (String? newValue) {},
|
|
items: <String>['One', 'Two', 'Three', 'Four'].map<DropdownMenuItem<String>>((
|
|
String value,
|
|
) {
|
|
return DropdownMenuItem<String>(value: value, child: Text(value));
|
|
}).toList(),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Finder dropdownButtonFinder = find.byKey(key);
|
|
final Offset onDropdownButton = tester.getCenter(dropdownButtonFinder);
|
|
final Offset offDropdownButton =
|
|
tester.getBottomRight(dropdownButtonFinder) + const Offset(1, 1);
|
|
final TestGesture gesture = await tester.createGesture(
|
|
kind: PointerDeviceKind.mouse,
|
|
pointer: 1,
|
|
);
|
|
|
|
await gesture.addPointer(location: onDropdownButton);
|
|
|
|
await tester.pump();
|
|
|
|
expect(
|
|
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
|
kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic,
|
|
);
|
|
|
|
await gesture.moveTo(offDropdownButton);
|
|
expect(
|
|
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
|
SystemMouseCursors.basic,
|
|
);
|
|
|
|
// Test that mouse cursor doesn't change when button is disabled
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: DropdownButton<String>(
|
|
key: key,
|
|
onChanged: null,
|
|
items: <String>['One', 'Two', 'Three', 'Four'].map<DropdownMenuItem<String>>((
|
|
String value,
|
|
) {
|
|
return DropdownMenuItem<String>(value: value, child: Text(value));
|
|
}).toList(),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await gesture.moveTo(onDropdownButton);
|
|
expect(
|
|
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
|
SystemMouseCursors.basic,
|
|
);
|
|
await gesture.moveTo(offDropdownButton);
|
|
expect(
|
|
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
|
SystemMouseCursors.basic,
|
|
);
|
|
});
|
|
|
|
testWidgets('DropdownButton has expected mouse cursor when explicitly configured', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const menuKey = Key('testDropdownButton');
|
|
const itemKey = Key('testDropdownMenuItem');
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: DropdownButton<String>(
|
|
key: menuKey,
|
|
mouseCursor: SystemMouseCursors.cell,
|
|
dropdownMenuItemMouseCursor: SystemMouseCursors.grab,
|
|
onChanged: (String? newValue) {},
|
|
items: const <DropdownMenuItem<String>>[
|
|
DropdownMenuItem<String>(key: itemKey, value: 'One', child: Text('One')),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Finder dropdownButtonFinder = find.byKey(menuKey);
|
|
final Offset onDropdownButton = tester.getCenter(dropdownButtonFinder);
|
|
final TestGesture gesture = await tester.createGesture(
|
|
kind: PointerDeviceKind.mouse,
|
|
pointer: 1,
|
|
);
|
|
|
|
await gesture.addPointer(location: onDropdownButton);
|
|
await tester.pump();
|
|
|
|
expect(
|
|
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
|
SystemMouseCursors.cell,
|
|
);
|
|
});
|
|
|
|
testWidgets('DropdownMenuItem has expected mouse cursor when explicitly configured', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const menuKey = Key('testDropdownButton');
|
|
const itemKey = Key('testDropdownMenuItem');
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: DropdownButton<String>(
|
|
key: menuKey,
|
|
dropdownMenuItemMouseCursor: SystemMouseCursors.grab,
|
|
onChanged: (String? newValue) {},
|
|
items: const <DropdownMenuItem<String>>[
|
|
DropdownMenuItem<String>(key: itemKey, value: 'One', child: Text('One')),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final TestGesture gesture = await tester.createGesture(
|
|
kind: PointerDeviceKind.mouse,
|
|
pointer: 1,
|
|
);
|
|
|
|
// Open DropdownButton.
|
|
await tester.tap(find.byKey(menuKey));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Find DropdownMenuItem.
|
|
final Finder menuItemFinder = find.byKey(itemKey);
|
|
final Offset onMenuItem = tester.getCenter(menuItemFinder);
|
|
|
|
await gesture.addPointer(location: onMenuItem);
|
|
await tester.pump();
|
|
|
|
expect(
|
|
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
|
SystemMouseCursors.grab,
|
|
);
|
|
});
|
|
|
|
testWidgets(
|
|
'Conflicting scrollbars are not applied by ScrollBehavior to Dropdown',
|
|
(WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/83819
|
|
// Open the dropdown menu
|
|
final Key buttonKey = UniqueKey();
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
buttonKey: buttonKey,
|
|
initialValue: null, // nothing selected
|
|
items: List<String>.generate(100, (int index) => index.toString()),
|
|
onChanged: onChanged,
|
|
),
|
|
);
|
|
await tester.tap(find.byKey(buttonKey));
|
|
await tester.pump();
|
|
await tester.pumpAndSettle(); // finish the menu animation
|
|
|
|
// The inherited ScrollBehavior should not apply Scrollbars since they are
|
|
// already built in to the widget. For iOS platform, ScrollBar directly returns
|
|
// CupertinoScrollbar
|
|
expect(
|
|
find.byType(CupertinoScrollbar),
|
|
debugDefaultTargetPlatformOverride == TargetPlatform.iOS ? findsOneWidget : findsNothing,
|
|
);
|
|
expect(find.byType(Scrollbar), findsOneWidget);
|
|
expect(find.byType(RawScrollbar), findsNothing);
|
|
},
|
|
variant: TargetPlatformVariant.all(),
|
|
);
|
|
|
|
testWidgets('borderRadius property works properly', (WidgetTester tester) async {
|
|
const radius = 20.0;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(useMaterial3: false),
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: DropdownButton<String>(
|
|
borderRadius: BorderRadius.circular(radius),
|
|
value: 'One',
|
|
items: <String>['One', 'Two', 'Three', 'Four'].map<DropdownMenuItem<String>>((
|
|
String value,
|
|
) {
|
|
return DropdownMenuItem<String>(value: value, child: Text(value));
|
|
}).toList(),
|
|
onChanged: (_) {},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('One'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
find.ancestor(of: find.text('One').last, matching: find.byType(CustomPaint)).at(2),
|
|
paints
|
|
..save()
|
|
..rrect()
|
|
..rrect()
|
|
..rrect()
|
|
..rrect(rrect: const RRect.fromLTRBXY(0.0, 0.0, 144.0, 208.0, radius, radius)),
|
|
);
|
|
});
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/88574
|
|
testWidgets("specifying itemHeight affects popup menu items' height", (
|
|
WidgetTester tester,
|
|
) async {
|
|
const value = 'One';
|
|
const double itemHeight = 80;
|
|
final List<DropdownMenuItem<String>> menuItems = <String>[value, 'Two', 'Free', 'Four']
|
|
.map<DropdownMenuItem<String>>((String value) {
|
|
return DropdownMenuItem<String>(value: value, child: Text(value));
|
|
})
|
|
.toList();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: DropdownButton<String>(
|
|
value: value,
|
|
itemHeight: itemHeight,
|
|
onChanged: (_) {},
|
|
items: menuItems,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text(value));
|
|
await tester.pumpAndSettle();
|
|
|
|
for (final item in menuItems) {
|
|
final Iterable<Element> elements = tester.elementList(find.byWidget(item));
|
|
for (final element in elements) {
|
|
expect(element.size!.height, itemHeight);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/92438
|
|
testWidgets('Do not throw due to the double precision', (WidgetTester tester) async {
|
|
const value = 'One';
|
|
const itemHeight = 77.701;
|
|
final List<DropdownMenuItem<String>> menuItems = <String>[value, 'Two', 'Free']
|
|
.map<DropdownMenuItem<String>>((String value) {
|
|
return DropdownMenuItem<String>(value: value, child: Text(value));
|
|
})
|
|
.toList();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: DropdownButton<String>(
|
|
value: value,
|
|
itemHeight: itemHeight,
|
|
onChanged: (_) {},
|
|
items: menuItems,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text(value));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(tester.takeException(), null);
|
|
});
|
|
|
|
testWidgets('BorderRadius property works properly for DropdownButtonFormField', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const radius = 20.0;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: DropdownButtonFormField<String>(
|
|
borderRadius: BorderRadius.circular(radius),
|
|
initialValue: 'One',
|
|
items: <String>['One', 'Two', 'Three', 'Four'].map<DropdownMenuItem<String>>((
|
|
String value,
|
|
) {
|
|
return DropdownMenuItem<String>(value: value, child: Text(value));
|
|
}).toList(),
|
|
onChanged: (_) {},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('One'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
find.ancestor(of: find.text('One').last, matching: find.byType(CustomPaint)).at(2),
|
|
paints
|
|
..save()
|
|
..rrect()
|
|
..rrect()
|
|
..rrect()
|
|
..rrect(rrect: const RRect.fromLTRBXY(0.0, 0.0, 800.0, 208.0, radius, radius)),
|
|
);
|
|
});
|
|
|
|
testWidgets('DropdownButton hint alignment', (WidgetTester tester) async {
|
|
const hintText = 'hint';
|
|
|
|
// AlignmentDirectional.centerStart (default)
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(alignment: AlignmentDirectional.centerStart, isExpanded: false),
|
|
);
|
|
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dx, 348.0);
|
|
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dy, 292.0);
|
|
// AlignmentDirectional.topStart
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(alignment: AlignmentDirectional.topStart, isExpanded: false),
|
|
);
|
|
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dx, 348.0);
|
|
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dy, 250.0);
|
|
// AlignmentDirectional.bottomStart
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(alignment: AlignmentDirectional.bottomStart, isExpanded: false),
|
|
);
|
|
expect(tester.getBottomLeft(find.text(hintText, skipOffstage: false)).dx, 348.0);
|
|
expect(tester.getBottomLeft(find.text(hintText, skipOffstage: false)).dy, 350.0);
|
|
// AlignmentDirectional.center
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(alignment: AlignmentDirectional.center, isExpanded: false),
|
|
);
|
|
expect(tester.getCenter(find.text(hintText, skipOffstage: false)).dx, 388.0);
|
|
expect(tester.getCenter(find.text(hintText, skipOffstage: false)).dy, 300.0);
|
|
// AlignmentDirectional.topEnd
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(alignment: AlignmentDirectional.topEnd, isExpanded: false),
|
|
);
|
|
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dx, 428.0);
|
|
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dy, 250.0);
|
|
// AlignmentDirectional.centerEnd
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(alignment: AlignmentDirectional.centerEnd, isExpanded: false),
|
|
);
|
|
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dx, 428.0);
|
|
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dy, 292.0);
|
|
// AlignmentDirectional.bottomEnd
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(alignment: AlignmentDirectional.bottomEnd, isExpanded: false),
|
|
);
|
|
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dx, 428.0);
|
|
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dy, 334.0);
|
|
|
|
// DropdownButton with `isExpanded: true`
|
|
// AlignmentDirectional.centerStart (default)
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(alignment: AlignmentDirectional.centerStart, isExpanded: true),
|
|
);
|
|
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dx, 0.0);
|
|
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dy, 292.0);
|
|
// AlignmentDirectional.topStart
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(alignment: AlignmentDirectional.topStart, isExpanded: true),
|
|
);
|
|
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dx, 0.0);
|
|
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dy, 250.0);
|
|
// AlignmentDirectional.bottomStart
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(alignment: AlignmentDirectional.bottomStart, isExpanded: true),
|
|
);
|
|
expect(tester.getBottomLeft(find.text(hintText, skipOffstage: false)).dx, 0.0);
|
|
expect(tester.getBottomLeft(find.text(hintText, skipOffstage: false)).dy, 350.0);
|
|
// AlignmentDirectional.center
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(alignment: AlignmentDirectional.center, isExpanded: true),
|
|
);
|
|
expect(tester.getCenter(find.text(hintText, skipOffstage: false)).dx, 388.0);
|
|
expect(tester.getCenter(find.text(hintText, skipOffstage: false)).dy, 300.0);
|
|
// AlignmentDirectional.topEnd
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(alignment: AlignmentDirectional.topEnd, isExpanded: true),
|
|
);
|
|
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dx, 776.0);
|
|
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dy, 250.0);
|
|
// AlignmentDirectional.centerEnd
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(alignment: AlignmentDirectional.centerEnd, isExpanded: true),
|
|
);
|
|
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dx, 776.0);
|
|
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dy, 292.0);
|
|
// AlignmentDirectional.bottomEnd
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(alignment: AlignmentDirectional.bottomEnd, isExpanded: true),
|
|
);
|
|
expect(tester.getBottomRight(find.text(hintText, skipOffstage: false)).dx, 776.0);
|
|
expect(tester.getBottomRight(find.text(hintText, skipOffstage: false)).dy, 350.0);
|
|
});
|
|
|
|
testWidgets('DropdownButton hint alignment with selectedItemBuilder', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const hintText = 'hint';
|
|
|
|
// AlignmentDirectional.centerStart (default)
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(
|
|
alignment: AlignmentDirectional.centerStart,
|
|
isExpanded: false,
|
|
enableSelectedItemBuilder: true,
|
|
),
|
|
);
|
|
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dx, 348.0);
|
|
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dy, 292.0);
|
|
// AlignmentDirectional.topStart
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(
|
|
alignment: AlignmentDirectional.topStart,
|
|
isExpanded: false,
|
|
enableSelectedItemBuilder: true,
|
|
),
|
|
);
|
|
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dx, 348.0);
|
|
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dy, 250.0);
|
|
// AlignmentDirectional.bottomStart
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(
|
|
alignment: AlignmentDirectional.bottomStart,
|
|
isExpanded: false,
|
|
enableSelectedItemBuilder: true,
|
|
),
|
|
);
|
|
expect(tester.getBottomLeft(find.text(hintText, skipOffstage: false)).dx, 348.0);
|
|
expect(tester.getBottomLeft(find.text(hintText, skipOffstage: false)).dy, 350.0);
|
|
// AlignmentDirectional.center
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(
|
|
alignment: AlignmentDirectional.center,
|
|
isExpanded: false,
|
|
enableSelectedItemBuilder: true,
|
|
),
|
|
);
|
|
expect(tester.getCenter(find.text(hintText, skipOffstage: false)).dx, 388.0);
|
|
expect(tester.getCenter(find.text(hintText, skipOffstage: false)).dy, 300.0);
|
|
// AlignmentDirectional.topEnd
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(
|
|
alignment: AlignmentDirectional.topEnd,
|
|
isExpanded: false,
|
|
enableSelectedItemBuilder: true,
|
|
),
|
|
);
|
|
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dx, 428.0);
|
|
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dy, 250.0);
|
|
// AlignmentDirectional.centerEnd
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(
|
|
alignment: AlignmentDirectional.centerEnd,
|
|
isExpanded: false,
|
|
enableSelectedItemBuilder: true,
|
|
),
|
|
);
|
|
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dx, 428.0);
|
|
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dy, 292.0);
|
|
// AlignmentDirectional.bottomEnd
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(
|
|
alignment: AlignmentDirectional.bottomEnd,
|
|
isExpanded: false,
|
|
enableSelectedItemBuilder: true,
|
|
),
|
|
);
|
|
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dx, 428.0);
|
|
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dy, 334.0);
|
|
|
|
// DropdownButton with `isExpanded: true`
|
|
// AlignmentDirectional.centerStart (default)
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(
|
|
alignment: AlignmentDirectional.centerStart,
|
|
isExpanded: true,
|
|
enableSelectedItemBuilder: true,
|
|
),
|
|
);
|
|
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dx, 0.0);
|
|
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dy, 292.0);
|
|
// AlignmentDirectional.topStart
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(
|
|
alignment: AlignmentDirectional.topStart,
|
|
isExpanded: true,
|
|
enableSelectedItemBuilder: true,
|
|
),
|
|
);
|
|
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dx, 0.0);
|
|
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dy, 250.0);
|
|
// AlignmentDirectional.bottomStart
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(
|
|
alignment: AlignmentDirectional.bottomStart,
|
|
isExpanded: true,
|
|
enableSelectedItemBuilder: true,
|
|
),
|
|
);
|
|
expect(tester.getBottomLeft(find.text(hintText, skipOffstage: false)).dx, 0.0);
|
|
expect(tester.getBottomLeft(find.text(hintText, skipOffstage: false)).dy, 350.0);
|
|
// AlignmentDirectional.center
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(
|
|
alignment: AlignmentDirectional.center,
|
|
isExpanded: true,
|
|
enableSelectedItemBuilder: true,
|
|
),
|
|
);
|
|
expect(tester.getCenter(find.text(hintText, skipOffstage: false)).dx, 388.0);
|
|
expect(tester.getCenter(find.text(hintText, skipOffstage: false)).dy, 300.0);
|
|
// AlignmentDirectional.topEnd
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(
|
|
alignment: AlignmentDirectional.topEnd,
|
|
isExpanded: true,
|
|
enableSelectedItemBuilder: true,
|
|
),
|
|
);
|
|
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dx, 776.0);
|
|
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dy, 250.0);
|
|
// AlignmentDirectional.centerEnd
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(
|
|
alignment: AlignmentDirectional.centerEnd,
|
|
isExpanded: true,
|
|
enableSelectedItemBuilder: true,
|
|
),
|
|
);
|
|
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dx, 776.0);
|
|
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dy, 292.0);
|
|
// AlignmentDirectional.bottomEnd
|
|
await tester.pumpWidget(
|
|
buildDropdownWithHint(
|
|
alignment: AlignmentDirectional.bottomEnd,
|
|
isExpanded: true,
|
|
enableSelectedItemBuilder: true,
|
|
),
|
|
);
|
|
expect(tester.getBottomRight(find.text(hintText, skipOffstage: false)).dx, 776.0);
|
|
expect(tester.getBottomRight(find.text(hintText, skipOffstage: false)).dy, 350.0);
|
|
});
|
|
|
|
group('DropdownButtonFormField decoration hintText', () {
|
|
const decorationHintText = 'Decoration Hint text';
|
|
const hintText = 'Hint text';
|
|
const disabledHintText = 'Disabled Hint text';
|
|
|
|
testWidgets('is the fallback value for DropdownButtonFormField.hint', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
isFormField: true,
|
|
onChanged: (String? newValue) {},
|
|
decoration: const InputDecoration(hintText: decorationHintText),
|
|
),
|
|
);
|
|
|
|
expect(find.text(decorationHintText, skipOffstage: false), findsOne);
|
|
});
|
|
|
|
testWidgets('does not override DropdownButtonFormField.hint', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
hint: const Text(hintText),
|
|
isFormField: true,
|
|
onChanged: (String? newValue) {},
|
|
decoration: const InputDecoration(hintText: decorationHintText),
|
|
),
|
|
);
|
|
|
|
expect(find.text(hintText, skipOffstage: false), findsOne);
|
|
expect(find.text(decorationHintText, skipOffstage: false), findsNothing);
|
|
});
|
|
|
|
testWidgets('is the fallback value for DropdownButtonFormField.disabledHint', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// The Dropdown is disabled because onChanged is not defined.
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
isFormField: true,
|
|
decoration: const InputDecoration(hintText: decorationHintText),
|
|
),
|
|
);
|
|
|
|
expect(find.text(decorationHintText, skipOffstage: false), findsOne);
|
|
});
|
|
|
|
testWidgets('does not override DropdownButtonFormField.disabledHint', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// The Dropdown is disabled because onChanged is not defined.
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
disabledHint: const Text(disabledHintText),
|
|
isFormField: true,
|
|
decoration: const InputDecoration(hintText: decorationHintText),
|
|
),
|
|
);
|
|
|
|
expect(find.text(disabledHintText, skipOffstage: false), findsOne);
|
|
expect(find.text(decorationHintText, skipOffstage: false), findsNothing);
|
|
});
|
|
|
|
testWidgets('is not used for disabledHint if DropdownButtonFormField.hint is provided', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// The Dropdown is disabled because onChanged is not defined.
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
hint: const Text(hintText),
|
|
isFormField: true,
|
|
decoration: const InputDecoration(hintText: decorationHintText),
|
|
),
|
|
);
|
|
|
|
expect(find.text(hintText, skipOffstage: false), findsOne);
|
|
expect(find.text(decorationHintText, skipOffstage: false), findsNothing);
|
|
});
|
|
});
|
|
|
|
testWidgets('BorderRadius property clips dropdown menu', (WidgetTester tester) async {
|
|
const radius = 20.0;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: DropdownButtonFormField<String>(
|
|
borderRadius: BorderRadius.circular(radius),
|
|
initialValue: 'One',
|
|
items: <String>['One', 'Two', 'Three', 'Four'].map<DropdownMenuItem<String>>((
|
|
String value,
|
|
) {
|
|
return DropdownMenuItem<String>(value: value, child: Text(value));
|
|
}).toList(),
|
|
onChanged: (_) {},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('One'));
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderClipRRect renderClip = tester.allRenderObjects.whereType<RenderClipRRect>().first;
|
|
expect(renderClip.borderRadius, BorderRadius.circular(radius));
|
|
});
|
|
|
|
testWidgets('Size of DropdownButton with padding', (WidgetTester tester) async {
|
|
const double padVertical = 5;
|
|
const double padHorizontal = 10;
|
|
final Key buttonKey = UniqueKey();
|
|
EdgeInsets? padding;
|
|
|
|
Widget build() => buildFrame(buttonKey: buttonKey, onChanged: onChanged, padding: padding);
|
|
|
|
await tester.pumpWidget(build());
|
|
final RenderBox buttonBoxNoPadding = tester.renderObject<RenderBox>(find.byKey(buttonKey));
|
|
assert(buttonBoxNoPadding.attached);
|
|
final noPaddingSize = Size.copy(buttonBoxNoPadding.size);
|
|
|
|
padding = const EdgeInsets.symmetric(vertical: padVertical, horizontal: padHorizontal);
|
|
await tester.pumpWidget(build());
|
|
final RenderBox buttonBoxPadded = tester.renderObject<RenderBox>(find.byKey(buttonKey));
|
|
assert(buttonBoxPadded.attached);
|
|
final paddedSize = Size.copy(buttonBoxPadded.size);
|
|
|
|
// dropdowns with padding should be that much larger than with no padding
|
|
expect(noPaddingSize.height, equals(paddedSize.height - padVertical * 2));
|
|
expect(noPaddingSize.width, equals(paddedSize.width - padHorizontal * 2));
|
|
});
|
|
|
|
testWidgets('Dropdown closes when barrier is tapped by default', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: DropdownButton<String>(
|
|
value: 'first',
|
|
items: const <DropdownMenuItem<String>>[
|
|
DropdownMenuItem<String>(enabled: false, child: Text('disabled')),
|
|
DropdownMenuItem<String>(value: 'first', child: Text('first')),
|
|
DropdownMenuItem<String>(value: 'second', child: Text('second')),
|
|
],
|
|
onChanged: (_) {},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open dropdown.
|
|
await tester.tap(find.text('first').hitTestable());
|
|
await tester.pumpAndSettle();
|
|
|
|
// Tap on the barrier.
|
|
await tester.tapAt(const Offset(400, 400));
|
|
await tester.pumpAndSettle();
|
|
|
|
// The dropdown should be closed, i.e., there should be no widget with 'second' text.
|
|
expect(find.text('second'), findsNothing);
|
|
});
|
|
|
|
testWidgets('Dropdown does not close when barrier dismissible set to false', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: DropdownButton<String>(
|
|
value: 'first',
|
|
barrierDismissible: false,
|
|
items: const <DropdownMenuItem<String>>[
|
|
DropdownMenuItem<String>(enabled: false, child: Text('disabled')),
|
|
DropdownMenuItem<String>(value: 'first', child: Text('first')),
|
|
DropdownMenuItem<String>(value: 'second', child: Text('second')),
|
|
],
|
|
onChanged: (_) {},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Open dropdown.
|
|
await tester.tap(find.text('first').hitTestable());
|
|
await tester.pumpAndSettle();
|
|
|
|
// Tap on the barrier.
|
|
await tester.tapAt(const Offset(400, 400));
|
|
await tester.pumpAndSettle();
|
|
|
|
// The dropdown should still be open, i.e., there should be one widget with 'second' text.
|
|
expect(find.text('second'), findsOneWidget);
|
|
});
|
|
|
|
// This is a regression test for https://github.com/flutter/flutter/issues/70294.
|
|
testWidgets(
|
|
'The previous selected item should be highlighted when reopening dropdown on mobile',
|
|
(WidgetTester tester) async {
|
|
final Color selectedColor = Colors.black.withValues(alpha: 0.12);
|
|
var currentValue = 'one';
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(focusColor: selectedColor),
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return DropdownButton<String>(
|
|
value: currentValue,
|
|
items: menuItems
|
|
.map(
|
|
(String item) => DropdownMenuItem<String>(value: item, child: Text(item)),
|
|
)
|
|
.toList(),
|
|
onChanged: (String? newValue) {
|
|
setState(() {
|
|
currentValue = newValue!;
|
|
});
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Make sure the current value of dropdown is the first one of items list menuItems.
|
|
expect(find.text('one'), findsOne);
|
|
|
|
// Tap to open the dropdown.
|
|
await tester.tap(find.text('one'));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Select the second item from the dropdown list.
|
|
await tester.tap(find.text('two'));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Make sure the current item of dropdown is the second item of items list menuItems.
|
|
expect(find.text('two'), findsOneWidget);
|
|
|
|
// Tap to reopen the dropdown.
|
|
await tester.tap(find.text('two'));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Make sure the current selected item is highlighted with selectedColor.
|
|
final Ink selectedItemInk = tester.widget<Ink>(
|
|
find.ancestor(of: find.text('two'), matching: find.byType(Ink)).first,
|
|
);
|
|
final decoration = selectedItemInk.decoration! as BoxDecoration;
|
|
expect(decoration.color, selectedColor);
|
|
},
|
|
variant: TargetPlatformVariant.mobile(),
|
|
);
|
|
|
|
testWidgets(
|
|
'DropdownButtonFormField deprecated "value" parameter can still be used to set the initial value',
|
|
(WidgetTester tester) async {
|
|
final fieldKey = GlobalKey<FormFieldState<String>>();
|
|
await tester.pumpWidget(
|
|
StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return MaterialApp(
|
|
home: Material(
|
|
child: DropdownButtonFormField<String>(
|
|
key: fieldKey,
|
|
value: 'one',
|
|
hint: const Text('Select Value'),
|
|
items: menuItems.map((String val) {
|
|
return DropdownMenuItem<String>(value: val, child: Text(val));
|
|
}).toList(),
|
|
onChanged: (_) {},
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
expect(fieldKey.currentState!.value, 'one');
|
|
},
|
|
);
|
|
|
|
testWidgets('DropdownButton does not crash at zero area', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: SizedBox.shrink(
|
|
child: DropdownButton<String>(
|
|
value: 'a',
|
|
onChanged: (_) {},
|
|
items: const <DropdownMenuItem<String>>[
|
|
DropdownMenuItem<String>(value: 'a', child: Text('a')),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(tester.getSize(find.byType(DropdownButton<String>)), Size.zero);
|
|
});
|
|
|
|
testWidgets('DropdownButtonFormField does not crash at zero area', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: SizedBox.shrink(
|
|
child: DropdownButtonFormField<String>(
|
|
onChanged: (_) {},
|
|
items: const <DropdownMenuItem<String>>[
|
|
DropdownMenuItem<String>(value: 'a', child: Text('a')),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(tester.getSize(find.byType(DropdownButtonFormField<String>)), Size.zero);
|
|
});
|
|
|
|
testWidgets('DropdownMenuItem does not crash at zero area', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
const MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: SizedBox.shrink(
|
|
child: DropdownMenuItem<String>(value: 'a', child: Text('a')),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(tester.getSize(find.byType(DropdownMenuItem<String>)), Size.zero);
|
|
});
|
|
|
|
testWidgets('DropdownButtonFormField can inherit from local InputDecorationThemeData', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const labelText = 'Label';
|
|
const Color labelColor = Colors.green;
|
|
const decoration = InputDecoration(labelText: labelText);
|
|
const decorationTheme = InputDecorationThemeData(labelStyle: TextStyle(color: labelColor));
|
|
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
isFormField: true,
|
|
decoration: decoration,
|
|
onChanged: (_) {},
|
|
localInputDecorationTheme: decorationTheme,
|
|
),
|
|
);
|
|
|
|
final TextStyle labelStyle = DefaultTextStyle.of(
|
|
tester.firstElement(find.text(labelText)),
|
|
).style;
|
|
expect(labelStyle.color, labelColor);
|
|
});
|
|
}
|