// 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']) library; 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 ValueNotifier<MagnifierInfo> magnifierPositioner = ValueNotifier<MagnifierInfo>(MagnifierInfo.empty); addTearDown(magnifierPositioner.dispose); final Widget? builtWidget = TextMagnifier.adaptiveMagnifierConfiguration.magnifierBuilder( context, MagnifierController(), magnifierPositioner, ); 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 ValueNotifier<MagnifierInfo> magnifierPositioner = ValueNotifier<MagnifierInfo>(MagnifierInfo.empty); addTearDown(magnifierPositioner.dispose); final Widget? builtWidget = TextMagnifier.adaptiveMagnifierConfiguration.magnifierBuilder( context, MagnifierController(), magnifierPositioner, ); 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 ValueNotifier<MagnifierInfo> magnifierPositioner = ValueNotifier<MagnifierInfo>(MagnifierInfo.empty); addTearDown(magnifierPositioner.dispose); final Widget? builtWidget = TextMagnifier.adaptiveMagnifierConfiguration.magnifierBuilder( context, MagnifierController(), magnifierPositioner, ); 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( ColoredBox( 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, )); addTearDown(magnifierInfo.dispose); 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)); late ValueNotifier<MagnifierInfo> magnifierPositioner; addTearDown(() => magnifierPositioner.dispose()); await showMagnifier( context, tester, magnifierPositioner = 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)); late ValueNotifier<MagnifierInfo> magnifierPositioner; addTearDown(() => magnifierPositioner.dispose()); await showMagnifier( context, tester, magnifierPositioner = 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)); late ValueNotifier<MagnifierInfo> magnifierPositioner; addTearDown(() => magnifierPositioner.dispose()); await showMagnifier( context, tester, magnifierPositioner = 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)); late ValueNotifier<MagnifierInfo> magnifierPositioner; addTearDown(() => magnifierPositioner.dispose()); await showMagnifier( context, tester, magnifierPositioner = 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)); late ValueNotifier<MagnifierInfo> magnifierPositioner; addTearDown(() => magnifierPositioner.dispose()); await showMagnifier( context, tester, magnifierPositioner = 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)); late ValueNotifier<MagnifierInfo> magnifierPositioner; addTearDown(() => magnifierPositioner.dispose()); await showMagnifier( context, tester, magnifierPositioner = 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 initial state', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: Placeholder(), )); final BuildContext context = tester.firstElement(find.byType(Placeholder)); late ValueNotifier<MagnifierInfo> magnifierInfo; addTearDown(() => magnifierInfo.dispose()); await showMagnifier( context, tester, magnifierInfo = 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, ), ); addTearDown(magnifierPositioner.dispose); 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, ), ); addTearDown(magnifierPositioner.dispose); 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, ), ); addTearDown(magnifierPositioner.dispose); 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); }); }); }); }