flutter_flutter/packages/flutter/test/cupertino/context_menu_test.dart
Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

1557 lines
57 KiB
Dart

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// 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);
});
}