// 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 double kOpenScale = 1.15;
  const double 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? boxDecoration =
          (tester.firstWidget(decoyChildDescendant) as Container).decoration as BoxDecoration?;
      const List<Color?> 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 Container decoyContainer =
          tester.firstElement(findBuilderDecoyChild()).widget as Container;
      final BoxDecoration? 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 Container decoyLaterContainer =
          tester.firstElement(findBuilderDecoyChild()).widget as Container;
      final BoxDecoration? 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 ClipRSuperellipse 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 Size 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 Size 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;
    bool 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 Duration 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 (int 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 int 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 String 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();
    bool 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 Size 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 List<Widget> 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 Size 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 List<Widget> 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 Size 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 Size 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,
      );
    });
  });
}
