| // Copyright 2016 The Chromium 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 window, SemanticsFlag; |
| |
| import 'package:flutter_test/flutter_test.dart'; |
| import 'package:flutter/material.dart'; |
| |
| import '../widgets/semantics_tester.dart'; |
| |
| void main() { |
| testWidgets('Navigator.push works within a PopupMenuButton', (WidgetTester tester) async { |
| final Key targetKey = new UniqueKey(); |
| await tester.pumpWidget( |
| new MaterialApp( |
| routes: <String, WidgetBuilder> { |
| '/next': (BuildContext context) { |
| return const Text('Next'); |
| }, |
| }, |
| home: new Material( |
| child: new Center( |
| child: new Builder( |
| key: targetKey, |
| builder: (BuildContext context) { |
| return new 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 onCanceled callback when an item is not selected', (WidgetTester tester) async { |
| int cancels = 0; |
| BuildContext popupContext; |
| final Key noCallbackKey = new UniqueKey(); |
| final Key withCallbackKey = new UniqueKey(); |
| |
| await tester.pumpWidget( |
| new MaterialApp( |
| home: new Material( |
| child: new Column( |
| children: <Widget>[ |
| new PopupMenuButton<int>( |
| key: noCallbackKey, |
| itemBuilder: (BuildContext context) { |
| return <PopupMenuEntry<int>>[ |
| const PopupMenuItem<int>( |
| value: 1, |
| child: Text('Tap me please!'), |
| ), |
| ]; |
| }, |
| ), |
| new 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(const Offset(0.0, 0.0)); |
| 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(const Offset(0.0, 0.0)); |
| 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('PopupMenuButton is horizontal on iOS', (WidgetTester tester) async { |
| Widget build(TargetPlatform platform) { |
| return new MaterialApp( |
| theme: new ThemeData(platform: platform), |
| home: new Scaffold( |
| appBar: new AppBar( |
| actions: <Widget>[ |
| new 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); |
| }); |
| |
| 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(() { |
| new PopupMenuButton<int>( |
| child: const Text('heyo'), |
| icon: const Icon(Icons.view_carousel), |
| itemBuilder: simplePopupMenuItemBuilder, |
| ); |
| }, throwsA(isInstanceOf<AssertionError>())); |
| }); |
| |
| testWidgets('PopupMenuButton creates IconButton when given an icon', (WidgetTester tester) async { |
| final PopupMenuButton<int> button = new PopupMenuButton<int>( |
| icon: const Icon(Icons.view_carousel), |
| itemBuilder: simplePopupMenuItemBuilder, |
| ); |
| |
| await tester.pumpWidget(new MaterialApp( |
| home: new Scaffold( |
| appBar: new 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 = new 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'), |
| ), |
| ); |
| final WidgetPredicate 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<Null> openMenu(TextDirection textDirection, Alignment alignment) async { |
| return TestAsyncUtils.guard(() async { |
| await tester.pumpWidget(new Container()); // reset in case we had a menu up already |
| await tester.pumpWidget(new TestApp( |
| textDirection: textDirection, |
| child: new Align( |
| alignment: alignment, |
| child: testButton, |
| ), |
| )); |
| await tester.tap(find.text('XXX')); |
| await tester.pump(); |
| }); |
| } |
| |
| Future<Null> testPositioningDown( |
| WidgetTester tester, |
| TextDirection textDirection, |
| Alignment alignment, |
| TextDirection growthDirection, |
| Rect startRect, |
| ) { |
| return TestAsyncUtils.guard(() 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<Null> testPositioningDownThenUp( |
| WidgetTester tester, |
| TextDirection textDirection, |
| Alignment alignment, |
| TextDirection growthDirection, |
| Rect startRect, |
| ) { |
| return TestAsyncUtils.guard(() 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, new Rect.fromLTWH(792.0, 8.0, 0.0, 0.0)); |
| await testPositioningDown(tester, TextDirection.rtl, Alignment.topRight, TextDirection.rtl, new Rect.fromLTWH(792.0, 8.0, 0.0, 0.0)); |
| await testPositioningDown(tester, TextDirection.ltr, Alignment.topLeft, TextDirection.ltr, new Rect.fromLTWH(8.0, 8.0, 0.0, 0.0)); |
| await testPositioningDown(tester, TextDirection.rtl, Alignment.topLeft, TextDirection.ltr, new Rect.fromLTWH(8.0, 8.0, 0.0, 0.0)); |
| await testPositioningDown(tester, TextDirection.ltr, Alignment.topCenter, TextDirection.ltr, new Rect.fromLTWH(350.0, 8.0, 0.0, 0.0)); |
| await testPositioningDown(tester, TextDirection.rtl, Alignment.topCenter, TextDirection.rtl, new Rect.fromLTWH(450.0, 8.0, 0.0, 0.0)); |
| await testPositioningDown(tester, TextDirection.ltr, Alignment.centerRight, TextDirection.rtl, new Rect.fromLTWH(792.0, 250.0, 0.0, 0.0)); |
| await testPositioningDown(tester, TextDirection.rtl, Alignment.centerRight, TextDirection.rtl, new Rect.fromLTWH(792.0, 250.0, 0.0, 0.0)); |
| await testPositioningDown(tester, TextDirection.ltr, Alignment.centerLeft, TextDirection.ltr, new Rect.fromLTWH(8.0, 250.0, 0.0, 0.0)); |
| await testPositioningDown(tester, TextDirection.rtl, Alignment.centerLeft, TextDirection.ltr, new Rect.fromLTWH(8.0, 250.0, 0.0, 0.0)); |
| await testPositioningDown(tester, TextDirection.ltr, Alignment.center, TextDirection.ltr, new Rect.fromLTWH(350.0, 250.0, 0.0, 0.0)); |
| await testPositioningDown(tester, TextDirection.rtl, Alignment.center, TextDirection.rtl, new Rect.fromLTWH(450.0, 250.0, 0.0, 0.0)); |
| await testPositioningDownThenUp(tester, TextDirection.ltr, Alignment.bottomRight, TextDirection.rtl, new Rect.fromLTWH(792.0, 500.0, 0.0, 0.0)); |
| await testPositioningDownThenUp(tester, TextDirection.rtl, Alignment.bottomRight, TextDirection.rtl, new Rect.fromLTWH(792.0, 500.0, 0.0, 0.0)); |
| await testPositioningDownThenUp(tester, TextDirection.ltr, Alignment.bottomLeft, TextDirection.ltr, new Rect.fromLTWH(8.0, 500.0, 0.0, 0.0)); |
| await testPositioningDownThenUp(tester, TextDirection.rtl, Alignment.bottomLeft, TextDirection.ltr, new Rect.fromLTWH(8.0, 500.0, 0.0, 0.0)); |
| await testPositioningDownThenUp(tester, TextDirection.ltr, Alignment.bottomCenter, TextDirection.ltr, new Rect.fromLTWH(350.0, 500.0, 0.0, 0.0)); |
| await testPositioningDownThenUp(tester, TextDirection.rtl, Alignment.bottomCenter, TextDirection.rtl, new Rect.fromLTWH(450.0, 500.0, 0.0, 0.0)); |
| }); |
| |
| testWidgets('PopupMenu removes MediaQuery padding', (WidgetTester tester) async { |
| BuildContext popupContext; |
| |
| await tester.pumpWidget(new MaterialApp( |
| home: new MediaQuery( |
| data: const MediaQueryData( |
| padding: EdgeInsets.all(50.0), |
| ), |
| child: new Material( |
| child: new PopupMenuButton<int>( |
| itemBuilder: (BuildContext context) { |
| popupContext = context; |
| return <PopupMenuItem<int>>[ |
| new PopupMenuItem<int>( |
| value: 1, |
| child: new 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('open PopupMenu has correct semantics', (WidgetTester tester) async { |
| final SemanticsTester semantics = new SemanticsTester(tester); |
| await tester.pumpWidget(new MaterialApp( |
| home: new Material( |
| child: new 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( |
| new TestSemantics.root( |
| children: <TestSemantics>[ |
| new TestSemantics( |
| textDirection: TextDirection.ltr, |
| children: <TestSemantics>[ |
| new TestSemantics( |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.scopesRoute, |
| SemanticsFlag.namesRoute, |
| ], |
| label: 'Popup menu', |
| textDirection: TextDirection.ltr, |
| children: <TestSemantics>[ |
| new TestSemantics( |
| children: <TestSemantics>[ |
| new TestSemantics( |
| actions: <SemanticsAction>[SemanticsAction.tap], |
| label: '1', |
| textDirection: TextDirection.ltr, |
| ), |
| new TestSemantics( |
| actions: <SemanticsAction>[SemanticsAction.tap], |
| label: '2', |
| textDirection: TextDirection.ltr, |
| ), |
| new TestSemantics( |
| actions: <SemanticsAction>[SemanticsAction.tap], |
| label: '3', |
| textDirection: TextDirection.ltr, |
| ), |
| new TestSemantics( |
| actions: <SemanticsAction>[SemanticsAction.tap], |
| label: '4', |
| textDirection: TextDirection.ltr, |
| ), |
| new TestSemantics( |
| actions: <SemanticsAction>[SemanticsAction.tap], |
| label: '5', |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ignoreId: true, ignoreTransform: true, ignoreRect: true, |
| )); |
| |
| semantics.dispose(); |
| }); |
| } |
| |
| class TestApp extends StatefulWidget { |
| const TestApp({ this.textDirection, this.child }); |
| final TextDirection textDirection; |
| final Widget child; |
| @override |
| _TestAppState createState() => new _TestAppState(); |
| } |
| |
| class _TestAppState extends State<TestApp> { |
| @override |
| Widget build(BuildContext context) { |
| return new Localizations( |
| locale: const Locale('en', 'US'), |
| delegates: const <LocalizationsDelegate<dynamic>>[ |
| DefaultWidgetsLocalizations.delegate, |
| DefaultMaterialLocalizations.delegate, |
| ], |
| child: new MediaQuery( |
| data: new MediaQueryData.fromWindow(window), |
| child: new Directionality( |
| textDirection: widget.textDirection, |
| child: new Navigator( |
| onGenerateRoute: (RouteSettings settings) { |
| assert(settings.name == '/'); |
| return new MaterialPageRoute<void>( |
| settings: settings, |
| builder: (BuildContext context) => new Material( |
| child: widget.child, |
| ), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| } |
| } |