| // 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. |
| |
| @Tags(<String>['reduced-test-set']) |
| |
| import 'package:flutter/cupertino.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| void main() { |
| final MagnifierController magnifierController = MagnifierController(); |
| const Rect reasonableTextField = Rect.fromLTRB(50, 100, 200, 100); |
| final Offset basicOffset = Offset(Magnifier.kDefaultMagnifierSize.width / 2, |
| Magnifier.kStandardVerticalFocalPointShift + Magnifier.kDefaultMagnifierSize.height); |
| |
| Offset getMagnifierPosition(WidgetTester tester, [bool animated = false]) { |
| if (animated) { |
| final AnimatedPositioned animatedPositioned = |
| tester.firstWidget(find.byType(AnimatedPositioned)); |
| return Offset(animatedPositioned.left ?? 0, animatedPositioned.top ?? 0); |
| } else { |
| final Positioned positioned = tester.firstWidget(find.byType(Positioned)); |
| return Offset(positioned.left ?? 0, positioned.top ?? 0); |
| } |
| } |
| |
| Future<void> showMagnifier( |
| BuildContext context, |
| WidgetTester tester, |
| ValueNotifier<MagnifierInfo> magnifierInfo, |
| ) async { |
| final Future<void> magnifierShown = magnifierController.show( |
| context: context, |
| builder: (_) => TextMagnifier( |
| magnifierInfo: magnifierInfo, |
| )); |
| |
| WidgetsBinding.instance.scheduleFrame(); |
| await tester.pumpAndSettle(); |
| |
| // Verify that the magnifier is shown. |
| await magnifierShown; |
| } |
| |
| tearDown(() { |
| magnifierController.removeFromOverlay(); |
| magnifierController.animationController = null; |
| }); |
| |
| group('adaptiveMagnifierControllerBuilder', () { |
| testWidgets('should return a TextEditingMagnifier on Android', |
| (WidgetTester tester) async { |
| await tester.pumpWidget(const MaterialApp( |
| home: Placeholder(), |
| )); |
| |
| final BuildContext context = tester.firstElement(find.byType(Placeholder)); |
| |
| final Widget? builtWidget = TextMagnifier.adaptiveMagnifierConfiguration.magnifierBuilder( |
| context, |
| MagnifierController(), |
| ValueNotifier<MagnifierInfo>(MagnifierInfo.empty), |
| ); |
| |
| expect(builtWidget, isA<TextMagnifier>()); |
| }, variant: TargetPlatformVariant.only(TargetPlatform.android)); |
| |
| testWidgets('should return a CupertinoMagnifier on iOS', |
| (WidgetTester tester) async { |
| await tester.pumpWidget(const MaterialApp( |
| home: Placeholder(), |
| )); |
| |
| final BuildContext context = tester.firstElement(find.byType(Placeholder)); |
| |
| final Widget? builtWidget = TextMagnifier.adaptiveMagnifierConfiguration.magnifierBuilder( |
| context, |
| MagnifierController(), |
| ValueNotifier<MagnifierInfo>(MagnifierInfo.empty), |
| ); |
| |
| expect(builtWidget, isA<CupertinoTextMagnifier>()); |
| }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); |
| |
| testWidgets('should return null on all platforms not Android, iOS', |
| (WidgetTester tester) async { |
| await tester.pumpWidget(const MaterialApp( |
| home: Placeholder(), |
| )); |
| |
| final BuildContext context = tester.firstElement(find.byType(Placeholder)); |
| |
| final Widget? builtWidget = TextMagnifier.adaptiveMagnifierConfiguration.magnifierBuilder( |
| context, |
| MagnifierController(), |
| ValueNotifier<MagnifierInfo>(MagnifierInfo.empty), |
| ); |
| |
| expect(builtWidget, isNull); |
| }, |
| variant: TargetPlatformVariant.all( |
| excluding: <TargetPlatform>{ |
| TargetPlatform.iOS, |
| TargetPlatform.android |
| }), |
| ); |
| }); |
| |
| group('magnifier', () { |
| group('position', () { |
| testWidgets( |
| 'should be at gesture position if does not violate any positioning rules', |
| (WidgetTester tester) async { |
| final Key textField = UniqueKey(); |
| |
| await tester.pumpWidget(const MaterialApp( |
| home: Placeholder(), |
| )); |
| |
| await tester.pumpWidget( |
| Container( |
| color: const Color.fromARGB(255, 0, 255, 179), |
| child: MaterialApp( |
| home: Center( |
| child: Container( |
| key: textField, |
| width: 10, |
| height: 10, |
| color: Colors.red, |
| child: const Placeholder(), |
| )), |
| ), |
| ), |
| ); |
| |
| final BuildContext context = |
| tester.firstElement(find.byType(Placeholder)); |
| |
| // Magnifier should be positioned directly over the red square. |
| final RenderBox tapPointRenderBox = |
| tester.firstRenderObject(find.byKey(textField)) as RenderBox; |
| final Rect fakeTextFieldRect = |
| tapPointRenderBox.localToGlobal(Offset.zero) & |
| tapPointRenderBox.size; |
| |
| final ValueNotifier<MagnifierInfo> magnifierInfo = |
| ValueNotifier<MagnifierInfo>( |
| MagnifierInfo( |
| currentLineBoundaries: fakeTextFieldRect, |
| fieldBounds: fakeTextFieldRect, |
| caretRect: fakeTextFieldRect, |
| // The tap position is dragBelow units below the text field. |
| globalGesturePosition: fakeTextFieldRect.center, |
| )); |
| |
| await showMagnifier(context, tester, magnifierInfo); |
| |
| // Should show two red squares; original, and one in the magnifier, |
| // directly ontop of one another. |
| await expectLater( |
| find.byType(MaterialApp), |
| matchesGoldenFile('magnifier.position.default.png'), |
| ); |
| }); |
| |
| testWidgets( |
| 'should never move outside the right bounds of the editing line', |
| (WidgetTester tester) async { |
| const double gestureOutsideLine = 100; |
| |
| await tester.pumpWidget(const MaterialApp( |
| home: Placeholder(), |
| )); |
| |
| final BuildContext context = |
| tester.firstElement(find.byType(Placeholder)); |
| |
| await showMagnifier( |
| context, |
| tester, |
| ValueNotifier<MagnifierInfo>( |
| MagnifierInfo( |
| currentLineBoundaries: reasonableTextField, |
| // Inflate these two to make sure we're bounding on the |
| // current line boundaries, not anything else. |
| fieldBounds: reasonableTextField.inflate(gestureOutsideLine), |
| caretRect: reasonableTextField.inflate(gestureOutsideLine), |
| // The tap position is far out of the right side of the app. |
| globalGesturePosition: Offset(reasonableTextField.right + gestureOutsideLine, 0), |
| ), |
| ), |
| ); |
| |
| // Should be less than the right edge, since we have padding. |
| expect(getMagnifierPosition(tester).dx, |
| lessThanOrEqualTo(reasonableTextField.right)); |
| }); |
| |
| testWidgets( |
| 'should never move outside the left bounds of the editing line', |
| (WidgetTester tester) async { |
| const double gestureOutsideLine = 100; |
| |
| await tester.pumpWidget(const MaterialApp( |
| home: Placeholder(), |
| )); |
| |
| final BuildContext context = |
| tester.firstElement(find.byType(Placeholder)); |
| |
| await showMagnifier( |
| context, |
| tester, |
| ValueNotifier<MagnifierInfo>( |
| MagnifierInfo( |
| currentLineBoundaries: reasonableTextField, |
| // Inflate these two to make sure we're bounding on the |
| // current line boundaries, not anything else. |
| fieldBounds: reasonableTextField.inflate(gestureOutsideLine), |
| caretRect: reasonableTextField.inflate(gestureOutsideLine), |
| // The tap position is far out of the left side of the app. |
| globalGesturePosition: Offset(reasonableTextField.left - gestureOutsideLine, 0), |
| ), |
| ), |
| ); |
| |
| expect(getMagnifierPosition(tester).dx + basicOffset.dx, |
| greaterThanOrEqualTo(reasonableTextField.left)); |
| }); |
| |
| testWidgets('should position vertically at the center of the line', (WidgetTester tester) async { |
| await tester.pumpWidget(const MaterialApp( |
| home: Placeholder(), |
| )); |
| |
| final BuildContext context = |
| tester.firstElement(find.byType(Placeholder)); |
| |
| await showMagnifier( |
| context, |
| tester, |
| ValueNotifier<MagnifierInfo>( |
| MagnifierInfo( |
| currentLineBoundaries: reasonableTextField, |
| fieldBounds: reasonableTextField, |
| caretRect: reasonableTextField, |
| globalGesturePosition: reasonableTextField.center, |
| ))); |
| |
| expect(getMagnifierPosition(tester).dy, |
| reasonableTextField.center.dy - basicOffset.dy); |
| }); |
| |
| testWidgets('should reposition vertically if mashed against the ceiling', |
| (WidgetTester tester) async { |
| final Rect topOfScreenTextFieldRect = |
| Rect.fromPoints(Offset.zero, const Offset(200, 0)); |
| |
| await tester.pumpWidget(const MaterialApp( |
| home: Placeholder(), |
| )); |
| |
| final BuildContext context = |
| tester.firstElement(find.byType(Placeholder)); |
| |
| await showMagnifier( |
| context, |
| tester, |
| ValueNotifier<MagnifierInfo>( |
| MagnifierInfo( |
| currentLineBoundaries: topOfScreenTextFieldRect, |
| fieldBounds: topOfScreenTextFieldRect, |
| caretRect: topOfScreenTextFieldRect, |
| globalGesturePosition: topOfScreenTextFieldRect.topCenter, |
| ), |
| ), |
| ); |
| |
| expect(getMagnifierPosition(tester).dy, greaterThanOrEqualTo(0)); |
| }); |
| }); |
| |
| group('focal point', () { |
| Offset getMagnifierAdditionalFocalPoint(WidgetTester tester) { |
| final Magnifier magnifier = tester.firstWidget(find.byType(Magnifier)); |
| return magnifier.additionalFocalPointOffset; |
| } |
| |
| testWidgets( |
| 'should shift focal point so that the lens sees nothing out of bounds', |
| (WidgetTester tester) async { |
| await tester.pumpWidget(const MaterialApp( |
| home: Placeholder(), |
| )); |
| |
| final BuildContext context = |
| tester.firstElement(find.byType(Placeholder)); |
| |
| await showMagnifier( |
| context, |
| tester, |
| ValueNotifier<MagnifierInfo>( |
| MagnifierInfo( |
| currentLineBoundaries: reasonableTextField, |
| fieldBounds: reasonableTextField, |
| caretRect: reasonableTextField, |
| // Gesture on the far right of the magnifier. |
| globalGesturePosition: reasonableTextField.topLeft, |
| ), |
| ), |
| ); |
| |
| expect(getMagnifierAdditionalFocalPoint(tester).dx, |
| lessThan(reasonableTextField.left)); |
| }); |
| |
| testWidgets( |
| 'focal point should shift if mashed against the top to always point to text', |
| (WidgetTester tester) async { |
| final Rect topOfScreenTextFieldRect = |
| Rect.fromPoints(Offset.zero, const Offset(200, 0)); |
| |
| await tester.pumpWidget(const MaterialApp( |
| home: Placeholder(), |
| )); |
| |
| final BuildContext context = |
| tester.firstElement(find.byType(Placeholder)); |
| |
| await showMagnifier( |
| context, |
| tester, |
| ValueNotifier<MagnifierInfo>( |
| MagnifierInfo( |
| currentLineBoundaries: topOfScreenTextFieldRect, |
| fieldBounds: topOfScreenTextFieldRect, |
| caretRect: topOfScreenTextFieldRect, |
| globalGesturePosition: topOfScreenTextFieldRect.topCenter, |
| ), |
| ), |
| ); |
| |
| expect(getMagnifierAdditionalFocalPoint(tester).dy, lessThan(0)); |
| }); |
| }); |
| |
| group('animation state', () { |
| bool getIsAnimated(WidgetTester tester) { |
| final AnimatedPositioned animatedPositioned = |
| tester.firstWidget(find.byType(AnimatedPositioned)); |
| return animatedPositioned.duration.compareTo(Duration.zero) != 0; |
| } |
| |
| testWidgets('should not be animated on the inital state', |
| (WidgetTester tester) async { |
| await tester.pumpWidget(const MaterialApp( |
| home: Placeholder(), |
| )); |
| |
| final BuildContext context = |
| tester.firstElement(find.byType(Placeholder)); |
| |
| await showMagnifier( |
| context, |
| tester, |
| ValueNotifier<MagnifierInfo>( |
| MagnifierInfo( |
| currentLineBoundaries: reasonableTextField, |
| fieldBounds: reasonableTextField, |
| caretRect: reasonableTextField, |
| globalGesturePosition: reasonableTextField.center, |
| ), |
| ), |
| ); |
| |
| expect(getIsAnimated(tester), false); |
| }); |
| |
| testWidgets('should not be animated on horizontal shifts', |
| (WidgetTester tester) async { |
| await tester.pumpWidget(const MaterialApp( |
| home: Placeholder(), |
| )); |
| |
| final BuildContext context = |
| tester.firstElement(find.byType(Placeholder)); |
| |
| final ValueNotifier<MagnifierInfo> magnifierPositioner = |
| ValueNotifier<MagnifierInfo>( |
| MagnifierInfo( |
| currentLineBoundaries: reasonableTextField, |
| fieldBounds: reasonableTextField, |
| caretRect: reasonableTextField, |
| globalGesturePosition: reasonableTextField.center, |
| ), |
| ); |
| |
| await showMagnifier(context, tester, magnifierPositioner); |
| |
| // New position has a horizontal shift. |
| magnifierPositioner.value = MagnifierInfo( |
| currentLineBoundaries: reasonableTextField, |
| fieldBounds: reasonableTextField, |
| caretRect: reasonableTextField, |
| globalGesturePosition: |
| reasonableTextField.center + const Offset(200, 0), |
| ); |
| await tester.pumpAndSettle(); |
| |
| expect(getIsAnimated(tester), false); |
| }); |
| |
| testWidgets('should be animated on vertical shifts', |
| (WidgetTester tester) async { |
| const Offset verticalShift = Offset(0, 200); |
| |
| await tester.pumpWidget(const MaterialApp( |
| home: Placeholder(), |
| )); |
| |
| final BuildContext context = |
| tester.firstElement(find.byType(Placeholder)); |
| |
| final ValueNotifier<MagnifierInfo> magnifierPositioner = |
| ValueNotifier<MagnifierInfo>( |
| MagnifierInfo( |
| currentLineBoundaries: reasonableTextField, |
| fieldBounds: reasonableTextField, |
| caretRect: reasonableTextField, |
| globalGesturePosition: reasonableTextField.center, |
| ), |
| ); |
| |
| await showMagnifier(context, tester, magnifierPositioner); |
| |
| // New position has a vertical shift. |
| magnifierPositioner.value = MagnifierInfo( |
| currentLineBoundaries: reasonableTextField.shift(verticalShift), |
| fieldBounds: Rect.fromPoints(reasonableTextField.topLeft, |
| reasonableTextField.bottomRight + verticalShift), |
| caretRect: reasonableTextField.shift(verticalShift), |
| globalGesturePosition: reasonableTextField.center + verticalShift, |
| ); |
| |
| await tester.pump(); |
| expect(getIsAnimated(tester), true); |
| }); |
| |
| testWidgets('should stop being animated when timer is up', |
| (WidgetTester tester) async { |
| const Offset verticalShift = Offset(0, 200); |
| |
| await tester.pumpWidget(const MaterialApp( |
| home: Placeholder(), |
| )); |
| |
| final BuildContext context = |
| tester.firstElement(find.byType(Placeholder)); |
| |
| final ValueNotifier<MagnifierInfo> magnifierPositioner = |
| ValueNotifier<MagnifierInfo>( |
| MagnifierInfo( |
| currentLineBoundaries: reasonableTextField, |
| fieldBounds: reasonableTextField, |
| caretRect: reasonableTextField, |
| globalGesturePosition: reasonableTextField.center, |
| ), |
| ); |
| |
| await showMagnifier(context, tester, magnifierPositioner); |
| |
| // New position has a vertical shift. |
| magnifierPositioner.value = MagnifierInfo( |
| currentLineBoundaries: reasonableTextField.shift(verticalShift), |
| fieldBounds: Rect.fromPoints(reasonableTextField.topLeft, |
| reasonableTextField.bottomRight + verticalShift), |
| caretRect: reasonableTextField.shift(verticalShift), |
| globalGesturePosition: reasonableTextField.center + verticalShift, |
| ); |
| |
| await tester.pump(); |
| expect(getIsAnimated(tester), true); |
| await tester.pump(TextMagnifier.jumpBetweenLinesAnimationDuration + |
| const Duration(seconds: 2)); |
| expect(getIsAnimated(tester), false); |
| }); |
| }); |
| }); |
| } |