blob: 7b28d8836183ba88695a77d6482fc6eb051042e7 [file] [log] [blame]
// 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.
import 'dart:ui' show DisplayFeature, DisplayFeatureState, DisplayFeatureType, SemanticsFlag;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import '../widgets/semantics_tester.dart';
import 'feedback_tester.dart';
void main() {
testWidgets('Navigator.push works within a PopupMenuButton', (WidgetTester tester) async {
final Key targetKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
routes: <String, WidgetBuilder>{
'/next': (BuildContext context) {
return const Text('Next');
},
},
home: Material(
child: Center(
child: Builder(
key: targetKey,
builder: (BuildContext context) {
return PopupMenuButton<int>(
onSelected: (int value) {
Navigator.pushNamed(context, '/next');
},
itemBuilder: (BuildContext context) {
return <PopupMenuItem<int>>[
const PopupMenuItem<int>(
value: 1,
child: Text('One'),
),
];
},
);
},
),
),
),
),
);
await tester.tap(find.byKey(targetKey));
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
expect(find.text('One'), findsOneWidget);
expect(find.text('Next'), findsNothing);
await tester.tap(find.text('One'));
await tester.pump(); // return the future
await tester.pump(); // start the navigation
await tester.pump(const Duration(seconds: 1)); // end the navigation
expect(find.text('One'), findsNothing);
expect(find.text('Next'), findsOneWidget);
});
testWidgets('PopupMenuButton calls onOpened callback when the menu is opened', (WidgetTester tester) async {
int opens = 0;
late BuildContext popupContext;
final Key noItemsKey = UniqueKey();
final Key noCallbackKey = UniqueKey();
final Key withCallbackKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
PopupMenuButton<int>(
key: noItemsKey,
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<int>>[];
},
onOpened: () => opens++,
),
PopupMenuButton<int>(
key: noCallbackKey,
itemBuilder: (BuildContext context) {
popupContext = context;
return <PopupMenuEntry<int>>[
const PopupMenuItem<int>(
value: 1,
child: Text('Tap me please!'),
),
];
},
),
PopupMenuButton<int>(
key: withCallbackKey,
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<int>>[
const PopupMenuItem<int>(
value: 1,
child: Text('Tap me, too!'),
),
];
},
onOpened: () => opens++,
),
],
),
),
),
);
// Make sure callback is not called when the menu is not shown
await tester.tap(find.byKey(noItemsKey));
await tester.pump();
expect(opens, equals(0));
// Make sure everything works if no callback is provided
await tester.tap(find.byKey(noCallbackKey));
await tester.pump();
expect(opens, equals(0));
// Close the opened menu
Navigator.of(popupContext).pop();
await tester.pump();
// Make sure callback is called when the button is tapped
await tester.tap(find.byKey(withCallbackKey));
await tester.pump();
expect(opens, equals(1));
});
testWidgets('PopupMenuButton calls onCanceled callback when an item is not selected', (WidgetTester tester) async {
int cancels = 0;
late BuildContext popupContext;
final Key noCallbackKey = UniqueKey();
final Key withCallbackKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
PopupMenuButton<int>(
key: noCallbackKey,
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<int>>[
const PopupMenuItem<int>(
value: 1,
child: Text('Tap me please!'),
),
];
},
),
PopupMenuButton<int>(
key: withCallbackKey,
onCanceled: () => cancels++,
itemBuilder: (BuildContext context) {
popupContext = context;
return <PopupMenuEntry<int>>[
const PopupMenuItem<int>(
value: 1,
child: Text('Tap me, too!'),
),
];
},
),
],
),
),
),
);
// Make sure everything works if no callback is provided
await tester.tap(find.byKey(noCallbackKey));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
await tester.tapAt(Offset.zero);
await tester.pump();
expect(cancels, equals(0));
// Make sure callback is called when a non-selection tap occurs
await tester.tap(find.byKey(withCallbackKey));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
await tester.tapAt(Offset.zero);
await tester.pump();
expect(cancels, equals(1));
// Make sure callback is called when back navigation occurs
await tester.tap(find.byKey(withCallbackKey));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
Navigator.of(popupContext).pop();
await tester.pump();
expect(cancels, equals(2));
});
testWidgets('disabled PopupMenuButton will not call itemBuilder, onOpened, onSelected or onCanceled', (WidgetTester tester) async {
final GlobalKey popupButtonKey = GlobalKey();
bool itemBuilderCalled = false;
bool onOpenedCalled = false;
bool onSelectedCalled = false;
bool onCanceledCalled = false;
Widget buildApp({bool directional = false}) {
return MaterialApp(
home: Builder(builder: (BuildContext context) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
navigationMode: NavigationMode.directional,
),
child: Material(
child: Column(
children: <Widget>[
PopupMenuButton<int>(
enabled: false,
child: Text('Tap Me', key: popupButtonKey),
itemBuilder: (BuildContext context) {
itemBuilderCalled = true;
return <PopupMenuEntry<int>>[
const PopupMenuItem<int>(
value: 1,
child: Text('Tap me please!'),
),
];
},
onOpened: ()=> onOpenedCalled = true,
onSelected: (int selected) => onSelectedCalled = true,
onCanceled: () => onCanceledCalled = true,
),
],
),
),
);
}),
);
}
await tester.pumpWidget(buildApp());
// Try to bring up the popup menu and select the first item from it
await tester.tap(find.byKey(popupButtonKey));
await tester.pumpAndSettle();
await tester.tap(find.byKey(popupButtonKey));
await tester.pumpAndSettle();
expect(itemBuilderCalled, isFalse);
expect(onOpenedCalled, isFalse);
expect(onSelectedCalled, isFalse);
// Try to bring up the popup menu and tap outside it to cancel the menu
await tester.tap(find.byKey(popupButtonKey));
await tester.pumpAndSettle();
await tester.tapAt(Offset.zero);
await tester.pumpAndSettle();
expect(itemBuilderCalled, isFalse);
expect(onOpenedCalled, isFalse);
expect(onCanceledCalled, isFalse);
// Test again, with directional navigation mode and after focusing the button.
await tester.pumpWidget(buildApp(directional: true));
// Try to bring up the popup menu and select the first item from it
Focus.of(popupButtonKey.currentContext!).requestFocus();
await tester.pumpAndSettle();
await tester.tap(find.byKey(popupButtonKey));
await tester.pumpAndSettle();
await tester.tap(find.byKey(popupButtonKey));
await tester.pumpAndSettle();
expect(itemBuilderCalled, isFalse);
expect(onOpenedCalled, isFalse);
expect(onSelectedCalled, isFalse);
// Try to bring up the popup menu and tap outside it to cancel the menu
await tester.tap(find.byKey(popupButtonKey));
await tester.pumpAndSettle();
await tester.tapAt(Offset.zero);
await tester.pumpAndSettle();
expect(itemBuilderCalled, isFalse);
expect(onOpenedCalled, isFalse);
expect(onCanceledCalled, isFalse);
});
testWidgets('disabled PopupMenuButton is not focusable', (WidgetTester tester) async {
final Key popupButtonKey = UniqueKey();
final GlobalKey childKey = GlobalKey();
bool itemBuilderCalled = false;
bool onOpenedCalled = false;
bool onSelectedCalled = false;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
PopupMenuButton<int>(
key: popupButtonKey,
enabled: false,
child: Container(key: childKey),
itemBuilder: (BuildContext context) {
itemBuilderCalled = true;
return <PopupMenuEntry<int>>[
const PopupMenuItem<int>(
value: 1,
child: Text('Tap me please!'),
),
];
},
onOpened: () => onOpenedCalled = true,
onSelected: (int selected) => onSelectedCalled = true,
),
],
),
),
),
);
Focus.of(childKey.currentContext!).requestFocus();
await tester.pump();
expect(Focus.of(childKey.currentContext!).hasPrimaryFocus, isFalse);
expect(itemBuilderCalled, isFalse);
expect(onOpenedCalled, isFalse);
expect(onSelectedCalled, isFalse);
});
testWidgets('disabled PopupMenuButton is focusable with directional navigation', (WidgetTester tester) async {
final Key popupButtonKey = UniqueKey();
final GlobalKey childKey = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: Builder(builder: (BuildContext context) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
navigationMode: NavigationMode.directional,
),
child: Material(
child: Column(
children: <Widget>[
PopupMenuButton<int>(
key: popupButtonKey,
enabled: false,
child: Container(key: childKey),
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<int>>[
const PopupMenuItem<int>(
value: 1,
child: Text('Tap me please!'),
),
];
},
onSelected: (int selected) {},
),
],
),
),
);
}),
),
);
Focus.of(childKey.currentContext!).requestFocus();
await tester.pump();
expect(Focus.of(childKey.currentContext!).hasPrimaryFocus, isTrue);
});
testWidgets('PopupMenuItem onTap callback is called when defined', (WidgetTester tester) async {
final List<int> menuItemTapCounters = <int>[0, 0];
await tester.pumpWidget(
TestApp(
textDirection: TextDirection.ltr,
child: Material(
child: RepaintBoundary(
child: PopupMenuButton<void>(
child: const Text('Actions'),
itemBuilder: (BuildContext context) => <PopupMenuItem<void>>[
PopupMenuItem<void>(
child: const Text('First option'),
onTap: () {
menuItemTapCounters[0] += 1;
},
),
PopupMenuItem<void>(
child: const Text('Second option'),
onTap: () {
menuItemTapCounters[1] += 1;
},
),
const PopupMenuItem<void>(
child: Text('Option without onTap'),
),
],
),
),
),
),
);
// Tap the first time
await tester.tap(find.text('Actions'));
await tester.pumpAndSettle();
await tester.tap(find.text('First option'));
await tester.pumpAndSettle();
expect(menuItemTapCounters, <int>[1, 0]);
// Tap the item again
await tester.tap(find.text('Actions'));
await tester.pumpAndSettle();
await tester.tap(find.text('First option'));
await tester.pumpAndSettle();
expect(menuItemTapCounters, <int>[2, 0]);
// Tap a different item
await tester.tap(find.text('Actions'));
await tester.pumpAndSettle();
await tester.tap(find.text('Second option'));
await tester.pumpAndSettle();
expect(menuItemTapCounters, <int>[2, 1]);
// Tap an item without onTap
await tester.tap(find.text('Actions'));
await tester.pumpAndSettle();
await tester.tap(find.text('Option without onTap'));
await tester.pumpAndSettle();
expect(menuItemTapCounters, <int>[2, 1]);
});
testWidgets('PopupMenuItem can have both onTap and value', (WidgetTester tester) async {
final List<int> menuItemTapCounters = <int>[0, 0];
String? selected;
await tester.pumpWidget(
TestApp(
textDirection: TextDirection.ltr,
child: Material(
child: RepaintBoundary(
child: PopupMenuButton<String>(
child: const Text('Actions'),
onSelected: (String value) { selected = value; },
itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
PopupMenuItem<String>(
value: 'first',
child: const Text('First option'),
onTap: () {
menuItemTapCounters[0] += 1;
},
),
PopupMenuItem<String>(
value: 'second',
child: const Text('Second option'),
onTap: () {
menuItemTapCounters[1] += 1;
},
),
const PopupMenuItem<String>(
value: 'third',
child: Text('Option without onTap'),
),
],
),
),
),
),
);
// Tap the first item
await tester.tap(find.text('Actions'));
await tester.pumpAndSettle();
await tester.tap(find.text('First option'));
await tester.pumpAndSettle();
expect(menuItemTapCounters, <int>[1, 0]);
expect(selected, 'first');
// Tap the item again
await tester.tap(find.text('Actions'));
await tester.pumpAndSettle();
await tester.tap(find.text('First option'));
await tester.pumpAndSettle();
expect(menuItemTapCounters, <int>[2, 0]);
expect(selected, 'first');
// Tap a different item
await tester.tap(find.text('Actions'));
await tester.pumpAndSettle();
await tester.tap(find.text('Second option'));
await tester.pumpAndSettle();
expect(menuItemTapCounters, <int>[2, 1]);
expect(selected, 'second');
// Tap an item without onTap
await tester.tap(find.text('Actions'));
await tester.pumpAndSettle();
await tester.tap(find.text('Option without onTap'));
await tester.pumpAndSettle();
expect(menuItemTapCounters, <int>[2, 1]);
expect(selected, 'third');
});
testWidgets('PopupMenuItem is only focusable when enabled', (WidgetTester tester) async {
final Key popupButtonKey = UniqueKey();
final GlobalKey childKey = GlobalKey();
bool itemBuilderCalled = false;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
PopupMenuButton<int>(
key: popupButtonKey,
itemBuilder: (BuildContext context) {
itemBuilderCalled = true;
return <PopupMenuEntry<int>>[
PopupMenuItem<int>(
value: 1,
child: Text('Tap me please!', key: childKey),
),
];
},
),
],
),
),
),
);
// Open the popup to build and show the menu contents.
await tester.tap(find.byKey(popupButtonKey));
await tester.pumpAndSettle();
final FocusNode childNode = Focus.of(childKey.currentContext!);
// Now that the contents are shown, request focus on the child text.
childNode.requestFocus();
await tester.pumpAndSettle();
expect(itemBuilderCalled, isTrue);
// Make sure that the focus went where we expected it to.
expect(childNode.hasPrimaryFocus, isTrue);
itemBuilderCalled = false;
// Close the popup.
await tester.tap(find.byKey(popupButtonKey), warnIfMissed: false);
await tester.pumpAndSettle();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
PopupMenuButton<int>(
key: popupButtonKey,
itemBuilder: (BuildContext context) {
itemBuilderCalled = true;
return <PopupMenuEntry<int>>[
PopupMenuItem<int>(
enabled: false,
value: 1,
child: Text('Tap me please!', key: childKey),
),
];
},
),
],
),
),
),
);
await tester.pumpAndSettle();
// Open the popup again to rebuild the contents with enabled == false.
await tester.tap(find.byKey(popupButtonKey));
await tester.pumpAndSettle();
expect(itemBuilderCalled, isTrue);
expect(Focus.of(childKey.currentContext!).hasPrimaryFocus, isFalse);
});
testWidgets('PopupMenuButton is horizontal on iOS', (WidgetTester tester) async {
Widget build(TargetPlatform platform) {
debugDefaultTargetPlatformOverride = platform;
return MaterialApp(
home: Scaffold(
appBar: AppBar(
actions: <Widget>[
PopupMenuButton<int>(
itemBuilder: (BuildContext context) {
return <PopupMenuItem<int>>[
const PopupMenuItem<int>(
value: 1,
child: Text('One'),
),
];
},
),
],
),
),
);
}
await tester.pumpWidget(build(TargetPlatform.android));
expect(find.byIcon(Icons.more_vert), findsOneWidget);
expect(find.byIcon(Icons.more_horiz), findsNothing);
await tester.pumpWidget(build(TargetPlatform.iOS));
await tester.pumpAndSettle(); // Run theme change animation.
expect(find.byIcon(Icons.more_vert), findsNothing);
expect(find.byIcon(Icons.more_horiz), findsOneWidget);
await tester.pumpWidget(build(TargetPlatform.macOS));
await tester.pumpAndSettle(); // Run theme change animation.
expect(find.byIcon(Icons.more_vert), findsNothing);
expect(find.byIcon(Icons.more_horiz), findsOneWidget);
debugDefaultTargetPlatformOverride = null;
});
group('PopupMenuButton with Icon', () {
// Helper function to create simple and valid popup menus.
List<PopupMenuItem<int>> simplePopupMenuItemBuilder(BuildContext context) {
return <PopupMenuItem<int>>[
const PopupMenuItem<int>(
value: 1,
child: Text('1'),
),
];
}
testWidgets('PopupMenuButton fails when given both child and icon', (WidgetTester tester) async {
expect(() {
PopupMenuButton<int>(
icon: const Icon(Icons.view_carousel),
itemBuilder: simplePopupMenuItemBuilder,
child: const Text('heyo'),
);
}, throwsAssertionError);
});
testWidgets('PopupMenuButton creates IconButton when given an icon', (WidgetTester tester) async {
final PopupMenuButton<int> button = PopupMenuButton<int>(
icon: const Icon(Icons.view_carousel),
itemBuilder: simplePopupMenuItemBuilder,
);
await tester.pumpWidget(MaterialApp(
home: Scaffold(
appBar: AppBar(
actions: <Widget>[button],
),
),
),
);
expect(find.byType(IconButton), findsOneWidget);
expect(find.byIcon(Icons.view_carousel), findsOneWidget);
});
});
testWidgets('PopupMenu positioning', (WidgetTester tester) async {
final Widget testButton = PopupMenuButton<int>(
itemBuilder: (BuildContext context) {
return <PopupMenuItem<int>>[
const PopupMenuItem<int>(value: 1, child: Text('AAA')),
const PopupMenuItem<int>(value: 2, child: Text('BBB')),
const PopupMenuItem<int>(value: 3, child: Text('CCC')),
];
},
child: const SizedBox(
height: 100.0,
width: 100.0,
child: Text('XXX'),
),
);
bool popupMenu(Widget widget) {
final String widgetType = widget.runtimeType.toString();
// TODO(mraleph): Remove the old case below.
return widgetType == '_PopupMenu<int?>' // normal case
|| widgetType == '_PopupMenu'; // for old versions of Dart that don't reify method type arguments
}
Future<void> openMenu(TextDirection textDirection, Alignment alignment) async {
return TestAsyncUtils.guard<void>(() async {
await tester.pumpWidget(Container()); // reset in case we had a menu up already
await tester.pumpWidget(TestApp(
textDirection: textDirection,
child: Align(
alignment: alignment,
child: testButton,
),
));
await tester.tap(find.text('XXX'));
await tester.pump();
});
}
Future<void> testPositioningDown(
WidgetTester tester,
TextDirection textDirection,
Alignment alignment,
TextDirection growthDirection,
Rect startRect,
) {
return TestAsyncUtils.guard<void>(() async {
await openMenu(textDirection, alignment);
Rect rect = tester.getRect(find.byWidgetPredicate(popupMenu));
expect(rect, startRect);
bool doneVertically = false;
bool doneHorizontally = false;
do {
await tester.pump(const Duration(milliseconds: 20));
final Rect newRect = tester.getRect(find.byWidgetPredicate(popupMenu));
expect(newRect.top, rect.top);
if (doneVertically) {
expect(newRect.bottom, rect.bottom);
} else {
if (newRect.bottom == rect.bottom) {
doneVertically = true;
} else {
expect(newRect.bottom, greaterThan(rect.bottom));
}
}
switch (growthDirection) {
case TextDirection.rtl:
expect(newRect.right, rect.right);
if (doneHorizontally) {
expect(newRect.left, rect.left);
} else {
if (newRect.left == rect.left) {
doneHorizontally = true;
} else {
expect(newRect.left, lessThan(rect.left));
}
}
break;
case TextDirection.ltr:
expect(newRect.left, rect.left);
if (doneHorizontally) {
expect(newRect.right, rect.right);
} else {
if (newRect.right == rect.right) {
doneHorizontally = true;
} else {
expect(newRect.right, greaterThan(rect.right));
}
}
break;
}
rect = newRect;
} while (tester.binding.hasScheduledFrame);
});
}
Future<void> testPositioningDownThenUp(
WidgetTester tester,
TextDirection textDirection,
Alignment alignment,
TextDirection growthDirection,
Rect startRect,
) {
return TestAsyncUtils.guard<void>(() async {
await openMenu(textDirection, alignment);
Rect rect = tester.getRect(find.byWidgetPredicate(popupMenu));
expect(rect, startRect);
int verticalStage = 0; // 0=down, 1=up, 2=done
bool doneHorizontally = false;
do {
await tester.pump(const Duration(milliseconds: 20));
final Rect newRect = tester.getRect(find.byWidgetPredicate(popupMenu));
switch (verticalStage) {
case 0:
if (newRect.top < rect.top) {
verticalStage = 1;
expect(newRect.bottom, greaterThanOrEqualTo(rect.bottom));
break;
}
expect(newRect.top, rect.top);
expect(newRect.bottom, greaterThan(rect.bottom));
break;
case 1:
if (newRect.top == rect.top) {
verticalStage = 2;
expect(newRect.bottom, rect.bottom);
break;
}
expect(newRect.top, lessThan(rect.top));
expect(newRect.bottom, rect.bottom);
break;
case 2:
expect(newRect.bottom, rect.bottom);
expect(newRect.top, rect.top);
break;
default:
assert(false);
}
switch (growthDirection) {
case TextDirection.rtl:
expect(newRect.right, rect.right);
if (doneHorizontally) {
expect(newRect.left, rect.left);
} else {
if (newRect.left == rect.left) {
doneHorizontally = true;
} else {
expect(newRect.left, lessThan(rect.left));
}
}
break;
case TextDirection.ltr:
expect(newRect.left, rect.left);
if (doneHorizontally) {
expect(newRect.right, rect.right);
} else {
if (newRect.right == rect.right) {
doneHorizontally = true;
} else {
expect(newRect.right, greaterThan(rect.right));
}
}
break;
}
rect = newRect;
} while (tester.binding.hasScheduledFrame);
});
}
await testPositioningDown(tester, TextDirection.ltr, Alignment.topRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 8.0, 0.0, 0.0));
await testPositioningDown(tester, TextDirection.rtl, Alignment.topRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 8.0, 0.0, 0.0));
await testPositioningDown(tester, TextDirection.ltr, Alignment.topLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 8.0, 0.0, 0.0));
await testPositioningDown(tester, TextDirection.rtl, Alignment.topLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 8.0, 0.0, 0.0));
await testPositioningDown(tester, TextDirection.ltr, Alignment.topCenter, TextDirection.ltr, const Rect.fromLTWH(350.0, 8.0, 0.0, 0.0));
await testPositioningDown(tester, TextDirection.rtl, Alignment.topCenter, TextDirection.rtl, const Rect.fromLTWH(450.0, 8.0, 0.0, 0.0));
await testPositioningDown(tester, TextDirection.ltr, Alignment.centerRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 250.0, 0.0, 0.0));
await testPositioningDown(tester, TextDirection.rtl, Alignment.centerRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 250.0, 0.0, 0.0));
await testPositioningDown(tester, TextDirection.ltr, Alignment.centerLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 250.0, 0.0, 0.0));
await testPositioningDown(tester, TextDirection.rtl, Alignment.centerLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 250.0, 0.0, 0.0));
await testPositioningDown(tester, TextDirection.ltr, Alignment.center, TextDirection.ltr, const Rect.fromLTWH(350.0, 250.0, 0.0, 0.0));
await testPositioningDown(tester, TextDirection.rtl, Alignment.center, TextDirection.rtl, const Rect.fromLTWH(450.0, 250.0, 0.0, 0.0));
await testPositioningDownThenUp(tester, TextDirection.ltr, Alignment.bottomRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 500.0, 0.0, 0.0));
await testPositioningDownThenUp(tester, TextDirection.rtl, Alignment.bottomRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 500.0, 0.0, 0.0));
await testPositioningDownThenUp(tester, TextDirection.ltr, Alignment.bottomLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 500.0, 0.0, 0.0));
await testPositioningDownThenUp(tester, TextDirection.rtl, Alignment.bottomLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 500.0, 0.0, 0.0));
await testPositioningDownThenUp(tester, TextDirection.ltr, Alignment.bottomCenter, TextDirection.ltr, const Rect.fromLTWH(350.0, 500.0, 0.0, 0.0));
await testPositioningDownThenUp(tester, TextDirection.rtl, Alignment.bottomCenter, TextDirection.rtl, const Rect.fromLTWH(450.0, 500.0, 0.0, 0.0));
});
testWidgets('PopupMenu positioning inside nested Overlay', (WidgetTester tester) async {
final Key buttonKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Example')),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (_) => Center(
child: PopupMenuButton<int>(
key: buttonKey,
itemBuilder: (_) => <PopupMenuItem<int>>[
const PopupMenuItem<int>(value: 1, child: Text('Item 1')),
const PopupMenuItem<int>(value: 2, child: Text('Item 2')),
],
child: const Text('Show Menu'),
),
),
),
],
),
),
),
),
);
final Finder buttonFinder = find.byKey(buttonKey);
final Finder popupFinder = find.bySemanticsLabel('Popup menu');
await tester.tap(buttonFinder);
await tester.pumpAndSettle();
final Offset buttonTopLeft = tester.getTopLeft(buttonFinder);
expect(tester.getTopLeft(popupFinder), buttonTopLeft);
});
testWidgets('PopupMenu positioning inside nested Navigator', (WidgetTester tester) async {
final Key buttonKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Example')),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Navigator(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: PopupMenuButton<int>(
key: buttonKey,
itemBuilder: (_) => <PopupMenuItem<int>>[
const PopupMenuItem<int>(value: 1, child: Text('Item 1')),
const PopupMenuItem<int>(value: 2, child: Text('Item 2')),
],
child: const Text('Show Menu'),
),
),
);
},
);
},
),
),
),
),
);
final Finder buttonFinder = find.byKey(buttonKey);
final Finder popupFinder = find.bySemanticsLabel('Popup menu');
await tester.tap(buttonFinder);
await tester.pumpAndSettle();
final Offset buttonTopLeft = tester.getTopLeft(buttonFinder);
expect(tester.getTopLeft(popupFinder), buttonTopLeft);
});
testWidgets('PopupMenu positioning around display features', (WidgetTester tester) async {
final Key buttonKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: MediaQuery(
data: const MediaQueryData(
size: Size(800, 600),
displayFeatures: <DisplayFeature>[
// A 20-pixel wide vertical display feature, similar to a foldable
// with a visible hinge. Splits the display into two "virtual screens"
// and the popup menu should never overlap the display feature.
DisplayFeature(
bounds: Rect.fromLTRB(390, 0, 410, 600),
type: DisplayFeatureType.cutout,
state: DisplayFeatureState.unknown,
),
],
),
child: Scaffold(
body: Navigator(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return Padding(
// Position the button in the top-right of the first "virtual screen"
padding: const EdgeInsets.only(right:390.0),
child: Align(
alignment: Alignment.topRight,
child: PopupMenuButton<int>(
key: buttonKey,
itemBuilder: (_) => <PopupMenuItem<int>>[
const PopupMenuItem<int>(value: 1, child: Text('Item 1')),
const PopupMenuItem<int>(value: 2, child: Text('Item 2')),
],
child: const Text('Show Menu'),
),
),
);
},
);
},
),
),
),
),
);
final Finder buttonFinder = find.byKey(buttonKey);
final Finder popupFinder = find.bySemanticsLabel('Popup menu');
await tester.tap(buttonFinder);
await tester.pumpAndSettle();
// Since the display feature splits the display into 2 sub-screens, popup
// menu should be positioned to fit in the first virtual screen, where the
// originating button is.
// The 8 pixels is [_kMenuScreenPadding].
expect(tester.getTopRight(popupFinder), const Offset(390 - 8, 8));
});
testWidgets('PopupMenu removes MediaQuery padding', (WidgetTester tester) async {
late BuildContext popupContext;
await tester.pumpWidget(MaterialApp(
home: MediaQuery(
data: const MediaQueryData(
padding: EdgeInsets.all(50.0),
),
child: Material(
child: PopupMenuButton<int>(
itemBuilder: (BuildContext context) {
popupContext = context;
return <PopupMenuItem<int>>[
PopupMenuItem<int>(
value: 1,
child: Builder(
builder: (BuildContext context) {
popupContext = context;
return const Text('AAA');
},
),
),
];
},
child: const SizedBox(
height: 100.0,
width: 100.0,
child: Text('XXX'),
),
),
),
),
));
await tester.tap(find.text('XXX'));
await tester.pump();
expect(MediaQuery.of(popupContext).padding, EdgeInsets.zero);
});
testWidgets('Popup Menu Offset Test', (WidgetTester tester) async {
PopupMenuButton<int> buildMenuButton({Offset offset = Offset.zero}) {
return PopupMenuButton<int>(
offset: offset,
itemBuilder: (BuildContext context) {
return <PopupMenuItem<int>>[
PopupMenuItem<int>(
value: 1,
child: Builder(
builder: (BuildContext context) {
return const Text('AAA');
},
),
),
];
},
);
}
// Popup a menu without any offset.
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Material(
child: buildMenuButton(),
),
),
),
);
// Popup the menu.
await tester.tap(find.byType(IconButton));
await tester.pumpAndSettle();
// Initial state, the menu start at Offset(8.0, 8.0), the 8 pixels is edge padding when offset.dx < 8.0.
expect(tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_PopupMenu<int?>')), const Offset(8.0, 8.0));
// Collapse the menu.
await tester.tap(find.byType(IconButton), warnIfMissed: false);
await tester.pumpAndSettle();
// Popup a new menu with Offset(50.0, 50.0).
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Material(
child: buildMenuButton(offset: const Offset(50.0, 50.0)),
),
),
),
);
await tester.tap(find.byType(IconButton));
await tester.pumpAndSettle();
// This time the menu should start at Offset(50.0, 50.0), the padding only added when offset.dx < 8.0.
expect(tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_PopupMenu<int?>')), const Offset(50.0, 50.0));
});
testWidgets('open PopupMenu has correct semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: PopupMenuButton<int>(
itemBuilder: (BuildContext context) {
return <PopupMenuItem<int>>[
const PopupMenuItem<int>(value: 1, child: Text('1')),
const PopupMenuItem<int>(value: 2, child: Text('2')),
const PopupMenuItem<int>(value: 3, child: Text('3')),
const PopupMenuItem<int>(value: 4, child: Text('4')),
const PopupMenuItem<int>(value: 5, child: Text('5')),
];
},
child: const SizedBox(
height: 100.0,
width: 100.0,
child: Text('XXX'),
),
),
),
),
);
await tester.tap(find.text('XXX'));
await tester.pumpAndSettle();
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.scopesRoute,
SemanticsFlag.namesRoute,
],
label: 'Popup menu',
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.hasImplicitScrolling,
],
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: '1',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: '2',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: '3',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: '4',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: '5',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
TestSemantics(),
],
),
],
),
ignoreId: true, ignoreTransform: true, ignoreRect: true,
));
semantics.dispose();
});
testWidgets('PopupMenuItem merges the semantics of its descendants', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: PopupMenuButton<int>(
itemBuilder: (BuildContext context) {
return <PopupMenuItem<int>>[
PopupMenuItem<int>(
value: 1,
child: Row(
children: <Widget>[
Semantics(
child: const Text('test1'),
),
Semantics(
child: const Text('test2'),
),
],
),
),
];
},
child: const SizedBox(
height: 100.0,
width: 100.0,
child: Text('XXX'),
),
),
),
),
);
await tester.tap(find.text('XXX'));
await tester.pumpAndSettle();
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.scopesRoute,
SemanticsFlag.namesRoute,
],
label: 'Popup menu',
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.hasImplicitScrolling,
],
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'test1\ntest2',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
TestSemantics(),
],
),
],
),
ignoreId: true, ignoreTransform: true, ignoreRect: true,
));
semantics.dispose();
});
testWidgets('disabled PopupMenuItem has correct semantics', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/45044.
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: PopupMenuButton<int>(
itemBuilder: (BuildContext context) {
return <PopupMenuItem<int>>[
const PopupMenuItem<int>(value: 1, child: Text('1')),
const PopupMenuItem<int>(value: 2, enabled: false ,child: Text('2')),
const PopupMenuItem<int>(value: 3, child: Text('3')),
const PopupMenuItem<int>(value: 4, child: Text('4')),
const PopupMenuItem<int>(value: 5, child: Text('5')),
];
},
child: const SizedBox(
height: 100.0,
width: 100.0,
child: Text('XXX'),
),
),
),
),
);
await tester.tap(find.text('XXX'));
await tester.pumpAndSettle();
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.scopesRoute,
SemanticsFlag.namesRoute,
],
label: 'Popup menu',
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.hasImplicitScrolling,
],
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: '1',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
],
actions: <SemanticsAction>[],
label: '2',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: '3',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: '4',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: '5',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
TestSemantics(),
],
),
],
),
ignoreId: true, ignoreTransform: true, ignoreRect: true,
));
semantics.dispose();
});
testWidgets('PopupMenuButton PopupMenuDivider', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/27072
late String selectedValue;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: PopupMenuButton<String>(
onSelected: (String result) {
selectedValue = result;
},
initialValue: '1',
child: const Text('Menu Button'),
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
const PopupMenuItem<String>(
value: '1',
child: Text('1'),
),
const PopupMenuDivider(),
const PopupMenuItem<String>(
value: '2',
child: Text('2'),
),
],
),
),
),
),
);
await tester.tap(find.text('Menu Button'));
await tester.pumpAndSettle();
expect(find.text('1'), findsOneWidget);
expect(find.byType(PopupMenuDivider), findsOneWidget);
expect(find.text('2'), findsOneWidget);
await tester.tap(find.text('1'));
await tester.pumpAndSettle();
expect(selectedValue, '1');
await tester.tap(find.text('Menu Button'));
await tester.pumpAndSettle();
expect(find.text('1'), findsOneWidget);
expect(find.byType(PopupMenuDivider), findsOneWidget);
expect(find.text('2'), findsOneWidget);
await tester.tap(find.text('2'));
await tester.pumpAndSettle();
expect(selectedValue, '2');
});
testWidgets('PopupMenuItem child height is a minimum, child is vertically centered', (WidgetTester tester) async {
final Key popupMenuButtonKey = UniqueKey();
final Type menuItemType = const PopupMenuItem<String>(child: Text('item')).runtimeType;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: PopupMenuButton<String>(
key: popupMenuButtonKey,
child: const Text('button'),
onSelected: (String result) { },
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<String>>[
// This menu item's height will be 48 because the default minimum height
// is 48 and the height of the text is less than 48.
const PopupMenuItem<String>(
value: '0',
child: Text('Item 0'),
),
// This menu item's height parameter specifies its minimum height. The
// overall height of the menu item will be 50 because the child's
// height 40, is less than 50.
const PopupMenuItem<String>(
height: 50,
value: '1',
child: SizedBox(
height: 40,
child: Text('Item 1'),
),
),
// This menu item's height parameter specifies its minimum height, so the
// overall height of the menu item will be 75.
const PopupMenuItem<String>(
height: 75,
value: '2',
child: SizedBox(
child: Text('Item 2'),
),
),
// This menu item's height will be 100.
const PopupMenuItem<String>(
value: '3',
child: SizedBox(
height: 100,
child: Text('Item 3'),
),
),
];
},
),
),
),
),
);
// Show the menu
await tester.tap(find.byKey(popupMenuButtonKey));
await tester.pumpAndSettle();
// The menu items and their InkWells should have the expected vertical size
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).height, 48);
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 1')).height, 50);
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 2')).height, 75);
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 3')).height, 100);
expect(tester.getSize(find.widgetWithText(InkWell, 'Item 0')).height, 48);
expect(tester.getSize(find.widgetWithText(InkWell, 'Item 1')).height, 50);
expect(tester.getSize(find.widgetWithText(InkWell, 'Item 2')).height, 75);
expect(tester.getSize(find.widgetWithText(InkWell, 'Item 3')).height, 100);
// Menu item children which whose height is less than the PopupMenuItem
// are vertically centered.
expect(
tester.getRect(find.widgetWithText(menuItemType, 'Item 0')).center.dy,
tester.getRect(find.text('Item 0')).center.dy,
);
expect(
tester.getRect(find.widgetWithText(menuItemType, 'Item 2')).center.dy,
tester.getRect(find.text('Item 2')).center.dy,
);
});
testWidgets('PopupMenuItem custom padding', (WidgetTester tester) async {
final Key popupMenuButtonKey = UniqueKey();
final Type menuItemType = const PopupMenuItem<String>(child: Text('item')).runtimeType;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: PopupMenuButton<String>(
key: popupMenuButtonKey,
child: const Text('button'),
onSelected: (String result) { },
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<String>>[
const PopupMenuItem<String>(
padding: EdgeInsets.zero,
value: '0',
child: Text('Item 0'),
),
const PopupMenuItem<String>(
padding: EdgeInsets.zero,
height: 0,
value: '0',
child: Text('Item 1'),
),
const PopupMenuItem<String>(
padding: EdgeInsets.all(20),
value: '0',
child: Text('Item 2'),
),
const PopupMenuItem<String>(
padding: EdgeInsets.all(20),
height: 100,
value: '0',
child: Text('Item 3'),
),
];
},
),
),
),
),
);
// Show the menu
await tester.tap(find.byKey(popupMenuButtonKey));
await tester.pumpAndSettle();
// The menu items and their InkWells should have the expected vertical size
// given the interactions between heights and padding.
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).height, 48); // Minimum interactive height (48)
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 1')).height, 16); // Height of text (16)
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 2')).height, 56); // Padding (20.0 + 20.0) + Height of text (16) = 56
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 3')).height, 100); // Height value of 100, since child (16) + padding (40) < 100
expect(tester.widget<Container>(find.widgetWithText(Container, 'Item 0')).padding, EdgeInsets.zero);
expect(tester.widget<Container>(find.widgetWithText(Container, 'Item 1')).padding, EdgeInsets.zero);
expect(tester.widget<Container>(find.widgetWithText(Container, 'Item 2')).padding, const EdgeInsets.all(20));
expect(tester.widget<Container>(find.widgetWithText(Container, 'Item 3')).padding, const EdgeInsets.all(20));
});
testWidgets('CheckedPopupMenuItem child height is a minimum, child is vertically centered', (WidgetTester tester) async {
final Key popupMenuButtonKey = UniqueKey();
final Type menuItemType = const CheckedPopupMenuItem<String>(child: Text('item')).runtimeType;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: PopupMenuButton<String>(
key: popupMenuButtonKey,
child: const Text('button'),
onSelected: (String result) { },
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<String>>[
// This menu item's height will be 56.0 because the default minimum height
// is 48, but the contents of PopupMenuItem are 56.0 tall.
const CheckedPopupMenuItem<String>(
checked: true,
value: '0',
child: Text('Item 0'),
),
// This menu item's height parameter specifies its minimum height. The
// overall height of the menu item will be 60 because the child's
// height 56, is less than 60.
const CheckedPopupMenuItem<String>(
checked: true,
height: 60,
value: '1',
child: SizedBox(
height: 40,
child: Text('Item 1'),
),
),
// This menu item's height parameter specifies its minimum height, so the
// overall height of the menu item will be 75.
const CheckedPopupMenuItem<String>(
checked: true,
height: 75,
value: '2',
child: SizedBox(
child: Text('Item 2'),
),
),
// This menu item's height will be 100.
const CheckedPopupMenuItem<String>(
checked: true,
height: 100,
value: '3',
child: SizedBox(
child: Text('Item 3'),
),
),
];
},
),
),
),
),
);
// Show the menu
await tester.tap(find.byKey(popupMenuButtonKey));
await tester.pumpAndSettle();
// The menu items and their InkWells should have the expected vertical size
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).height, 56);
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 1')).height, 60);
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 2')).height, 75);
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 3')).height, 100);
// We evaluate the InkWell at the first index because that is the ListTile's
// InkWell, which wins in the gesture arena over the child's InkWell and
// is the one of interest.
expect(tester.getSize(find.widgetWithText(InkWell, 'Item 0').at(1)).height, 56);
expect(tester.getSize(find.widgetWithText(InkWell, 'Item 1').at(1)).height, 60);
expect(tester.getSize(find.widgetWithText(InkWell, 'Item 2').at(1)).height, 75);
expect(tester.getSize(find.widgetWithText(InkWell, 'Item 3').at(1)).height, 100);
// Menu item children which whose height is less than the PopupMenuItem
// are vertically centered.
expect(
tester.getRect(find.widgetWithText(menuItemType, 'Item 0')).center.dy,
tester.getRect(find.text('Item 0')).center.dy,
);
expect(
tester.getRect(find.widgetWithText(menuItemType, 'Item 2')).center.dy,
tester.getRect(find.text('Item 2')).center.dy,
);
});
testWidgets('CheckedPopupMenuItem custom padding', (WidgetTester tester) async {
final Key popupMenuButtonKey = UniqueKey();
final Type menuItemType = const CheckedPopupMenuItem<String>(child: Text('item')).runtimeType;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: PopupMenuButton<String>(
key: popupMenuButtonKey,
child: const Text('button'),
onSelected: (String result) { },
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<String>>[
const CheckedPopupMenuItem<String>(
padding: EdgeInsets.zero,
value: '0',
child: Text('Item 0'),
),
const CheckedPopupMenuItem<String>(
padding: EdgeInsets.zero,
height: 0,
value: '0',
child: Text('Item 1'),
),
const CheckedPopupMenuItem<String>(
padding: EdgeInsets.all(20),
value: '0',
child: Text('Item 2'),
),
const CheckedPopupMenuItem<String>(
padding: EdgeInsets.all(20),
height: 100,
value: '0',
child: Text('Item 3'),
),
];
},
),
),
),
),
);
// Show the menu
await tester.tap(find.byKey(popupMenuButtonKey));
await tester.pumpAndSettle();
// The menu items and their InkWells should have the expected vertical size
// given the interactions between heights and padding.
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).height, 56); // Minimum ListTile height (56)
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 1')).height, 56); // Minimum ListTile height (56)
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 2')).height, 96); // Padding (20.0 + 20.0) + Height of ListTile (56) = 96
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 3')).height, 100); // Height value of 100, since child (56) + padding (40) < 100
expect(tester.widget<Container>(find.widgetWithText(Container, 'Item 0')).padding, EdgeInsets.zero);
expect(tester.widget<Container>(find.widgetWithText(Container, 'Item 1')).padding, EdgeInsets.zero);
expect(tester.widget<Container>(find.widgetWithText(Container, 'Item 2')).padding, const EdgeInsets.all(20));
expect(tester.widget<Container>(find.widgetWithText(Container, 'Item 3')).padding, const EdgeInsets.all(20));
});
testWidgets('Update PopupMenuItem layout while the menu is visible', (WidgetTester tester) async {
final Key popupMenuButtonKey = UniqueKey();
final Type menuItemType = const PopupMenuItem<String>(child: Text('item')).runtimeType;
Widget buildFrame({
TextDirection textDirection = TextDirection.ltr,
double fontSize = 24,
}) {
return MaterialApp(
builder: (BuildContext context, Widget? child) {
return Directionality(
textDirection: textDirection,
child: PopupMenuTheme(
data: PopupMenuTheme.of(context).copyWith(
textStyle: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: fontSize),
),
child: child!,
),
);
},
home: Scaffold(
body: PopupMenuButton<String>(
key: popupMenuButtonKey,
child: const Text('button'),
onSelected: (String result) { },
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<String>>[
const PopupMenuItem<String>(
value: '0',
child: Text('Item 0'),
),
const PopupMenuItem<String>(
value: '1',
child: Text('Item 1'),
),
];
},
),
),
);
}
// Show the menu
await tester.pumpWidget(buildFrame());
await tester.tap(find.byKey(popupMenuButtonKey));
await tester.pumpAndSettle();
// The menu items should have their default heights and horizontal alignment.
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).height, 48);
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 1')).height, 48);
expect(tester.getTopLeft(find.text('Item 0')).dx, 24);
expect(tester.getTopLeft(find.text('Item 1')).dx, 24);
// While the menu is up, change its font size to 64 (default is 16).
await tester.pumpWidget(buildFrame(fontSize: 64));
await tester.pumpAndSettle(); // Theme changes are animated.
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).height, 128);
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 1')).height, 128);
expect(tester.getSize(find.text('Item 0')).height, 128);
expect(tester.getSize(find.text('Item 1')).height, 128);
expect(tester.getTopLeft(find.text('Item 0')).dx, 24);
expect(tester.getTopLeft(find.text('Item 1')).dx, 24);
// While the menu is up, change the textDirection to rtl. Now menu items
// will be aligned right.
await tester.pumpWidget(buildFrame(textDirection: TextDirection.rtl));
await tester.pumpAndSettle(); // Theme changes are animated.
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).height, 48);
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 1')).height, 48);
expect(tester.getTopLeft(find.text('Item 0')).dx, 72);
expect(tester.getTopLeft(find.text('Item 1')).dx, 72);
});
test("PopupMenuButton's child and icon properties cannot be simultaneously defined", () {
expect(() {
PopupMenuButton<int>(
itemBuilder: (BuildContext context) => <PopupMenuItem<int>>[],
icon: const Icon(Icons.error),
child: Container(),
);
}, throwsAssertionError);
});
testWidgets('PopupMenuButton default tooltip', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
// Default Tooltip should be present when [PopupMenuButton.icon]
// and [PopupMenuButton.child] are undefined.
PopupMenuButton<int>(
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<int>>[
const PopupMenuItem<int>(
value: 1,
child: Text('Tap me please!'),
),
];
},
),
// Default Tooltip should be present when
// [PopupMenuButton.child] is defined.
PopupMenuButton<int>(
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<int>>[
const PopupMenuItem<int>(
value: 1,
child: Text('Tap me please!'),
),
];
},
child: const Text('Test text'),
),
// Default Tooltip should be present when
// [PopupMenuButton.icon] is defined.
PopupMenuButton<int>(
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<int>>[
const PopupMenuItem<int>(
value: 1,
child: Text('Tap me please!'),
),
];
},
icon: const Icon(Icons.check),
),
],
),
),
),
);
// The default tooltip is defined as [MaterialLocalizations.showMenuTooltip]
// and it is used when no tooltip is provided.
expect(find.byType(Tooltip), findsNWidgets(3));
expect(find.byTooltip(const DefaultMaterialLocalizations().showMenuTooltip), findsNWidgets(3));
});
testWidgets('PopupMenuButton custom tooltip', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
// Tooltip should work when [PopupMenuButton.icon]
// and [PopupMenuButton.child] are undefined.
PopupMenuButton<int>(
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<int>>[
const PopupMenuItem<int>(
value: 1,
child: Text('Tap me please!'),
),
];
},
tooltip: 'Test tooltip',
),
// Tooltip should work when
// [PopupMenuButton.child] is defined.
PopupMenuButton<int>(
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<int>>[
const PopupMenuItem<int>(
value: 1,
child: Text('Tap me please!'),
),
];
},
tooltip: 'Test tooltip',
child: const Text('Test text'),
),
// Tooltip should work when
// [PopupMenuButton.icon] is defined.
PopupMenuButton<int>(
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<int>>[
const PopupMenuItem<int>(
value: 1,
child: Text('Tap me please!'),
),
];
},
tooltip: 'Test tooltip',
icon: const Icon(Icons.check),
),
],
),
),
),
);
expect(find.byType(Tooltip), findsNWidgets(3));
expect(find.byTooltip('Test tooltip'), findsNWidgets(3));
});
testWidgets('Allow Widget for PopupMenuButton.icon', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: PopupMenuButton<int>(
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<int>>[
const PopupMenuItem<int>(
value: 1,
child: Text('Tap me please!'),
),
];
},
tooltip: 'Test tooltip',
icon: const Text('PopupMenuButton icon'),
),
),
),
);
expect(find.text('PopupMenuButton icon'), findsOneWidget);
});
testWidgets('showMenu uses nested navigator by default', (WidgetTester tester) async {
final MenuObserver rootObserver = MenuObserver();
final MenuObserver nestedObserver = MenuObserver();
await tester.pumpWidget(MaterialApp(
navigatorObservers: <NavigatorObserver>[rootObserver],
home: Navigator(
observers: <NavigatorObserver>[nestedObserver],
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return ElevatedButton(
onPressed: () {
showMenu<int>(
context: context,
position: RelativeRect.fill,
items: <PopupMenuItem<int>>[
const PopupMenuItem<int>(
value: 1, child: Text('1'),
),
],
);
},
child: const Text('Show Menu'),
);
},
);
},
),
));
// Open the dialog.
await tester.tap(find.byType(ElevatedButton));
expect(rootObserver.menuCount, 0);
expect(nestedObserver.menuCount, 1);
});
testWidgets('showMenu uses root navigator if useRootNavigator is true', (WidgetTester tester) async {
final MenuObserver rootObserver = MenuObserver();
final MenuObserver nestedObserver = MenuObserver();
await tester.pumpWidget(MaterialApp(
navigatorObservers: <NavigatorObserver>[rootObserver],
home: Navigator(
observers: <NavigatorObserver>[nestedObserver],
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return ElevatedButton(
onPressed: () {
showMenu<int>(
context: context,
useRootNavigator: true,
position: RelativeRect.fill,
items: <PopupMenuItem<int>>[
const PopupMenuItem<int>(
value: 1, child: Text('1'),
),
],
);
},
child: const Text('Show Menu'),
);
},
);
},
),
));
// Open the dialog.
await tester.tap(find.byType(ElevatedButton));
expect(rootObserver.menuCount, 1);
expect(nestedObserver.menuCount, 0);
});
testWidgets('PopupMenuButton calling showButtonMenu manually', (WidgetTester tester) async {
final GlobalKey<PopupMenuButtonState<int>> globalKey = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
PopupMenuButton<int>(
key: globalKey,
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<int>>[
const PopupMenuItem<int>(
value: 1,
child: Text('Tap me please!'),
),
];
},
),
],
),
),
),
);
expect(find.text('Tap me please!'), findsNothing);
globalKey.currentState!.showButtonMenu();
// The PopupMenuItem will appear after an animation, hence,
// we have to first wait for the tester to settle.
await tester.pumpAndSettle();
expect(find.text('Tap me please!'), findsOneWidget);
});
testWidgets('PopupMenuItem changes mouse cursor when hovered', (WidgetTester tester) async {
const Key key = ValueKey<int>(1);
// Test PopupMenuItem() constructor
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: PopupMenuItem<int>(
key: key,
mouseCursor: SystemMouseCursors.text,
value: 1,
child: Container(),
),
),
),
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: tester.getCenter(find.byKey(key)));
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
// Test default cursor
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: PopupMenuItem<int>(
key: key,
value: 1,
child: Container(),
),
),
),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
// Test default cursor when disabled
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: PopupMenuItem<int>(
key: key,
value: 1,
enabled: false,
child: Container(),
),
),
),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
});
testWidgets('CheckedPopupMenuItem changes mouse cursor when hovered', (WidgetTester tester) async {
const Key key = ValueKey<int>(1);
// Test CheckedPopupMenuItem() constructor
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: CheckedPopupMenuItem<int>(
key: key,
mouseCursor: SystemMouseCursors.text,
value: 1,
child: Container(),
),
),
),
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: tester.getCenter(find.byKey(key)));
addTearDown(gesture.removePointer);
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
// Test default cursor
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: CheckedPopupMenuItem<int>(
key: key,
value: 1,
child: Container(),
),
),
),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
// Test default cursor when disabled
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: CheckedPopupMenuItem<int>(
key: key,
value: 1,
enabled: false,
child: Container(),
),
),
),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
});
testWidgets('PopupMenu in AppBar does not overlap with the status bar', (WidgetTester tester) async {
const List<PopupMenuItem<int>> choices = <PopupMenuItem<int>>[
PopupMenuItem<int>(value: 1, child: Text('Item 1')),
PopupMenuItem<int>(value: 2, child: Text('Item 2')),
PopupMenuItem<int>(value: 3, child: Text('Item 3')),
];
const double statusBarHeight = 24.0;
final PopupMenuItem<int> firstItem = choices[0];
int selectedValue = choices[0].value!;
await tester.pumpWidget(
MaterialApp(
builder: (BuildContext context, Widget? child) {
return MediaQuery(
data: const MediaQueryData(padding: EdgeInsets.only(top: statusBarHeight)), // status bar
child: child!,
);
},
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Scaffold(
appBar: AppBar(
title: const Text('PopupMenu Test'),
actions: <Widget>[
PopupMenuButton<int>(
onSelected: (int result) {
setState(() {
selectedValue = result;
});
},
initialValue: selectedValue,
itemBuilder: (BuildContext context) {
return choices;
},
),
],
),
);
},
),
),
);
await tester.tap(find.byIcon(Icons.more_vert));
await tester.pumpAndSettle();
// Tap third item.
await tester.tap(find.text('Item 3'));
await tester.pumpAndSettle();
// Open popupMenu again.
await tester.tap(find.byIcon(Icons.more_vert));
await tester.pumpAndSettle();
// Check whether the first item is not overlapping with status bar.
expect(tester.getTopLeft(find.byWidget(firstItem)).dy, greaterThan(statusBarHeight));
});
testWidgets('Vertically long PopupMenu does not overlap with the status bar and bottom notch', (WidgetTester tester) async {
const double windowPaddingTop = 44;
const double windowPaddingBottom = 34;
await tester.pumpWidget(
MaterialApp(
builder: (BuildContext context, Widget? child) {
return MediaQuery(
data: const MediaQueryData(
padding: EdgeInsets.only(
top: windowPaddingTop,
bottom: windowPaddingBottom,
),
),
child: child!,
);
},
home: Scaffold(
appBar: AppBar(
title: const Text('PopupMenu Test'),
),
body: PopupMenuButton<int>(
child: const Text('Show Menu'),
itemBuilder: (BuildContext context) => Iterable<PopupMenuItem<int>>.generate(
20, (int i) => PopupMenuItem<int>(
value: i,
child: Text('Item $i'),
),
).toList(),
),
),
),
);
await tester.tap(find.text('Show Menu'));
await tester.pumpAndSettle();
final Offset topRightOfMenu = tester.getTopRight(find.byType(SingleChildScrollView));
final Offset bottomRightOfMenu = tester.getBottomRight(find.byType(SingleChildScrollView));
expect(topRightOfMenu.dy, windowPaddingTop + 8.0);
expect(bottomRightOfMenu.dy, 600.0 - windowPaddingBottom - 8.0); // Screen height is 600.
});
testWidgets('PopupMenu position test when have unsafe area', (WidgetTester tester) async {
final GlobalKey buttonKey = GlobalKey();
Widget buildFrame(double width, double height) {
return MaterialApp(
builder: (BuildContext context, Widget? child) {
return MediaQuery(
data: const MediaQueryData(
padding: EdgeInsets.only(
top: 32.0,
bottom: 32.0,
),
),
child: child!,
);
},
home: Scaffold(
appBar: AppBar(
title: const Text('PopupMenu Test'),
actions: <Widget>[
PopupMenuButton<int>(
child: SizedBox(
key: buttonKey,
height: height,
width: width,
child: const ColoredBox(
color: Colors.pink,
),
),
itemBuilder: (BuildContext context) => <PopupMenuEntry<int>>[
const PopupMenuItem<int>(value: 1, child: Text('-1-')),
const PopupMenuItem<int>(value: 2, child: Text('-2-')),
],
),
],
),
body: Container(),
),
);
}
await tester.pumpWidget(buildFrame(20.0, 20.0));
await tester.tap(find.byKey(buttonKey));
await tester.pumpAndSettle();
final Offset button = tester.getTopRight(find.byKey(buttonKey));
expect(button, const Offset(800.0, 32.0)); // The topPadding is 32.0.
final Offset popupMenu = tester.getTopRight(find.byType(SingleChildScrollView));
// The menu should be positioned directly next to the top of the button.
// The 8.0 pixels is [_kMenuScreenPadding].
expect(popupMenu, Offset(button.dx - 8.0, button.dy + 8.0));
});
// Regression test for https://github.com/flutter/flutter/issues/82874
testWidgets('PopupMenu position test when have unsafe area - left/right padding', (WidgetTester tester) async {
final GlobalKey buttonKey = GlobalKey();
const EdgeInsets padding = EdgeInsets.only(left: 300.0, top: 32.0, right: 310.0, bottom: 64.0);
EdgeInsets? mediaQueryPadding;
Widget buildFrame(double width, double height) {
return MaterialApp(
builder: (BuildContext context, Widget? child) {
return MediaQuery(
data: const MediaQueryData(
padding: padding,
),
child: child!,
);
},
home: Scaffold(
appBar: AppBar(
title: const Text('PopupMenu Test'),
actions: <Widget>[
PopupMenuButton<int>(
child: SizedBox(
key: buttonKey,
height: height,
width: width,
child: const ColoredBox(
color: Colors.pink,
),
),
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<int>>[
PopupMenuItem<int>(
value: 1,
child: Builder(
builder: (BuildContext context) {
mediaQueryPadding = MediaQuery.of(context).padding;
return Text('-1-' * 500); // A long long text string.
},
),
),
const PopupMenuItem<int>(value: 2, child: Text('-2-')),
];
},
),
],
),
body: const SizedBox.shrink(),
),
);
}
await tester.pumpWidget(buildFrame(20.0, 20.0));
await tester.tap(find.byKey(buttonKey));
await tester.pumpAndSettle();
final Offset button = tester.getTopRight(find.byKey(buttonKey));
expect(button, Offset(800.0 - padding.right, padding.top)); // The topPadding is 32.0.
final Offset popupMenuTopRight = tester.getTopRight(find.byType(SingleChildScrollView));
// The menu should be positioned directly next to the top of the button.
// The 8.0 pixels is [_kMenuScreenPadding].
expect(popupMenuTopRight, Offset(800.0 - padding.right - 8.0, padding.top + 8.0));
final Offset popupMenuTopLeft = tester.getTopLeft(find.byType(SingleChildScrollView));
expect(popupMenuTopLeft, Offset(padding.left + 8.0, padding.top + 8.0));
final Offset popupMenuBottomLeft = tester.getBottomLeft(find.byType(SingleChildScrollView));
expect(popupMenuBottomLeft, Offset(padding.left + 8.0, 600.0 - padding.bottom - 8.0));
// The `MediaQueryData.padding` should be removed.
expect(mediaQueryPadding, EdgeInsets.zero);
});
group('feedback', () {
late FeedbackTester feedback;
setUp(() {
feedback = FeedbackTester();
});
tearDown(() {
feedback.dispose();
});
Widget buildFrame({ bool? widgetEnableFeedback, bool? themeEnableFeedback }) {
return MaterialApp(
home: Scaffold(
body: PopupMenuTheme(
data: PopupMenuThemeData(
enableFeedback: themeEnableFeedback,
),
child: PopupMenuButton<int>(
enableFeedback: widgetEnableFeedback,
child: const Text('Show Menu'),
itemBuilder: (BuildContext context) {
return <PopupMenuItem<int>>[
const PopupMenuItem<int>(
value: 1,
child: Text('One'),
),
];
},
),
),
),
);
}
testWidgets('PopupMenuButton enableFeedback works properly', (WidgetTester tester) async {
expect(feedback.clickSoundCount, 0);
expect(feedback.hapticCount, 0);
// PopupMenuButton with enabled feedback.
await tester.pumpWidget(buildFrame(widgetEnableFeedback: true));
await tester.tap(find.text('Show Menu'));
await tester.pumpAndSettle();
expect(feedback.clickSoundCount, 1);
expect(feedback.hapticCount, 0);
await tester.pumpWidget(Container());
// PopupMenuButton with disabled feedback.
await tester.pumpWidget(buildFrame(widgetEnableFeedback: false));
await tester.tap(find.text('Show Menu'));
await tester.pumpAndSettle();
expect(feedback.clickSoundCount, 1);
expect(feedback.hapticCount, 0);
await tester.pumpWidget(Container());
// PopupMenuButton with enabled feedback by default.
await tester.pumpWidget(buildFrame());
await tester.tap(find.text('Show Menu'));
await tester.pumpAndSettle();
expect(feedback.clickSoundCount, 2);
expect(feedback.hapticCount, 0);
await tester.pumpWidget(Container());
// PopupMenu with disabled feedback using PopupMenuButtonTheme.
await tester.pumpWidget(buildFrame(themeEnableFeedback: false));
await tester.tap(find.text('Show Menu'));
await tester.pumpAndSettle();
expect(feedback.clickSoundCount, 2);
expect(feedback.hapticCount, 0);
await tester.pumpWidget(Container());
// PopupMenu enableFeedback property overrides PopupMenuButtonTheme.
await tester.pumpWidget(buildFrame(widgetEnableFeedback: false,themeEnableFeedback: true));
await tester.tap(find.text('Show Menu'));
await tester.pumpAndSettle();
expect(feedback.clickSoundCount, 2);
expect(feedback.hapticCount, 0);
});
});
testWidgets('iconSize parameter tests', (WidgetTester tester) async {
Future<void> buildFrame({double? iconSize}) {
return tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: PopupMenuButton<String>(
iconSize: iconSize,
itemBuilder: (_) => <PopupMenuEntry<String>>[
const PopupMenuItem<String>(
value: 'value',
child: Text('child'),
),
],
),
),
),
),
);
}
await buildFrame();
expect(tester.getSize(find.byIcon(Icons.adaptive.more)), const Size(24, 24));
await buildFrame(iconSize: 50);
expect(tester.getSize(find.byIcon(Icons.adaptive.more)), const Size(50, 50));
});
testWidgets('does not crash in small overlay', (WidgetTester tester) async {
final GlobalKey navigator = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Column(
children: <Widget>[
OutlinedButton(
onPressed: () {
showMenu<void>(
context: navigator.currentContext!,
position: RelativeRect.fill,
items: const <PopupMenuItem<void>>[
PopupMenuItem<void>(child: Text('foo')),
],
);
},
child: const Text('press'),
),
SizedBox(
height: 10,
width: 10,
child: Navigator(
key: navigator,
onGenerateRoute: (RouteSettings settings) => MaterialPageRoute<void>(
builder: (BuildContext context) => Container(color: Colors.red),
),
),
),
],
),
),
),
);
await tester.tap(find.text('press'));
await tester.pumpAndSettle();
expect(find.text('foo'), findsOneWidget);
});
// Regression test for https://github.com/flutter/flutter/issues/80869
testWidgets('The menu position test in the scrollable widget', (WidgetTester tester) async {
final GlobalKey buttonKey = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SingleChildScrollView(
child: Column(
children: <Widget>[
const SizedBox(height: 100),
PopupMenuButton<int>(
child: SizedBox(
key: buttonKey,
height: 10.0,
width: 10.0,
child: const ColoredBox(
color: Colors.pink,
),
),
itemBuilder: (BuildContext context) => <PopupMenuEntry<int>>[
const PopupMenuItem<int>(value: 1, child: Text('-1-')),
const PopupMenuItem<int>(value: 2, child: Text('-2-')),
],
),
const SizedBox(height: 600),
],
),
),
),
),
);
// Open the menu.
await tester.tap(find.byKey(buttonKey));
await tester.pumpAndSettle();
Offset button = tester.getTopLeft(find.byKey(buttonKey));
expect(button, const Offset(0.0, 100.0));
Offset popupMenu = tester.getTopLeft(find.byType(SingleChildScrollView).last);
// The menu should be positioned directly next to the top of the button.
// The 8.0 pixels is [_kMenuScreenPadding].
expect(popupMenu, const Offset(8.0, 100.0));
// Close the menu.
await tester.tap(find.byKey(buttonKey), warnIfMissed: false);
await tester.pumpAndSettle();
// Scroll a little bit.
await tester.drag(find.byType(SingleChildScrollView), const Offset(0.0, -50.0));
button = tester.getTopLeft(find.byKey(buttonKey));
expect(button, const Offset(0.0, 50.0));
// Open the menu again.
await tester.tap(find.byKey(buttonKey));
await tester.pumpAndSettle();
popupMenu = tester.getTopLeft(find.byType(SingleChildScrollView).last);
// The menu should be positioned directly next to the top of the button.
// The 8.0 pixels is [_kMenuScreenPadding].
expect(popupMenu, const Offset(8.0, 50.0));
});
testWidgets('PopupMenuButton custom splash radius', (WidgetTester tester) async {
Future<void> buildFrameWithoutChild({double? splashRadius}) {
return tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: PopupMenuButton<String>(
splashRadius: splashRadius,
itemBuilder: (_) => <PopupMenuEntry<String>>[
const PopupMenuItem<String>(
value: 'value',
child: Text('child'),
),
],
),
),
),
),
);
}
Future<void> buildFrameWithChild({double? splashRadius}) {
return tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: PopupMenuButton<String>(
splashRadius: splashRadius,
child: const Text('An item'),
itemBuilder: (_) => <PopupMenuEntry<String>>[
const PopupMenuDivider(),
],
),
),
),
),
);
}
await buildFrameWithoutChild();
expect(tester.widget<InkResponse>(find.byType(InkResponse)).radius,
Material.defaultSplashRadius);
await buildFrameWithChild();
expect(tester.widget<InkWell>(find.byType(InkWell)).radius, null);
const double testSplashRadius = 50;
await buildFrameWithoutChild(splashRadius: testSplashRadius);
expect(tester.widget<InkResponse>(find.byType(InkResponse)).radius,
testSplashRadius);
await buildFrameWithChild(splashRadius: testSplashRadius);
expect(tester.widget<InkWell>(find.byType(InkWell)).radius,
testSplashRadius);
});
testWidgets('Can override menu size constraints', (WidgetTester tester) async {
final Key popupMenuButtonKey = UniqueKey();
final Type menuItemType = const PopupMenuItem<String>(child: Text('item')).runtimeType;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: PopupMenuButton<String>(
key: popupMenuButtonKey,
constraints: const BoxConstraints(
minWidth: 500,
),
itemBuilder: (_) => <PopupMenuEntry<String>>[
const PopupMenuItem<String>(
value: 'value',
child: Text('Item 0'),
),
],
),
),
),
),
);
// Show the menu
await tester.tap(find.byKey(popupMenuButtonKey));
await tester.pumpAndSettle();
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).height, 48);
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).width, 500);
});
testWidgets('Can change menu position and offset', (WidgetTester tester) async {
PopupMenuButton<int> buildMenuButton({required PopupMenuPosition position}) {
return PopupMenuButton<int>(
position: position,
itemBuilder: (BuildContext context) {
return <PopupMenuItem<int>>[
PopupMenuItem<int>(
value: 1,
child: Builder(
builder: (BuildContext context) {
return const Text('AAA');
},
),
),
];
},
);
}
// Popup menu with `MenuPosition.over (default) with default offset`.
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Material(
child: buildMenuButton(position: PopupMenuPosition.over),
),
),
),
);
// Open the popup menu.
await tester.tap(find.byType(IconButton));
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_PopupMenu<int?>')), const Offset(8.0, 8.0));
// Close the popup menu.
await tester.tapAt(Offset.zero);
await tester.pump();
// Popup menu with `MenuPosition.under`(custom) with default offset`.
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Material(
child: buildMenuButton(position: PopupMenuPosition.under),
),
),
),
);
// Open the popup menu.
await tester.tap(find.byType(IconButton));
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_PopupMenu<int?>')), const Offset(8.0, 40.0));
// Close the popup menu.
await tester.tapAt(Offset.zero);
await tester.pump();
// Popup menu with `MenuPosition.over (default) with custom offset`.
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Material(
child: PopupMenuButton<int>(
offset: const Offset(0.0, 50),
itemBuilder: (BuildContext context) {
return <PopupMenuItem<int>>[
PopupMenuItem<int>(
value: 1,
child: Builder(
builder: (BuildContext context) {
return const Text('AAA');
},
),
),
];
},
),
),
),
),
);
// Open the popup menu.
await tester.tap(find.byType(IconButton));
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_PopupMenu<int?>')), const Offset(8.0, 50.0));
// Close the popup menu.
await tester.tapAt(Offset.zero);
await tester.pump();
// Popup menu with `MenuPosition.under (custom) with custom offset`.
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Material(
child: PopupMenuButton<int>(
offset: const Offset(0.0, 50),
position: PopupMenuPosition.under,
itemBuilder: (BuildContext context) {
return <PopupMenuItem<int>>[
PopupMenuItem<int>(
value: 1,
child: Builder(
builder: (BuildContext context) {
return const Text('AAA');
},
),
),
];
},
),
),
),
),
);
// Open the popup menu.
await tester.tap(find.byType(IconButton));
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_PopupMenu<int?>')), const Offset(8.0, 90.0));
});
testWidgets("PopupMenuButton icon inherits IconTheme's size", (WidgetTester tester) async {
Widget buildPopupMenu({double? themeIconSize, double? iconSize}) {
return MaterialApp(
theme: ThemeData(
iconTheme: IconThemeData(
size: themeIconSize,
),
),
home: Scaffold(
body: Center(
child: PopupMenuButton<String>(
iconSize: iconSize,
itemBuilder: (_) => <PopupMenuEntry<String>>[
const PopupMenuItem<String>(
value: 'value',
child: Text('Item 0'),
),
],
),
),
),
);
}
// Popup menu with default icon size.
await tester.pumpWidget(buildPopupMenu());
// Default PopupMenuButton icon size is 24.0.
expect(tester.getSize(find.byIcon(Icons.more_vert)), const Size(24.0, 24.0));
// Popup menu with custom theme icon size.
await tester.pumpWidget(buildPopupMenu(themeIconSize: 30.0));
await tester.pumpAndSettle();
// PopupMenuButton icon inherits IconTheme's size.
expect(tester.getSize(find.byIcon(Icons.more_vert)), const Size(30.0, 30.0));
// Popup menu with custom icon size.
await tester.pumpWidget(buildPopupMenu(themeIconSize: 30.0, iconSize: 50.0));
await tester.pumpAndSettle();
// PopupMenuButton icon size overrides IconTheme's size.
expect(tester.getSize(find.byIcon(Icons.more_vert)), const Size(50.0, 50.0));
});
testWidgets('Popup menu clip behavior', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/107215
final Key popupButtonKey = UniqueKey();
const double radius = 20.0;
Widget buildPopupMenu({required Clip clipBehavior}) {
return MaterialApp(
home: Scaffold(
body: Center(
child: PopupMenuButton<String>(
key: popupButtonKey,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(radius)),
),
clipBehavior: clipBehavior,
itemBuilder: (_) => <PopupMenuEntry<String>>[
const PopupMenuItem<String>(
value: 'value',
child: Text('Item 0'),
),
],
),
),
),
);
}
// Popup menu with default ClipBehavior.
await tester.pumpWidget(buildPopupMenu(clipBehavior: Clip.none));
// Open the popup to build and show the menu contents.
await tester.tap(find.byKey(popupButtonKey));
await tester.pumpAndSettle();
Material material = tester.widget<Material>(find.byType(Material).last);
expect(material.clipBehavior, Clip.none);
// Close the popup menu.
await tester.tapAt(Offset.zero);
await tester.pumpAndSettle();
// Popup menu with custom ClipBehavior.
await tester.pumpWidget(buildPopupMenu(clipBehavior: Clip.hardEdge));
// Open the popup to build and show the menu contents.
await tester.tap(find.byKey(popupButtonKey));
await tester.pumpAndSettle();
material = tester.widget<Material>(find.byType(Material).last);
expect(material.clipBehavior, Clip.hardEdge);
});
}
class TestApp extends StatelessWidget {
const TestApp({
super.key,
required this.textDirection,
this.child,
});
final TextDirection textDirection;
final Widget? child;
@override
Widget build(BuildContext context) {
return Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultWidgetsLocalizations.delegate,
DefaultMaterialLocalizations.delegate,
],
child: MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
child: Directionality(
textDirection: textDirection,
child: Navigator(
onGenerateRoute: (RouteSettings settings) {
assert(settings.name == '/');
return MaterialPageRoute<void>(
settings: settings,
builder: (BuildContext context) => Material(
child: child,
),
);
},
),
),
),
);
}
}
class MenuObserver extends NavigatorObserver {
int menuCount = 0;
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route.toString().contains('_PopupMenuRoute')) {
menuCount++;
}
super.didPush(route, previousRoute);
}
}