| // 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. |
| |
| // reduced-test-set: |
| // This file is run as part of a reduced test set in CI on Mac and Windows |
| // machines. |
| @Tags(<String>['reduced-test-set']) |
| library; |
| |
| import 'package:clock/clock.dart'; |
| import 'package:flutter/cupertino.dart'; |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/gestures.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| void main() { |
| final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); |
| const kOpenScale = 1.15; |
| const kMinScaleFactor = 1.02; |
| |
| Widget getChild({double width = 300.0, double height = 100.0}) { |
| return Container(width: width, height: height, color: CupertinoColors.activeOrange); |
| } |
| |
| List<Widget> getActions({int number = 10}) { |
| return List<Widget>.generate( |
| number, |
| (int index) => CupertinoContextMenuAction(child: Text('Action $index')), |
| ); |
| } |
| |
| Widget getBuilder(BuildContext context, Animation<double> animation) { |
| return getChild(); |
| } |
| |
| Widget getContextMenu({ |
| Alignment alignment = Alignment.center, |
| Size screenSize = const Size(800.0, 600.0), |
| Widget? child, |
| }) { |
| return CupertinoApp( |
| home: CupertinoPageScaffold( |
| child: MediaQuery( |
| data: MediaQueryData(size: screenSize), |
| child: Align( |
| alignment: alignment, |
| child: CupertinoContextMenu( |
| actions: <CupertinoContextMenuAction>[ |
| CupertinoContextMenuAction(child: Text('CupertinoContextMenuAction $alignment')), |
| ], |
| child: child ?? getChild(), |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| Widget getBuilderContextMenu({ |
| Alignment alignment = Alignment.center, |
| Size screenSize = const Size(800.0, 600.0), |
| CupertinoContextMenuBuilder? builder, |
| }) { |
| return CupertinoApp( |
| home: CupertinoPageScaffold( |
| child: MediaQuery( |
| data: MediaQueryData(size: screenSize), |
| child: Align( |
| alignment: alignment, |
| child: CupertinoContextMenu.builder( |
| actions: <CupertinoContextMenuAction>[ |
| CupertinoContextMenuAction(child: Text('CupertinoContextMenuAction $alignment')), |
| ], |
| builder: builder ?? getBuilder, |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| // Finds the child widget that is rendered inside of _DecoyChild. |
| Finder findDecoyChild(Widget child) { |
| return find.descendant( |
| of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), |
| matching: find.byWidget(child), |
| ); |
| } |
| |
| // Finds the child widget rendered inside of _ContextMenuRouteStatic. |
| Finder findStatic() { |
| return find.descendant( |
| of: find.byType(CupertinoApp), |
| matching: find.byWidgetPredicate( |
| (Widget w) => '${w.runtimeType}' == '_ContextMenuRouteStatic', |
| ), |
| ); |
| } |
| |
| Finder findStaticChild(Widget child) { |
| return find.descendant(of: findStatic(), matching: find.byWidget(child)); |
| } |
| |
| Finder findStaticChildColor(WidgetTester tester) { |
| return find.descendant( |
| of: findStatic(), |
| matching: find.byWidgetPredicate( |
| (Widget widget) => widget is ColoredBox && widget.color != CupertinoColors.activeOrange, |
| ), |
| ); |
| } |
| |
| Finder findFittedBox() { |
| return find.descendant(of: findStatic(), matching: find.byType(FittedBox)); |
| } |
| |
| Finder findStaticDefaultPreview() { |
| return find.descendant(of: findFittedBox(), matching: find.byType(ClipRSuperellipse)); |
| } |
| |
| group('CupertinoContextMenu before and during opening', () { |
| testWidgets('An unopened CupertinoContextMenu renders child in the same place as without', ( |
| WidgetTester tester, |
| ) async { |
| // Measure the child in the scene with no CupertinoContextMenu. |
| final Widget child = getChild(); |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CupertinoPageScaffold(child: Center(child: child)), |
| ), |
| ); |
| final Rect childRect = tester.getRect(find.byWidget(child)); |
| |
| // When wrapped in a CupertinoContextMenu, the child is rendered in the same Rect. |
| await tester.pumpWidget(getContextMenu(child: child)); |
| expect(find.byWidget(child), findsOneWidget); |
| expect(tester.getRect(find.byWidget(child)), childRect); |
| }); |
| |
| testWidgets('Can open CupertinoContextMenu by tap and hold', (WidgetTester tester) async { |
| final Widget child = getChild(); |
| await tester.pumpWidget(getContextMenu(child: child)); |
| expect(find.byWidget(child), findsOneWidget); |
| final Rect childRect = tester.getRect(find.byWidget(child)); |
| expect( |
| find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), |
| findsNothing, |
| ); |
| |
| // Start a press on the child. |
| final TestGesture gesture = await tester.startGesture(childRect.center); |
| await tester.pump(); |
| |
| // The _DecoyChild is showing directly on top of the child. |
| expect(findDecoyChild(child), findsOneWidget); |
| Rect decoyChildRect = tester.getRect(findDecoyChild(child)); |
| expect(childRect, equals(decoyChildRect)); |
| |
| expect( |
| find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), |
| findsOneWidget, |
| ); |
| |
| // After a small delay, the _DecoyChild has begun to animate. |
| await tester.pump(const Duration(milliseconds: 400)); |
| decoyChildRect = tester.getRect(findDecoyChild(child)); |
| expect(childRect, isNot(equals(decoyChildRect))); |
| |
| // Eventually the decoy fully scales by _kOpenSize. |
| await tester.pump(const Duration(milliseconds: 800)); |
| decoyChildRect = tester.getRect(findDecoyChild(child)); |
| expect(childRect, isNot(equals(decoyChildRect))); |
| expect(decoyChildRect.width, childRect.width * kOpenScale); |
| |
| // Then the CupertinoContextMenu opens. |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsOneWidget); |
| }); |
| |
| testWidgets('CupertinoContextMenu is in the correct position when within a nested navigator', ( |
| WidgetTester tester, |
| ) async { |
| final Widget child = getChild(); |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CupertinoPageScaffold( |
| child: MediaQuery( |
| data: const MediaQueryData(size: Size(800, 600)), |
| child: Align( |
| alignment: Alignment.bottomRight, |
| child: SizedBox( |
| width: 700, |
| height: 500, |
| child: Navigator( |
| onGenerateRoute: (RouteSettings settings) { |
| return CupertinoPageRoute<void>( |
| builder: (BuildContext context) => Align( |
| child: CupertinoContextMenu( |
| actions: const <CupertinoContextMenuAction>[ |
| CupertinoContextMenuAction(child: Text('CupertinoContextMenuAction')), |
| ], |
| child: child, |
| ), |
| ), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| expect(find.byWidget(child), findsOneWidget); |
| final Rect childRect = tester.getRect(find.byWidget(child)); |
| expect( |
| find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), |
| findsNothing, |
| ); |
| |
| // Start a press on the child. |
| final TestGesture gesture = await tester.startGesture(childRect.center); |
| await tester.pump(); |
| |
| // The _DecoyChild is showing directly on top of the child. |
| expect(findDecoyChild(child), findsOneWidget); |
| Rect decoyChildRect = tester.getRect(findDecoyChild(child)); |
| expect(childRect, equals(decoyChildRect)); |
| |
| expect( |
| find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), |
| findsOneWidget, |
| ); |
| |
| // After a small delay, the _DecoyChild has begun to animate. |
| await tester.pump(const Duration(milliseconds: 400)); |
| decoyChildRect = tester.getRect(findDecoyChild(child)); |
| expect(childRect, isNot(equals(decoyChildRect))); |
| |
| // Eventually the decoy fully scales by _kOpenSize. |
| await tester.pump(const Duration(milliseconds: 800)); |
| decoyChildRect = tester.getRect(findDecoyChild(child)); |
| expect(childRect, isNot(equals(decoyChildRect))); |
| expect(decoyChildRect.width, childRect.width * kOpenScale); |
| |
| // Then the CupertinoContextMenu opens. |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsOneWidget); |
| }); |
| |
| testWidgets('_DecoyChild preserves the child color', (WidgetTester tester) async { |
| final Widget child = getChild(); |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CupertinoPageScaffold( |
| backgroundColor: CupertinoColors.black, |
| child: MediaQuery( |
| data: const MediaQueryData(size: Size(800, 600)), |
| child: Center( |
| child: CupertinoContextMenu( |
| actions: const <CupertinoContextMenuAction>[ |
| CupertinoContextMenuAction(child: Text('CupertinoContextMenuAction')), |
| ], |
| child: child, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Expect no _DecoyChild to be present before the gesture. |
| final Finder decoyChild = find.byWidgetPredicate( |
| (Widget w) => '${w.runtimeType}' == '_DecoyChild', |
| ); |
| expect(decoyChild, findsNothing); |
| |
| // Start press gesture on the child. |
| final Rect childRect = tester.getRect(find.byWidget(child)); |
| final TestGesture gesture = await tester.startGesture(childRect.center); |
| await tester.pump(); |
| |
| // Find the _DecoyChild by runtimeType, |
| // find the Container descendant with the BoxDecoration, |
| // then read the boxDecoration property. |
| final Finder decoyChildDescendant = find.descendant( |
| of: decoyChild, |
| matching: find.byType(Container), |
| ); |
| final boxDecoration = |
| (tester.firstWidget(decoyChildDescendant) as Container).decoration as BoxDecoration?; |
| const expectedColors = <Color?>[null, Color(0x00000000)]; |
| |
| // `Color(0x00000000)` -> Is `CupertinoColors.transparent`. |
| // `null` -> Default when no color argument is given in `BoxDecoration`. |
| // Any other color won't preserve the child's property. |
| expect(expectedColors, contains(boxDecoration?.color)); |
| |
| // End the gesture. |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // Expect no _DecoyChild to be present after ending the gesture. |
| final Finder decoyChildAfterEnding = find.byWidgetPredicate( |
| (Widget w) => '${w.runtimeType}' == '_DecoyChild', |
| ); |
| expect(decoyChildAfterEnding, findsNothing); |
| }); |
| |
| testWidgets( |
| 'CupertinoContextMenu with a basic builder opens and closes the same as when providing a child', |
| (WidgetTester tester) async { |
| final Widget child = getChild(); |
| await tester.pumpWidget( |
| getBuilderContextMenu( |
| builder: (BuildContext context, Animation<double> animation) { |
| return child; |
| }, |
| ), |
| ); |
| expect(find.byWidget(child), findsOneWidget); |
| final Rect childRect = tester.getRect(find.byWidget(child)); |
| expect( |
| find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), |
| findsNothing, |
| ); |
| |
| // Start a press on the child. |
| final TestGesture gesture = await tester.startGesture(childRect.center); |
| await tester.pump(); |
| |
| // The _DecoyChild is showing directly on top of the child. |
| expect(findDecoyChild(child), findsOneWidget); |
| Rect decoyChildRect = tester.getRect(findDecoyChild(child)); |
| expect(childRect, equals(decoyChildRect)); |
| |
| expect( |
| find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), |
| findsOneWidget, |
| ); |
| |
| // After a small delay, the _DecoyChild has begun to animate. |
| await tester.pump(const Duration(milliseconds: 400)); |
| decoyChildRect = tester.getRect(findDecoyChild(child)); |
| expect(childRect, isNot(equals(decoyChildRect))); |
| |
| // Eventually the decoy fully scales by _kOpenSize. |
| await tester.pump(const Duration(milliseconds: 800)); |
| decoyChildRect = tester.getRect(findDecoyChild(child)); |
| expect(childRect, isNot(equals(decoyChildRect))); |
| expect(decoyChildRect.width, childRect.width * kOpenScale); |
| |
| // Then the CupertinoContextMenu opens. |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsOneWidget); |
| }, |
| ); |
| |
| testWidgets('CupertinoContextMenu with a builder can change the animation', ( |
| WidgetTester tester, |
| ) async { |
| await tester.pumpWidget( |
| getBuilderContextMenu( |
| builder: (BuildContext context, Animation<double> animation) { |
| return Container( |
| width: 300.0, |
| height: 100.0, |
| decoration: BoxDecoration( |
| color: CupertinoColors.activeOrange, |
| borderRadius: BorderRadius.circular(25.0 * animation.value), |
| ), |
| ); |
| }, |
| ), |
| ); |
| |
| final Widget child = find |
| .descendant(of: find.byType(TickerMode), matching: find.byType(Container)) |
| .evaluate() |
| .single |
| .widget; |
| final Rect childRect = tester.getRect(find.byWidget(child)); |
| expect( |
| find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), |
| findsNothing, |
| ); |
| |
| // Start a press on the child. |
| final TestGesture gesture = await tester.startGesture(childRect.center); |
| await tester.pump(); |
| |
| Finder findBuilderDecoyChild() { |
| return find.descendant( |
| of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), |
| matching: find.byType(Container), |
| ); |
| } |
| |
| final decoyContainer = tester.firstElement(findBuilderDecoyChild()).widget as Container; |
| final decoyDecoration = decoyContainer.decoration as BoxDecoration?; |
| expect(decoyDecoration?.borderRadius, equals(BorderRadius.circular(0))); |
| |
| expect(findBuilderDecoyChild(), findsOneWidget); |
| |
| // After a small delay, the _DecoyChild has begun to animate with a different border radius. |
| await tester.pump(const Duration(milliseconds: 500)); |
| final decoyLaterContainer = tester.firstElement(findBuilderDecoyChild()).widget as Container; |
| final decoyLaterDecoration = decoyLaterContainer.decoration as BoxDecoration?; |
| expect(decoyLaterDecoration?.borderRadius, isNot(equals(BorderRadius.circular(0)))); |
| |
| // Finish gesture to release resources. |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| }); |
| |
| testWidgets('Hovering over Cupertino context menu updates cursor to clickable on Web', ( |
| WidgetTester tester, |
| ) async { |
| final Widget child = getChild(); |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CupertinoPageScaffold( |
| child: Center( |
| child: CupertinoContextMenu( |
| actions: const <CupertinoContextMenuAction>[ |
| CupertinoContextMenuAction(child: Text('CupertinoContextMenuAction One')), |
| ], |
| child: child, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final TestGesture gesture = await tester.createGesture( |
| kind: PointerDeviceKind.mouse, |
| pointer: 1, |
| ); |
| await gesture.addPointer(location: const Offset(10, 10)); |
| await tester.pumpAndSettle(); |
| expect( |
| RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
| SystemMouseCursors.basic, |
| ); |
| |
| final Offset contextMenu = tester.getCenter(find.byWidget(child)); |
| await gesture.moveTo(contextMenu); |
| await tester.pumpAndSettle(); |
| expect( |
| RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
| kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic, |
| ); |
| }); |
| |
| testWidgets('CupertinoContextMenu is in the correct position when within a Transform.scale', ( |
| WidgetTester tester, |
| ) async { |
| final Widget child = getChild(); |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CupertinoPageScaffold( |
| child: MediaQuery( |
| data: const MediaQueryData(size: Size(800, 600)), |
| child: Transform.scale( |
| scale: 0.5, |
| child: Align( |
| //alignment: Alignment.bottomRight, |
| child: CupertinoContextMenu( |
| actions: const <CupertinoContextMenuAction>[ |
| CupertinoContextMenuAction(child: Text('CupertinoContextMenuAction')), |
| ], |
| child: child, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| expect(find.byWidget(child), findsOneWidget); |
| final Rect childRect = tester.getRect(find.byWidget(child)); |
| expect( |
| find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), |
| findsNothing, |
| ); |
| |
| // Start a press on the child. |
| final TestGesture gesture = await tester.startGesture(childRect.center); |
| await tester.pump(); |
| |
| // The _DecoyChild is showing directly on top of the child. |
| expect(findDecoyChild(child), findsOneWidget); |
| Rect decoyChildRect = tester.getRect(findDecoyChild(child)); |
| expect(childRect, equals(decoyChildRect)); |
| |
| expect( |
| find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), |
| findsOneWidget, |
| ); |
| |
| // After a small delay, the _DecoyChild has begun to animate. |
| await tester.pump(const Duration(milliseconds: 400)); |
| decoyChildRect = tester.getRect(findDecoyChild(child)); |
| expect(childRect, isNot(equals(decoyChildRect))); |
| |
| // Eventually the decoy fully scales by _kOpenSize. |
| await tester.pump(const Duration(milliseconds: 800)); |
| decoyChildRect = tester.getRect(findDecoyChild(child)); |
| expect(childRect, isNot(equals(decoyChildRect))); |
| expect(decoyChildRect.width, childRect.width * kOpenScale); |
| |
| // Then the CupertinoContextMenu opens. |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsOneWidget); |
| }); |
| }); |
| |
| group('CupertinoContextMenu when open', () { |
| testWidgets('Last action does not have border', (WidgetTester tester) async { |
| final Widget child = getChild(); |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CupertinoPageScaffold( |
| child: Center( |
| child: CupertinoContextMenu( |
| actions: const <CupertinoContextMenuAction>[ |
| CupertinoContextMenuAction(child: Text('CupertinoContextMenuAction One')), |
| ], |
| child: child, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Open the CupertinoContextMenu |
| final TestGesture firstGesture = await tester.startGesture( |
| tester.getCenter(find.byWidget(child)), |
| ); |
| await tester.pumpAndSettle(); |
| await firstGesture.up(); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsOneWidget); |
| |
| // Both the background color and the action colors are found. |
| expect(findStaticChildColor(tester), findsNWidgets(2)); |
| |
| // Close the CupertinoContextMenu. |
| await tester.tapAt(const Offset(1.0, 1.0)); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsNothing); |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CupertinoPageScaffold( |
| child: Center( |
| child: CupertinoContextMenu( |
| actions: const <CupertinoContextMenuAction>[ |
| CupertinoContextMenuAction(child: Text('CupertinoContextMenuAction One')), |
| CupertinoContextMenuAction(child: Text('CupertinoContextMenuAction Two')), |
| ], |
| child: child, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Open the CupertinoContextMenu |
| final TestGesture secondGesture = await tester.startGesture( |
| tester.getCenter(find.byWidget(child)), |
| ); |
| await tester.pumpAndSettle(); |
| await secondGesture.up(); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsOneWidget); |
| |
| expect(findStaticChildColor(tester), findsNWidgets(3)); |
| }); |
| |
| testWidgets('Can close CupertinoContextMenu by background tap', (WidgetTester tester) async { |
| final Widget child = getChild(); |
| await tester.pumpWidget(getContextMenu(child: child)); |
| |
| // Open the CupertinoContextMenu |
| final Rect childRect = tester.getRect(find.byWidget(child)); |
| final TestGesture gesture = await tester.startGesture(childRect.center); |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsOneWidget); |
| |
| // Tap and ensure that the CupertinoContextMenu is closed. |
| await tester.tapAt(const Offset(1.0, 1.0)); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsNothing); |
| }); |
| |
| testWidgets('Can close CupertinoContextMenu by dragging down', (WidgetTester tester) async { |
| final Widget child = getChild(); |
| await tester.pumpWidget(getContextMenu(child: child)); |
| |
| // Open the CupertinoContextMenu |
| final Rect childRect = tester.getRect(find.byWidget(child)); |
| final TestGesture gesture = await tester.startGesture(childRect.center); |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsOneWidget); |
| |
| // Drag down not far enough and it bounces back and doesn't close. |
| expect(findStaticChild(child), findsOneWidget); |
| Offset staticChildCenter = tester.getCenter(findStaticChild(child)); |
| TestGesture swipeGesture = await tester.startGesture(staticChildCenter); |
| await swipeGesture.moveBy( |
| const Offset(0.0, 100.0), |
| timeStamp: const Duration(milliseconds: 100), |
| ); |
| await tester.pump(); |
| await swipeGesture.up(); |
| await tester.pump(); |
| expect(tester.getCenter(findStaticChild(child)).dy, greaterThan(staticChildCenter.dy)); |
| await tester.pumpAndSettle(); |
| expect(tester.getCenter(findStaticChild(child)), equals(staticChildCenter)); |
| expect(findStatic(), findsOneWidget); |
| |
| // Drag down far enough and it does close. |
| expect(findStaticChild(child), findsOneWidget); |
| staticChildCenter = tester.getCenter(findStaticChild(child)); |
| swipeGesture = await tester.startGesture(staticChildCenter); |
| await swipeGesture.moveBy( |
| const Offset(0.0, 200.0), |
| timeStamp: const Duration(milliseconds: 100), |
| ); |
| await tester.pump(); |
| await swipeGesture.up(); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsNothing); |
| }); |
| |
| testWidgets('Can close CupertinoContextMenu by flinging down', (WidgetTester tester) async { |
| final Widget child = getChild(); |
| await tester.pumpWidget(getContextMenu(child: child)); |
| |
| // Open the CupertinoContextMenu |
| final Rect childRect = tester.getRect(find.byWidget(child)); |
| final TestGesture gesture = await tester.startGesture(childRect.center); |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsOneWidget); |
| |
| // Fling up and nothing happens. |
| expect(findStaticChild(child), findsOneWidget); |
| await tester.fling(findStaticChild(child), const Offset(0.0, -100.0), 1000.0); |
| await tester.pumpAndSettle(); |
| expect(findStaticChild(child), findsOneWidget); |
| |
| // Fling down to close the menu. |
| expect(findStaticChild(child), findsOneWidget); |
| await tester.fling(findStaticChild(child), const Offset(0.0, 100.0), 1000.0); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsNothing); |
| }); |
| |
| testWidgets("Backdrop is added using ModalRoute's filter parameter", ( |
| WidgetTester tester, |
| ) async { |
| final Widget child = getChild(); |
| await tester.pumpWidget(getContextMenu(child: child)); |
| expect(find.byType(BackdropFilter), findsNothing); |
| |
| // Open the CupertinoContextMenu |
| final Rect childRect = tester.getRect(find.byWidget(child)); |
| final TestGesture gesture = await tester.startGesture(childRect.center); |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsOneWidget); |
| expect(find.byType(BackdropFilter), findsOneWidget); |
| }); |
| |
| testWidgets('Preview widget should have the correct border radius', ( |
| WidgetTester tester, |
| ) async { |
| final Widget child = getChild(); |
| await tester.pumpWidget(getContextMenu(child: child)); |
| |
| // Open the CupertinoContextMenu. |
| final Rect childRect = tester.getRect(find.byWidget(child)); |
| final TestGesture gesture = await tester.startGesture(childRect.center); |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsOneWidget); |
| |
| // Check border radius. |
| expect(findStaticDefaultPreview(), findsOneWidget); |
| final previewWidget = tester.firstWidget(findStaticDefaultPreview()) as ClipRSuperellipse; |
| expect(previewWidget.borderRadius, equals(BorderRadius.circular(12.0))); |
| }); |
| |
| testWidgets('CupertinoContextMenu width is correct', (WidgetTester tester) async { |
| final Widget child = getChild(); |
| await tester.pumpWidget(getContextMenu(child: child)); |
| expect(find.byWidget(child), findsOneWidget); |
| final Rect childRect = tester.getRect(find.byWidget(child)); |
| expect( |
| find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), |
| findsNothing, |
| ); |
| |
| // Start a press on the child. |
| final TestGesture gesture = await tester.startGesture(childRect.center); |
| await tester.pump(); |
| |
| // The _DecoyChild is showing directly on top of the child. |
| expect(findDecoyChild(child), findsOneWidget); |
| Rect decoyChildRect = tester.getRect(findDecoyChild(child)); |
| expect(childRect, equals(decoyChildRect)); |
| |
| expect( |
| find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), |
| findsOneWidget, |
| ); |
| |
| // After a small delay, the _DecoyChild has begun to animate. |
| await tester.pump(const Duration(milliseconds: 400)); |
| decoyChildRect = tester.getRect(findDecoyChild(child)); |
| expect(childRect, isNot(equals(decoyChildRect))); |
| |
| // Eventually the decoy fully scales by _kOpenSize. |
| await tester.pump(const Duration(milliseconds: 800)); |
| decoyChildRect = tester.getRect(findDecoyChild(child)); |
| expect(childRect, isNot(equals(decoyChildRect))); |
| expect(decoyChildRect.width, childRect.width * kOpenScale); |
| |
| // Then the CupertinoContextMenu opens. |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsOneWidget); |
| |
| // The CupertinoContextMenu has the correct width and height. |
| final CupertinoContextMenu widget = tester.widget(find.byType(CupertinoContextMenu)); |
| for (final Widget action in widget.actions) { |
| // The value of the height is 80 because of the font and icon size. |
| expect(tester.getSize(find.byWidget(action)).width, 250); |
| } |
| }); |
| |
| testWidgets('CupertinoContextMenu minimizes scaling offscreen', (WidgetTester tester) async { |
| const portraitScreenSize = Size(600.0, 800.0); |
| await binding.setSurfaceSize(portraitScreenSize); |
| addTearDown(() => binding.setSurfaceSize(null)); |
| final Widget child = getChild(); |
| |
| // Pump a CupertinoContextMenu on the top-left of the screen and open it. |
| await tester.pumpWidget(getContextMenu(alignment: Alignment.topLeft, child: child)); |
| await tester.pump(); |
| Rect childRect = tester.getRect(find.byWidget(child)); |
| // Start a press on the child. |
| final TestGesture gesture1 = await tester.startGesture(childRect.center); |
| await tester.pump(); |
| |
| // The _DecoyChild is showing directly on top of the child. |
| expect(findDecoyChild(child), findsOneWidget); |
| Rect decoyChildRect = tester.getRect(findDecoyChild(child)); |
| expect(childRect, equals(decoyChildRect)); |
| |
| expect( |
| find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), |
| findsOneWidget, |
| ); |
| |
| // After a small delay, the _DecoyChild has begun to animate. |
| await tester.pump(const Duration(milliseconds: 400)); |
| decoyChildRect = tester.getRect(findDecoyChild(child)); |
| expect(childRect, isNot(equals(decoyChildRect))); |
| |
| // Eventually the decoy fully scales. Since the context menu is fully |
| // top-left aligned, the minimum scale factor is used so that the menu |
| // animates minimally off the screen. |
| await tester.pump(const Duration(milliseconds: 900)); |
| decoyChildRect = tester.getRect(findDecoyChild(child)); |
| expect(childRect, isNot(equals(decoyChildRect))); |
| expect(decoyChildRect.width, childRect.width * kMinScaleFactor); |
| |
| // Open and then close the CupertinoContextMenu. |
| await tester.pumpAndSettle(); |
| await tester.tapAt(const Offset(599.0, 799.0)); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsNothing); |
| |
| // Pump a CupertinoContextMenu on the bottom-right of the screen and open it. |
| await tester.pumpWidget(getContextMenu(alignment: Alignment.bottomRight, child: child)); |
| await tester.pump(); |
| childRect = tester.getRect(find.byWidget(child)); |
| // Start a press on the child. |
| final TestGesture gesture2 = await tester.startGesture(childRect.center); |
| await tester.pump(); |
| |
| // The _DecoyChild is showing directly on top of the child. |
| expect(findDecoyChild(child), findsOneWidget); |
| decoyChildRect = tester.getRect(findDecoyChild(child)); |
| expect(childRect, equals(decoyChildRect)); |
| |
| expect( |
| find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), |
| findsOneWidget, |
| ); |
| |
| // After a small delay, the _DecoyChild has begun to animate. |
| await tester.pump(const Duration(milliseconds: 400)); |
| decoyChildRect = tester.getRect(findDecoyChild(child)); |
| expect(childRect, isNot(equals(decoyChildRect))); |
| |
| // Eventually the decoy fully scales. Since the context menu is fully |
| // bottom-right aligned, the minimum scale factor is used so that the menu |
| // animates minimally off the screen. |
| await tester.pump(const Duration(milliseconds: 900)); |
| decoyChildRect = tester.getRect(findDecoyChild(child)); |
| expect(childRect, isNot(equals(decoyChildRect))); |
| expect(decoyChildRect.width, childRect.width * kMinScaleFactor); |
| |
| // Open and then close the CupertinoContextMenu. |
| await tester.pumpAndSettle(); |
| await tester.tapAt(const Offset(1.0, 1.0)); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsNothing); |
| await gesture1.up(); |
| await gesture2.up(); |
| }); |
| |
| testWidgets("ContextMenu route animation doesn't throw exception on dismiss", ( |
| WidgetTester tester, |
| ) async { |
| // This is a regression test for https://github.com/flutter/flutter/issues/124597. |
| final List<int> items = List<int>.generate(2, (int index) => index).toList(); |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CupertinoPageScaffold( |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setState) { |
| return ListView( |
| children: items |
| .map( |
| (int index) => CupertinoContextMenu( |
| actions: <CupertinoContextMenuAction>[ |
| CupertinoContextMenuAction( |
| child: const Text('DELETE'), |
| onPressed: () { |
| setState(() { |
| items.remove(index); |
| Navigator.of(context).pop(); |
| }); |
| Navigator.of(context).pop(); |
| }, |
| ), |
| ], |
| child: Text('Item $index'), |
| ), |
| ) |
| .toList(), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // Open the CupertinoContextMenu. |
| final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Item 1'))); |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // Tap the delete action. |
| await tester.tap(find.text('DELETE')); |
| await tester.pumpAndSettle(); |
| |
| // The CupertinoContextMenu should be closed with no exception. |
| expect(find.text('DELETE'), findsNothing); |
| expect(tester.takeException(), null); |
| }); |
| }); |
| |
| group("Open layout differs depending on child's position on screen", () { |
| testWidgets('Portrait', (WidgetTester tester) async { |
| const portraitScreenSize = Size(600.0, 800.0); |
| await binding.setSurfaceSize(portraitScreenSize); |
| addTearDown(() => binding.setSurfaceSize(null)); |
| |
| // Pump a CupertinoContextMenu in the center of the screen and open it. |
| final Widget child = getChild(); |
| await tester.pumpWidget(getContextMenu(screenSize: portraitScreenSize, child: child)); |
| expect(find.byType(CupertinoContextMenuAction), findsNothing); |
| Rect childRect = tester.getRect(find.byWidget(child)); |
| TestGesture gesture = await tester.startGesture(childRect.center); |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // The position of the action is in the center of the screen. |
| expect(find.byType(CupertinoContextMenuAction), findsOneWidget); |
| final Offset center = tester.getTopLeft(find.byType(CupertinoContextMenuAction)); |
| |
| // Close the CupertinoContextMenu. |
| await tester.tapAt(const Offset(1.0, 1.0)); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsNothing); |
| |
| // Pump a CupertinoContextMenu on the left of the screen and open it. |
| await tester.pumpWidget( |
| getContextMenu( |
| alignment: Alignment.centerLeft, |
| screenSize: portraitScreenSize, |
| child: child, |
| ), |
| ); |
| expect(find.byType(CupertinoContextMenuAction), findsNothing); |
| await tester.pumpAndSettle(); |
| childRect = tester.getRect(find.byWidget(child)); |
| gesture = await tester.startGesture(childRect.center); |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // The position of the action is on the left of the screen. |
| expect(find.byType(CupertinoContextMenuAction), findsOneWidget); |
| final Offset left = tester.getTopLeft(find.byType(CupertinoContextMenuAction)); |
| expect(left.dx, lessThan(center.dx)); |
| |
| // Close the CupertinoContextMenu. |
| await tester.tapAt(const Offset(559.0, 799.0)); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsNothing); |
| |
| // Pump a CupertinoContextMenu on the right of the screen and open it. |
| await tester.pumpWidget( |
| getContextMenu( |
| alignment: Alignment.centerRight, |
| screenSize: portraitScreenSize, |
| child: child, |
| ), |
| ); |
| expect(find.byType(CupertinoContextMenuAction), findsNothing); |
| childRect = tester.getRect(find.byWidget(child)); |
| gesture = await tester.startGesture(childRect.center); |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // The position of the action is on the right of the screen. |
| expect(find.byType(CupertinoContextMenuAction), findsOneWidget); |
| final Offset right = tester.getTopLeft(find.byType(CupertinoContextMenuAction)); |
| expect(right.dx, greaterThan(center.dx)); |
| }); |
| |
| testWidgets('Landscape', (WidgetTester tester) async { |
| // Pump a CupertinoContextMenu in the center of the screen and open it. |
| final Widget child = getChild(); |
| await tester.pumpWidget(getContextMenu(child: child)); |
| expect(find.byType(CupertinoContextMenuAction), findsNothing); |
| Rect childRect = tester.getRect(find.byWidget(child)); |
| TestGesture gesture = await tester.startGesture(childRect.center); |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // Landscape doesn't support a centered action list, so the action is on |
| // the left side of the screen. |
| expect(find.byType(CupertinoContextMenuAction), findsOneWidget); |
| final Offset center = tester.getTopLeft(find.byType(CupertinoContextMenuAction)); |
| |
| // Close the CupertinoContextMenu. |
| await tester.tapAt(const Offset(1.0, 1.0)); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsNothing); |
| |
| // Pump a CupertinoContextMenu on the left of the screen and open it. |
| await tester.pumpWidget(getContextMenu(alignment: Alignment.centerLeft, child: child)); |
| expect(find.byType(CupertinoContextMenuAction), findsNothing); |
| childRect = tester.getRect(find.byWidget(child)); |
| gesture = await tester.startGesture(childRect.center); |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // The position of the action is on the right of the screen, which is the |
| // same as for center aligned children in landscape. |
| expect(find.byType(CupertinoContextMenuAction), findsOneWidget); |
| final Offset left = tester.getTopLeft(find.byType(CupertinoContextMenuAction)); |
| expect(left.dx, equals(center.dx)); |
| |
| // Close the CupertinoContextMenu. |
| await tester.tapAt(const Offset(1.0, 1.0)); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsNothing); |
| |
| // Pump a CupertinoContextMenu on the right of the screen and open it. |
| await tester.pumpWidget(getContextMenu(alignment: Alignment.centerRight, child: child)); |
| expect(find.byType(CupertinoContextMenuAction), findsNothing); |
| childRect = tester.getRect(find.byWidget(child)); |
| gesture = await tester.startGesture(childRect.center); |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // The position of the action is on the left of the screen. |
| expect(find.byType(CupertinoContextMenuAction), findsOneWidget); |
| final Offset right = tester.getTopLeft(find.byType(CupertinoContextMenuAction)); |
| expect(right.dx, lessThan(left.dx)); |
| }); |
| }); |
| |
| testWidgets('Conflicting gesture detectors', (WidgetTester tester) async { |
| int? onPointerDownTime; |
| int? onPointerUpTime; |
| var insideTapTriggered = false; |
| // The required duration of the route to be pushed in is [500, 900]ms. |
| // 500ms is calculated from kPressTimeout+_previewLongPressTimeout/2. |
| // 900ms is calculated from kPressTimeout+_previewLongPressTimeout. |
| const pressDuration = Duration(milliseconds: 501); |
| |
| int now() => clock.now().millisecondsSinceEpoch; |
| |
| await tester.pumpWidget( |
| Listener( |
| onPointerDown: (PointerDownEvent event) => onPointerDownTime = now(), |
| onPointerUp: (PointerUpEvent event) => onPointerUpTime = now(), |
| child: CupertinoApp( |
| home: Align( |
| child: CupertinoContextMenu( |
| actions: const <CupertinoContextMenuAction>[ |
| CupertinoContextMenuAction(child: Text('CupertinoContextMenuAction')), |
| ], |
| child: GestureDetector( |
| onTap: () => insideTapTriggered = true, |
| child: Container( |
| width: 200, |
| height: 200, |
| key: const Key('container'), |
| color: const Color(0xFF00FF00), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Start a press on the child. |
| final TestGesture gesture = await tester.createGesture(); |
| await gesture.down(tester.getCenter(find.byKey(const Key('container')))); |
| // Simulate the actual situation: |
| // the user keeps pressing and requesting frames. |
| // If there is only one frame, |
| // the animation is mutant and cannot drive the value of the animation controller. |
| for (var i = 0; i < 100; i++) { |
| await tester.pump(pressDuration ~/ 100); |
| } |
| await gesture.up(); |
| // Await pushing route. |
| await tester.pumpAndSettle(); |
| |
| // Judge whether _ContextMenuRouteStatic present on the screen. |
| final Finder routeStatic = find.byWidgetPredicate( |
| (Widget w) => '${w.runtimeType}' == '_ContextMenuRouteStatic', |
| ); |
| |
| // The insideTap and the route should not be triggered at the same time. |
| if (insideTapTriggered) { |
| // Calculate the actual duration. |
| final int actualDuration = onPointerUpTime! - onPointerDownTime!; |
| |
| expect( |
| routeStatic, |
| findsNothing, |
| reason: |
| 'When actualDuration($actualDuration) is in the range of 500ms~900ms, ' |
| 'which means the route is pushed, ' |
| 'but insideTap should not be triggered at the same time.', |
| ); |
| } else { |
| // The route should be pushed when the insideTap is not triggered. |
| expect(routeStatic, findsOneWidget); |
| } |
| }); |
| |
| testWidgets('CupertinoContextMenu scrolls correctly', (WidgetTester tester) async { |
| const numMenuItems = 100; |
| final Widget child = getChild(); |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CupertinoPageScaffold( |
| child: MediaQuery( |
| data: const MediaQueryData(size: Size(100, 100)), |
| child: CupertinoContextMenu( |
| actions: List<CupertinoContextMenuAction>.generate(numMenuItems, (int index) { |
| return CupertinoContextMenuAction(child: Text('Item $index'), onPressed: () {}); |
| }), |
| child: child, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Open the CupertinoContextMenu. |
| final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byWidget(child))); |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(find.byType(CupertinoContextMenu), findsOneWidget); |
| |
| // Verify the first items are visible. |
| expect(find.text('Item 0'), findsOneWidget); |
| expect(find.text('Item 1'), findsOneWidget); |
| |
| // Find the scrollable part of the context menu. |
| final Finder scrollableFinder = find.byType(Scrollable); |
| expect(scrollableFinder, findsOneWidget); |
| |
| // Verify a scrollbar is displayed. |
| expect(find.byType(CupertinoScrollbar), findsOneWidget); |
| |
| // Scroll to the bottom. |
| await tester.drag(scrollableFinder, const Offset(0, -500)); |
| await tester.pumpAndSettle(); |
| |
| // Verify the last item is visible. |
| expect(find.text('Item ${numMenuItems - 1}'), findsOneWidget); |
| |
| // Scroll back to the top. |
| await tester.drag(scrollableFinder, const Offset(0, 500)); |
| await tester.pumpAndSettle(); |
| |
| // Verify the first items are still visible. |
| expect(find.text('Item 0'), findsOneWidget); |
| expect(find.text('Item 1'), findsOneWidget); |
| }); |
| |
| testWidgets('Pushing a new route removes overlay', (WidgetTester tester) async { |
| final Widget child = getChild(); |
| const page = 'Page 2'; |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Builder( |
| builder: (BuildContext context) { |
| return Center( |
| child: CupertinoContextMenu( |
| actions: const <Widget>[CupertinoContextMenuAction(child: Text('Test'))], |
| child: GestureDetector( |
| onTap: () { |
| Navigator.of(context).push( |
| CupertinoPageRoute<Widget>( |
| builder: (BuildContext context) => |
| const CupertinoPageScaffold(child: Text(page)), |
| ), |
| ); |
| }, |
| child: child, |
| ), |
| ), |
| ); |
| }, |
| ), |
| ), |
| ); |
| |
| expect(find.byWidget(child), findsOneWidget); |
| final Rect childRect = tester.getRect(find.byWidget(child)); |
| expect(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), findsNothing); |
| |
| // Start a press on the child. |
| final TestGesture gesture = await tester.startGesture(childRect.center); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 300)); |
| expect(find.text(page), findsNothing); |
| |
| await tester.pump(const Duration(milliseconds: 300)); |
| await gesture.up(); |
| |
| // Kickstart the route transition. |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 300)); |
| |
| // As the transition starts, the overlay has been removed. |
| // Only the child transitioning out is shown. |
| expect(find.text(page), findsOneWidget); |
| expect(find.byWidget(child), findsOneWidget); |
| }); |
| |
| testWidgets('Removing context menu from widget tree removes overlay', ( |
| WidgetTester tester, |
| ) async { |
| final Widget child = getChild(); |
| var ctxMenuRemoved = false; |
| late StateSetter setState; |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: StatefulBuilder( |
| builder: (BuildContext context, StateSetter stateSetter) { |
| setState = stateSetter; |
| return Center( |
| child: ctxMenuRemoved |
| ? const SizedBox() |
| : CupertinoContextMenu( |
| actions: <Widget>[ |
| CupertinoContextMenuAction(child: const Text('Test'), onPressed: () {}), |
| ], |
| child: child, |
| ), |
| ); |
| }, |
| ), |
| ), |
| ); |
| |
| expect(find.byWidget(child), findsOneWidget); |
| final Rect childRect = tester.getRect(find.byWidget(child)); |
| |
| // Start a press on the child. |
| final TestGesture gesture = await tester.startGesture(childRect.center); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| setState(() { |
| ctxMenuRemoved = true; |
| }); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(find.byWidget(child), findsNothing); |
| }); |
| |
| testWidgets('CupertinoContextMenu goldens in portrait orientation', (WidgetTester tester) async { |
| const portraitScreenSize = Size(800.0, 900.0); |
| await binding.setSurfaceSize(portraitScreenSize); |
| addTearDown(() => binding.setSurfaceSize(null)); |
| |
| final Widget leftChild = getChild(width: 200, height: 300); |
| final Widget rightChild = getChild(width: 200, height: 300); |
| final Widget centerChild = getChild(width: 200, height: 300); |
| final children = <Widget>[leftChild, centerChild, rightChild]; |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: GridView.count( |
| crossAxisCount: 3, |
| children: children.map((Widget child) { |
| return CupertinoContextMenu(actions: getActions(), child: child); |
| }).toList(), |
| ), |
| ), |
| ); |
| |
| Future<void> expectGolden(String name, Widget child) async { |
| // Open the child's CupertinoContextMenu. |
| final Rect childRect = tester.getRect(find.byWidget(child)); |
| final TestGesture gesture = await tester.startGesture(childRect.center); |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsOneWidget); |
| |
| await expectLater(findStatic(), matchesGoldenFile('context_menu.portrait.$name.png')); |
| |
| // Tap and ensure that the CupertinoContextMenu is closed. |
| await tester.tapAt(const Offset(1.0, 1.0)); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsNothing); |
| } |
| |
| await expectGolden('left', leftChild); |
| await expectGolden('center', centerChild); |
| await expectGolden('right', rightChild); |
| }); |
| |
| testWidgets('CupertinoContextMenu goldens in landscape orientation', (WidgetTester tester) async { |
| const landscapeScreenSize = Size(800.0, 600.0); |
| await binding.setSurfaceSize(landscapeScreenSize); |
| addTearDown(() => binding.setSurfaceSize(null)); |
| |
| final Widget leftChild = getChild(width: 200, height: 300); |
| final Widget rightChild = getChild(width: 200, height: 300); |
| final Widget centerChild = getChild(width: 200, height: 300); |
| final children = <Widget>[leftChild, centerChild, rightChild]; |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: GridView.count( |
| crossAxisCount: 3, |
| children: children.map((Widget child) { |
| return CupertinoContextMenu(actions: getActions(), child: child); |
| }).toList(), |
| ), |
| ), |
| ); |
| |
| Future<void> expectGolden(String name, Widget child) async { |
| // Open the child's CupertinoContextMenu. |
| final Rect childRect = tester.getRect(find.byWidget(child)); |
| final TestGesture gesture = await tester.startGesture(childRect.center); |
| await tester.pumpAndSettle(); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsOneWidget); |
| |
| await expectLater(findStatic(), matchesGoldenFile('context_menu.landscape.$name.png')); |
| |
| // Tap and ensure that the CupertinoContextMenu is closed. |
| await tester.tapAt(const Offset(1.0, 1.0)); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsNothing); |
| } |
| |
| await expectGolden('left', leftChild); |
| await expectGolden('center', centerChild); |
| await expectGolden('right', rightChild); |
| }); |
| |
| group('CupertinoContextMenu sheet shrink animation alignment - ', () { |
| Future<void> testShrinkAlignment({ |
| required WidgetTester tester, |
| required Alignment alignment, |
| required Size screenSize, |
| required AlignmentDirectional expectedAlignment, |
| }) async { |
| final Widget child = getChild(); |
| await tester.pumpWidget( |
| getContextMenu(alignment: alignment, screenSize: screenSize, child: child), |
| ); |
| |
| // Open the CupertinoContextMenu. |
| final Rect childRect = tester.getRect(find.byWidget(child)); |
| final TestGesture openGesture = await tester.startGesture(childRect.center); |
| await tester.pumpAndSettle(); |
| await openGesture.up(); |
| await tester.pumpAndSettle(); |
| expect(findStatic(), findsOneWidget); |
| |
| final Finder sheetFinder = find.byWidgetPredicate( |
| (Widget widget) => widget.runtimeType.toString() == '_ContextMenuSheet', |
| ); |
| expect(sheetFinder, findsOneWidget); |
| final Rect initialSheetRect = tester.getRect(sheetFinder); |
| final Finder staticChildFinder = findStaticChild(child); |
| expect(staticChildFinder, findsOneWidget); |
| await tester.pump(); |
| |
| // Drag down enough to trigger the shrink animation. |
| await tester.fling(staticChildFinder, Offset(0.0, childRect.height / 2), 1000.0); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 50)); |
| |
| // The sheet has shrunk. |
| expect(sheetFinder, findsOneWidget); |
| final Rect shrunkSheetRect = tester.getRect(sheetFinder); |
| expect(shrunkSheetRect.width, lessThan(initialSheetRect.width)); |
| expect(shrunkSheetRect.height, lessThan(initialSheetRect.height)); |
| |
| // Verify alignment based on how the rect has shrunk. |
| switch (expectedAlignment) { |
| case AlignmentDirectional.topStart: |
| expect( |
| shrunkSheetRect.left, |
| moreOrLessEquals(initialSheetRect.left, epsilon: Tolerance.defaultTolerance.distance), |
| ); |
| case AlignmentDirectional.topCenter: |
| expect( |
| shrunkSheetRect.center.dx, |
| moreOrLessEquals( |
| initialSheetRect.center.dx, |
| epsilon: Tolerance.defaultTolerance.distance, |
| ), |
| ); |
| case AlignmentDirectional.topEnd: |
| expect( |
| shrunkSheetRect.right, |
| moreOrLessEquals(initialSheetRect.right, epsilon: Tolerance.defaultTolerance.distance), |
| ); |
| default: |
| fail('Unhandled alignment: $expectedAlignment'); |
| } |
| await tester.pumpAndSettle(); |
| } |
| |
| testWidgets('Portrait', (WidgetTester tester) async { |
| const portraitScreenSize = Size(600.0, 800.0); |
| await binding.setSurfaceSize(portraitScreenSize); |
| addTearDown(() => binding.setSurfaceSize(null)); |
| |
| await testShrinkAlignment( |
| tester: tester, |
| alignment: Alignment.centerLeft, |
| screenSize: portraitScreenSize, |
| expectedAlignment: AlignmentDirectional.topStart, |
| ); |
| await testShrinkAlignment( |
| tester: tester, |
| alignment: Alignment.center, |
| screenSize: portraitScreenSize, |
| expectedAlignment: AlignmentDirectional.topCenter, |
| ); |
| await testShrinkAlignment( |
| tester: tester, |
| alignment: Alignment.centerRight, |
| screenSize: portraitScreenSize, |
| expectedAlignment: AlignmentDirectional.topEnd, |
| ); |
| }); |
| |
| testWidgets('Landscape', (WidgetTester tester) async { |
| const landscapeScreenSize = Size(800.0, 600.0); |
| await binding.setSurfaceSize(landscapeScreenSize); |
| addTearDown(() => binding.setSurfaceSize(null)); |
| |
| await testShrinkAlignment( |
| tester: tester, |
| alignment: Alignment.centerLeft, |
| screenSize: landscapeScreenSize, |
| expectedAlignment: AlignmentDirectional.topStart, |
| ); |
| await testShrinkAlignment( |
| tester: tester, |
| alignment: Alignment.center, |
| screenSize: landscapeScreenSize, |
| expectedAlignment: AlignmentDirectional.topStart, |
| ); |
| await testShrinkAlignment( |
| tester: tester, |
| alignment: Alignment.centerRight, |
| screenSize: landscapeScreenSize, |
| expectedAlignment: AlignmentDirectional.topEnd, |
| ); |
| }); |
| }); |
| |
| testWidgets('CupertinoContextMenu respects available screen width - Portrait', ( |
| WidgetTester tester, |
| ) async { |
| const portraitScreenSize = Size(300.0, 350.0); |
| await binding.setSurfaceSize(portraitScreenSize); |
| addTearDown(() => binding.setSurfaceSize(null)); |
| |
| final Widget child = getChild(); |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(size: portraitScreenSize), |
| child: CupertinoApp( |
| home: Center( |
| child: CupertinoContextMenu( |
| actions: <Widget>[ |
| CupertinoContextMenuAction(child: const Text('Test'), onPressed: () {}), |
| ], |
| child: child, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.byWidget(child), findsOneWidget); |
| final Rect childRect = tester.getRect(find.byWidget(child)); |
| |
| // Start a press on the child. |
| final TestGesture gesture = await tester.startGesture(childRect.center); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 500)); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(tester.takeException(), null); |
| |
| // Verify the child width is constrained correctly. |
| expect(findStatic(), findsOneWidget); |
| final Size fittedBoxSize = tester.getSize(findFittedBox()); |
| // availableWidth = 300.0 (screen width) - 2 * 20.0 (padding) = 260.0 |
| expect(fittedBoxSize.width, 260.0); |
| }); |
| |
| testWidgets('CupertinoContextMenu respects available screen width - Landscape', ( |
| WidgetTester tester, |
| ) async { |
| const landscapeScreenSize = Size(350.0, 300.0); |
| await binding.setSurfaceSize(landscapeScreenSize); |
| addTearDown(() => binding.setSurfaceSize(null)); |
| |
| final Widget child = getChild(width: 500); |
| await tester.pumpWidget( |
| MediaQuery( |
| data: const MediaQueryData(size: landscapeScreenSize), |
| child: CupertinoApp( |
| home: Center( |
| child: CupertinoContextMenu( |
| actions: <Widget>[ |
| CupertinoContextMenuAction(child: const Text('Test'), onPressed: () {}), |
| ], |
| child: child, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.byWidget(child), findsOneWidget); |
| final Rect childRect = tester.getRect(find.byWidget(child)); |
| |
| // Start a press on the child. |
| final TestGesture gesture = await tester.startGesture(childRect.center); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 500)); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| expect(tester.takeException(), null); |
| |
| // Verify the child width is constrained correctly. |
| expect(findStatic(), findsOneWidget); |
| final Size fittedBoxSize = tester.getSize(findFittedBox()); |
| // availableWidth = 350.0 (screen width) - 2 * 20.0 (padding) = 310.0 |
| // availableWidthForChild = 310.0 - 250.0 (menu width) = 60.0 |
| expect(fittedBoxSize.width, 60.0); |
| }); |
| |
| testWidgets('CupertinoContextMenu does not crash at zero area', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox.shrink( |
| child: CupertinoContextMenu( |
| actions: const <Widget>[Text('X'), Text('Y')], |
| child: const Text('Y'), |
| ), |
| ), |
| ), |
| ), |
| ); |
| expect(tester.getSize(find.byType(CupertinoContextMenu)), Size.zero); |
| }); |
| } |