magnifier_test.dart 18.1 KB
Newer Older
1 2 3 4 5
// 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'])
6
library;
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

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,
32
    ValueNotifier<MagnifierInfo> magnifierInfo,
33 34 35 36
  ) async {
    final Future<void> magnifierShown = magnifierController.show(
        context: context,
        builder: (_) => TextMagnifier(
37
              magnifierInfo: magnifierInfo,
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
            ));

    WidgetsBinding.instance.scheduleFrame();
    await tester.pumpAndSettle();

    // Verify that the magnifier is shown.
    await magnifierShown;
  }

  tearDown(() {
    magnifierController.removeFromOverlay();
    magnifierController.animationController = null;
  });

  group('adaptiveMagnifierControllerBuilder', () {
53
    testWidgets('should return a TextEditingMagnifier on Android',
54 55 56 57 58
        (WidgetTester tester) async {
      await tester.pumpWidget(const MaterialApp(
        home: Placeholder(),
      ));

59
      final BuildContext context = tester.firstElement(find.byType(Placeholder));
60

61 62 63
      final ValueNotifier<MagnifierInfo> magnifierPositioner = ValueNotifier<MagnifierInfo>(MagnifierInfo.empty);
      addTearDown(magnifierPositioner.dispose);

64
      final Widget? builtWidget = TextMagnifier.adaptiveMagnifierConfiguration.magnifierBuilder(
65 66
        context,
        MagnifierController(),
67
        magnifierPositioner,
68 69 70 71 72
      );

      expect(builtWidget, isA<TextMagnifier>());
    }, variant: TargetPlatformVariant.only(TargetPlatform.android));

73
    testWidgets('should return a CupertinoMagnifier on iOS',
74 75 76 77 78
        (WidgetTester tester) async {
      await tester.pumpWidget(const MaterialApp(
        home: Placeholder(),
      ));

79
      final BuildContext context = tester.firstElement(find.byType(Placeholder));
80

81 82 83
      final ValueNotifier<MagnifierInfo> magnifierPositioner = ValueNotifier<MagnifierInfo>(MagnifierInfo.empty);
      addTearDown(magnifierPositioner.dispose);

84 85 86
      final Widget? builtWidget = TextMagnifier.adaptiveMagnifierConfiguration.magnifierBuilder(
        context,
        MagnifierController(),
87
        magnifierPositioner,
88
      );
89 90 91 92

      expect(builtWidget, isA<CupertinoTextMagnifier>());
    }, variant: TargetPlatformVariant.only(TargetPlatform.iOS));

93
    testWidgets('should return null on all platforms not Android, iOS',
94 95 96 97 98
        (WidgetTester tester) async {
      await tester.pumpWidget(const MaterialApp(
        home: Placeholder(),
      ));

99
      final BuildContext context = tester.firstElement(find.byType(Placeholder));
100

101 102 103
      final ValueNotifier<MagnifierInfo> magnifierPositioner = ValueNotifier<MagnifierInfo>(MagnifierInfo.empty);
      addTearDown(magnifierPositioner.dispose);

104 105 106
      final Widget? builtWidget = TextMagnifier.adaptiveMagnifierConfiguration.magnifierBuilder(
        context,
        MagnifierController(),
107
        magnifierPositioner,
108
      );
109 110 111 112 113 114 115 116 117 118 119 120 121

      expect(builtWidget, isNull);
    },
      variant: TargetPlatformVariant.all(
        excluding: <TargetPlatform>{
          TargetPlatform.iOS,
          TargetPlatform.android
        }),
      );
  });

  group('magnifier', () {
    group('position', () {
122
      testWidgets(
123 124 125 126 127 128 129 130 131
          '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(
132
          ColoredBox(
133 134 135
            color: const Color.fromARGB(255, 0, 255, 179),
            child: MaterialApp(
              home: Center(
136 137 138 139 140 141 142 143
                child: Container(
                  key: textField,
                  width: 10,
                  height: 10,
                  color: Colors.red,
                  child: const Placeholder(),
                ),
              ),
144 145 146 147 148 149 150 151 152 153 154 155 156 157
            ),
          ),
        );

        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;

158 159 160
        final ValueNotifier<MagnifierInfo> magnifierInfo =
            ValueNotifier<MagnifierInfo>(
                MagnifierInfo(
161
          currentLineBoundaries: fakeTextFieldRect,
162 163 164 165 166
          fieldBounds: fakeTextFieldRect,
          caretRect: fakeTextFieldRect,
          // The tap position is dragBelow units below the text field.
          globalGesturePosition: fakeTextFieldRect.center,
        ));
167
        addTearDown(magnifierInfo.dispose);
168 169 170 171 172 173 174 175 176 177 178

        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'),
        );
      });

179
      testWidgets(
180 181 182 183 184 185 186 187 188 189 190
          '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));

191 192 193
        late ValueNotifier<MagnifierInfo> magnifierPositioner;
        addTearDown(() => magnifierPositioner.dispose());

194 195 196
        await showMagnifier(
          context,
          tester,
197
          magnifierPositioner = ValueNotifier<MagnifierInfo>(
198
            MagnifierInfo(
199
              currentLineBoundaries: reasonableTextField,
200
              // Inflate these two to make sure we're bounding on the
201
              // current line boundaries, not anything else.
202 203 204 205 206 207 208 209 210 211 212 213 214
              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));
      });

215
      testWidgets(
216 217 218 219 220 221 222 223 224 225 226
          '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));

227 228 229
        late ValueNotifier<MagnifierInfo> magnifierPositioner;
        addTearDown(() => magnifierPositioner.dispose());

230 231 232
        await showMagnifier(
          context,
          tester,
233
          magnifierPositioner = ValueNotifier<MagnifierInfo>(
234
            MagnifierInfo(
235
              currentLineBoundaries: reasonableTextField,
236
              // Inflate these two to make sure we're bounding on the
237
              // current line boundaries, not anything else.
238 239 240 241 242 243 244 245 246 247 248 249
              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));
      });

250
      testWidgets('should position vertically at the center of the line', (WidgetTester tester) async {
251 252 253 254 255 256 257
        await tester.pumpWidget(const MaterialApp(
          home: Placeholder(),
        ));

        final BuildContext context =
            tester.firstElement(find.byType(Placeholder));

258 259 260
        late ValueNotifier<MagnifierInfo> magnifierPositioner;
        addTearDown(() => magnifierPositioner.dispose());

261 262 263
        await showMagnifier(
            context,
            tester,
264
            magnifierPositioner = ValueNotifier<MagnifierInfo>(
265
                MagnifierInfo(
266
              currentLineBoundaries: reasonableTextField,
267 268 269 270 271 272 273 274 275
              fieldBounds: reasonableTextField,
              caretRect: reasonableTextField,
              globalGesturePosition: reasonableTextField.center,
            )));

        expect(getMagnifierPosition(tester).dy,
            reasonableTextField.center.dy - basicOffset.dy);
      });

276
      testWidgets('should reposition vertically if mashed against the ceiling',
277 278 279 280 281 282 283 284 285 286 287
          (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));

288 289 290
        late ValueNotifier<MagnifierInfo> magnifierPositioner;
        addTearDown(() => magnifierPositioner.dispose());

291 292 293
        await showMagnifier(
          context,
          tester,
294
          magnifierPositioner = ValueNotifier<MagnifierInfo>(
295
            MagnifierInfo(
296
              currentLineBoundaries: topOfScreenTextFieldRect,
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
              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;
      }

314
      testWidgets(
315 316 317 318 319 320 321 322 323
          '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));

324 325 326
        late ValueNotifier<MagnifierInfo> magnifierPositioner;
        addTearDown(() => magnifierPositioner.dispose());

327 328 329
        await showMagnifier(
          context,
          tester,
330
          magnifierPositioner =  ValueNotifier<MagnifierInfo>(
331
            MagnifierInfo(
332
              currentLineBoundaries: reasonableTextField,
333 334 335 336 337 338 339 340 341 342 343 344
              fieldBounds: reasonableTextField,
              caretRect: reasonableTextField,
              // Gesture on the far right of the magnifier.
              globalGesturePosition: reasonableTextField.topLeft,
            ),
          ),
        );

        expect(getMagnifierAdditionalFocalPoint(tester).dx,
            lessThan(reasonableTextField.left));
      });

345
      testWidgets(
346 347 348 349 350 351 352 353 354 355 356 357
          '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));

358 359 360
        late ValueNotifier<MagnifierInfo> magnifierPositioner;
        addTearDown(() => magnifierPositioner.dispose());

361 362 363
        await showMagnifier(
          context,
          tester,
364
          magnifierPositioner = ValueNotifier<MagnifierInfo>(
365
            MagnifierInfo(
366
              currentLineBoundaries: topOfScreenTextFieldRect,
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384
              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;
      }

385
      testWidgets('should not be animated on the initial state',
386 387 388 389 390 391 392 393
          (WidgetTester tester) async {
        await tester.pumpWidget(const MaterialApp(
          home: Placeholder(),
        ));

        final BuildContext context =
            tester.firstElement(find.byType(Placeholder));

394 395 396
        late ValueNotifier<MagnifierInfo> magnifierInfo;
        addTearDown(() => magnifierInfo.dispose());

397 398 399
        await showMagnifier(
          context,
          tester,
400
          magnifierInfo = ValueNotifier<MagnifierInfo>(
401
            MagnifierInfo(
402
              currentLineBoundaries: reasonableTextField,
403 404 405 406 407 408 409 410 411 412
              fieldBounds: reasonableTextField,
              caretRect: reasonableTextField,
              globalGesturePosition: reasonableTextField.center,
            ),
          ),
        );

        expect(getIsAnimated(tester), false);
      });

413
      testWidgets('should not be animated on horizontal shifts',
414 415 416 417 418 419 420 421
          (WidgetTester tester) async {
        await tester.pumpWidget(const MaterialApp(
          home: Placeholder(),
        ));

        final BuildContext context =
            tester.firstElement(find.byType(Placeholder));

422
        final ValueNotifier<MagnifierInfo> magnifierPositioner = ValueNotifier<MagnifierInfo>(
423
          MagnifierInfo(
424
            currentLineBoundaries: reasonableTextField,
425 426 427 428 429
            fieldBounds: reasonableTextField,
            caretRect: reasonableTextField,
            globalGesturePosition: reasonableTextField.center,
          ),
        );
430
        addTearDown(magnifierPositioner.dispose);
431 432 433 434

        await showMagnifier(context, tester, magnifierPositioner);

        // New position has a horizontal shift.
435
        magnifierPositioner.value = MagnifierInfo(
436
          currentLineBoundaries: reasonableTextField,
437 438 439 440 441 442 443 444 445 446
          fieldBounds: reasonableTextField,
          caretRect: reasonableTextField,
          globalGesturePosition:
              reasonableTextField.center + const Offset(200, 0),
        );
        await tester.pumpAndSettle();

        expect(getIsAnimated(tester), false);
      });

447
      testWidgets('should be animated on vertical shifts',
448 449 450 451 452 453 454 455 456 457
          (WidgetTester tester) async {
        const Offset verticalShift = Offset(0, 200);

        await tester.pumpWidget(const MaterialApp(
          home: Placeholder(),
        ));

        final BuildContext context =
            tester.firstElement(find.byType(Placeholder));

458
        final ValueNotifier<MagnifierInfo> magnifierPositioner = ValueNotifier<MagnifierInfo>(
459
          MagnifierInfo(
460
            currentLineBoundaries: reasonableTextField,
461 462 463 464 465
            fieldBounds: reasonableTextField,
            caretRect: reasonableTextField,
            globalGesturePosition: reasonableTextField.center,
          ),
        );
466
        addTearDown(magnifierPositioner.dispose);
467 468 469 470

        await showMagnifier(context, tester, magnifierPositioner);

        // New position has a vertical shift.
471
        magnifierPositioner.value = MagnifierInfo(
472
          currentLineBoundaries: reasonableTextField.shift(verticalShift),
473 474 475 476 477 478 479 480 481 482
          fieldBounds: Rect.fromPoints(reasonableTextField.topLeft,
              reasonableTextField.bottomRight + verticalShift),
          caretRect: reasonableTextField.shift(verticalShift),
          globalGesturePosition: reasonableTextField.center + verticalShift,
        );

        await tester.pump();
        expect(getIsAnimated(tester), true);
      });

483
      testWidgets('should stop being animated when timer is up',
484 485 486 487 488 489 490 491 492 493
          (WidgetTester tester) async {
        const Offset verticalShift = Offset(0, 200);

        await tester.pumpWidget(const MaterialApp(
          home: Placeholder(),
        ));

        final BuildContext context =
            tester.firstElement(find.byType(Placeholder));

494
        final ValueNotifier<MagnifierInfo> magnifierPositioner = ValueNotifier<MagnifierInfo>(
495
          MagnifierInfo(
496
            currentLineBoundaries: reasonableTextField,
497 498 499 500 501
            fieldBounds: reasonableTextField,
            caretRect: reasonableTextField,
            globalGesturePosition: reasonableTextField.center,
          ),
        );
502
        addTearDown(magnifierPositioner.dispose);
503 504 505 506

        await showMagnifier(context, tester, magnifierPositioner);

        // New position has a vertical shift.
507
        magnifierPositioner.value = MagnifierInfo(
508
          currentLineBoundaries: reasonableTextField.shift(verticalShift),
509 510 511 512 513 514 515 516 517 518 519 520 521 522 523
          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);
      });
    });
  });
}