// 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(['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 getActions({int number = 10}) { return List.generate( number, (int index) => CupertinoContextMenuAction(child: Text('Action $index')), ); } Widget getBuilder(BuildContext context, Animation 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(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(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( builder: (BuildContext context) => Align( child: CupertinoContextMenu( actions: const [ 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(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 = [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 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 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(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(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(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(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 items = List.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( 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(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.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 [CupertinoContextMenuAction(child: Text('Test'))], child: GestureDetector( onTap: () { Navigator.of(context).push( CupertinoPageRoute( 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: [ 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 = [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 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 = [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 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 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: [ 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: [ 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 [Text('X'), Text('Y')], child: const Text('Y'), ), ), ), ), ); expect(tester.getSize(find.byType(CupertinoContextMenu)), Size.zero); }); }