refresh_test.dart 47.5 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
Ian Hickson's avatar
Ian Hickson committed
4

5 6 7 8 9 10 11 12 13 14 15
import 'dart:async';

import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';

void main() {
  MockHelper mockHelper;

16
  /// Completer that holds the future given to the CupertinoSliverRefreshControl.
17 18
  Completer<void> refreshCompleter;

19
  /// The widget that the indicator builder given to the CupertinoSliverRefreshControl
20 21 22
  /// returns.
  Widget refreshIndicator;

23 24
  /// These two Functions are required to avoid tearing off of the MockHelper object,
  /// which is not supported when using Dart 2 runtime semantics.
25
  final RefreshControlIndicatorBuilder builder = (
26 27 28 29 30 31
    BuildContext context,
    RefreshIndicatorMode refreshState,
    double pulledExtent,
    double refreshTriggerPullDistance,
    double refreshIndicatorExtent,
  ) => mockHelper.builder(context, refreshState, pulledExtent, refreshTriggerPullDistance, refreshIndicatorExtent);
32

33
  Future<void> onRefresh() => mockHelper.refreshTask();
34

35
  setUp(() {
36 37 38
    mockHelper = MockHelper();
    refreshCompleter = Completer<void>.sync();
    refreshIndicator = Container();
39

40 41
    when(mockHelper.builder(any, any, any, any, any))
      .thenAnswer((Invocation i) {
42 43 44
        final double pulledExtent = i.positionalArguments[2] as double;
        final double refreshTriggerPullDistance = i.positionalArguments[3] as double;
        final double refreshIndicatorExtent = i.positionalArguments[4] as double;
45 46 47 48 49 50 51 52 53 54 55
        if (pulledExtent < 0.0) {
          throw TestFailure('The pulledExtent should never be less than 0.0');
        }
        if (refreshTriggerPullDistance < 0.0) {
          throw TestFailure('The refreshTriggerPullDistance should never be less than 0.0');
        }
        if (refreshIndicatorExtent < 0.0) {
          throw TestFailure('The refreshIndicatorExtent should never be less than 0.0');
        }
        return refreshIndicator;
      });
56

57
    when(mockHelper.refreshTask()).thenAnswer((_) => refreshCompleter.future);
58 59
  });

60
  int testListLength = 10;
61
  SliverList buildAListOfStuff() {
62 63
    return SliverList(
      delegate: SliverChildBuilderDelegate(
64
        (BuildContext context, int index) {
65
          return Container(
66
            height: 200.0,
67
            child: Center(child: Text(index.toString())),
68 69
          );
        },
70
        childCount: testListLength,
71 72 73 74
      ),
    );
  }

75
  final VoidCallback uiTestGroup = () {
76 77
    testWidgets("doesn't invoke anything without user interaction", (WidgetTester tester) async {
      await tester.pumpWidget(
78
        Directionality(
79
          textDirection: TextDirection.ltr,
80
          child: CustomScrollView(
81
            slivers: <Widget>[
82
              CupertinoSliverRefreshControl(
83
                builder: builder,
84 85 86 87 88 89 90 91 92 93 94 95 96
              ),
              buildAListOfStuff(),
            ],
          ),
        ),
      );

      verifyNoMoreInteractions(mockHelper);

      expect(
        tester.getTopLeft(find.widgetWithText(Container, '0')),
        const Offset(0.0, 0.0),
      );
Dan Field's avatar
Dan Field committed
97
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
98 99 100

    testWidgets('calls the indicator builder when starting to overscroll', (WidgetTester tester) async {
      await tester.pumpWidget(
101
        Directionality(
102
          textDirection: TextDirection.ltr,
103
          child: CustomScrollView(
104
            slivers: <Widget>[
105
              CupertinoSliverRefreshControl(
106
                builder: builder,
107 108 109 110 111 112 113 114
              ),
              buildAListOfStuff(),
            ],
          ),
        ),
      );

      // Drag down but not enough to trigger the refresh.
115
      await tester.drag(find.text('0'), const Offset(0.0, 50.0), touchSlopY: 0);
116 117
      await tester.pump();

118
      // The function is referenced once while passing into CupertinoSliverRefreshControl
119
      // and is called.
120
      verify(mockHelper.builder(
121
        any,
122 123 124 125 126 127 128 129 130 131 132
        RefreshIndicatorMode.drag,
        50.0,
        100.0, // Default value.
        60.0, // Default value.
      ));
      verifyNoMoreInteractions(mockHelper);

      expect(
        tester.getTopLeft(find.widgetWithText(Container, '0')),
        const Offset(0.0, 50.0),
      );
Dan Field's avatar
Dan Field committed
133
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
134 135 136 137 138

    testWidgets(
      "don't call the builder if overscroll doesn't move slivers like on Android",
      (WidgetTester tester) async {
        await tester.pumpWidget(
139
          Directionality(
140
            textDirection: TextDirection.ltr,
141
            child: CustomScrollView(
142
              slivers: <Widget>[
143
                CupertinoSliverRefreshControl(
144
                  builder: builder,
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
                ),
                buildAListOfStuff(),
              ],
            ),
          ),
        );

        // Drag down but not enough to trigger the refresh.
        await tester.drag(find.text('0'), const Offset(0.0, 50.0));
        await tester.pump();

        verifyNoMoreInteractions(mockHelper);

        expect(
          tester.getTopLeft(find.widgetWithText(Container, '0')),
          const Offset(0.0, 0.0),
        );
Dan Field's avatar
Dan Field committed
162
    }, variant: TargetPlatformVariant.only(TargetPlatform.android));
163

164
    testWidgets('let the builder update as canceled drag scrolls away', (WidgetTester tester) async {
165
      await tester.pumpWidget(
166
        Directionality(
167
          textDirection: TextDirection.ltr,
168
          child: CustomScrollView(
169
            slivers: <Widget>[
170
              CupertinoSliverRefreshControl(
171
                builder: builder,
172 173 174 175 176 177 178 179
              ),
              buildAListOfStuff(),
            ],
          ),
        ),
      );

      // Drag down but not enough to trigger the refresh.
180
      await tester.drag(find.text('0'), const Offset(0.0, 50.0), touchSlopY: 0);
181 182 183 184 185 186 187
      await tester.pump();
      await tester.pump(const Duration(milliseconds: 20));
      await tester.pump(const Duration(milliseconds: 20));
      await tester.pump(const Duration(seconds: 3));

      verifyInOrder(<void>[
        mockHelper.builder(
188
          any,
189 190 191 192 193 194
          RefreshIndicatorMode.drag,
          50.0,
          100.0, // Default value.
          60.0, // Default value.
        ),
        mockHelper.builder(
195
          any,
196
          RefreshIndicatorMode.drag,
197
          argThat(moreOrLessEquals(48.36801747187993)),
198 199 200 201
          100.0, // Default value.
          60.0, // Default value.
        ),
        mockHelper.builder(
202
          any,
203
          RefreshIndicatorMode.drag,
204
          argThat(moreOrLessEquals(44.63031931875867)),
205 206 207 208 209 210 211 212 213 214 215
          100.0, // Default value.
          60.0, // Default value.
        ),
        // The builder isn't called again when the sliver completely goes away.
      ]);
      verifyNoMoreInteractions(mockHelper);

      expect(
        tester.getTopLeft(find.widgetWithText(Container, '0')),
        const Offset(0.0, 0.0),
      );
Dan Field's avatar
Dan Field committed
216
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
217 218 219 220 221 222 223 224 225

    testWidgets('drag past threshold triggers refresh task', (WidgetTester tester) async {
      final List<MethodCall> platformCallLog = <MethodCall>[];

      SystemChannels.platform.setMockMethodCallHandler((MethodCall methodCall) async {
        platformCallLog.add(methodCall);
      });

      await tester.pumpWidget(
226
        Directionality(
227
          textDirection: TextDirection.ltr,
228
          child: CustomScrollView(
229
            slivers: <Widget>[
230
              CupertinoSliverRefreshControl(
231 232
                builder: builder,
                onRefresh: onRefresh,
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
              ),
              buildAListOfStuff(),
            ],
          ),
        ),
      );

      final TestGesture gesture = await tester.startGesture(const Offset(0.0, 0.0));
      await gesture.moveBy(const Offset(0.0, 99.0));
      await tester.pump();
      await gesture.moveBy(const Offset(0.0, -30.0));
      await tester.pump();
      await gesture.moveBy(const Offset(0.0, 50.0));
      await tester.pump();

      verifyInOrder(<void>[
        mockHelper.builder(
250
          any,
251 252 253 254 255 256
          RefreshIndicatorMode.drag,
          99.0,
          100.0, // Default value.
          60.0, // Default value.
        ),
        mockHelper.builder(
257
          any,
258
          RefreshIndicatorMode.drag,
259
          argThat(moreOrLessEquals(86.78169)),
260 261 262 263
          100.0, // Default value.
          60.0, // Default value.
        ),
        mockHelper.builder(
264
          any,
265
          RefreshIndicatorMode.armed,
266
          argThat(moreOrLessEquals(105.80452021305739)),
267 268 269 270 271 272 273 274 275 276 277 278
          100.0, // Default value.
          60.0, // Default value.
        ),
        // The refresh callback is triggered after the frame.
        mockHelper.refreshTask(),
      ]);
      verifyNoMoreInteractions(mockHelper);

      expect(
        platformCallLog.last,
        isMethodCall('HapticFeedback.vibrate', arguments: 'HapticFeedbackType.mediumImpact'),
      );
Dan Field's avatar
Dan Field committed
279
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
280 281 282 283 284

    testWidgets(
      'refreshing task keeps the sliver expanded forever until done',
      (WidgetTester tester) async {
        await tester.pumpWidget(
285
          Directionality(
286
            textDirection: TextDirection.ltr,
287
            child: CustomScrollView(
288
              slivers: <Widget>[
289
                CupertinoSliverRefreshControl(
290 291
                  builder: builder,
                  onRefresh: onRefresh,
292 293 294 295 296 297 298
                ),
                buildAListOfStuff(),
              ],
            ),
          ),
        );

299
        await tester.drag(find.text('0'), const Offset(0.0, 150.0), touchSlopY: 0);
300 301 302 303 304 305
        await tester.pump();
        // Let it start snapping back.
        await tester.pump(const Duration(milliseconds: 50));

        verifyInOrder(<void>[
          mockHelper.builder(
306
            any,
307 308 309 310 311 312 313
            RefreshIndicatorMode.armed,
            150.0,
            100.0, // Default value.
            60.0, // Default value.
          ),
          mockHelper.refreshTask(),
          mockHelper.builder(
314
            any,
315
            RefreshIndicatorMode.armed,
316
            argThat(moreOrLessEquals(127.10396988577114)),
317 318 319 320 321 322 323 324
            100.0, // Default value.
            60.0, // Default value.
          ),
        ]);

        // Reaches refresh state and sliver's at 60.0 in height after a while.
        await tester.pump(const Duration(seconds: 1));
        verify(mockHelper.builder(
325
          any,
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343
          RefreshIndicatorMode.refresh,
          60.0,
          100.0, // Default value.
          60.0, // Default value.
        ));

        // Stays in that state forever until future completes.
        await tester.pump(const Duration(seconds: 1000));
        verifyNoMoreInteractions(mockHelper);
        expect(
          tester.getTopLeft(find.widgetWithText(Container, '0')),
          const Offset(0.0, 60.0),
        );

        refreshCompleter.complete(null);
        await tester.pump();

        verify(mockHelper.builder(
344
          any,
345 346 347 348 349 350
          RefreshIndicatorMode.done,
          60.0,
          100.0, // Default value.
          60.0, // Default value.
        ));
        verifyNoMoreInteractions(mockHelper);
Dan Field's avatar
Dan Field committed
351
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
352

353 354 355 356 357 358
    testWidgets(
      'refreshing task keeps the sliver expanded forever until completes with error',
      (WidgetTester tester) async {
        final FlutterError error = FlutterError('Oops');
        double errorCount = 0;

359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
        runZoned(
          () async {
            refreshCompleter = Completer<void>.sync();

            await tester.pumpWidget(
              Directionality(
                textDirection: TextDirection.ltr,
                child: CustomScrollView(
                  slivers: <Widget>[
                    CupertinoSliverRefreshControl(
                      builder: builder,
                      onRefresh: onRefresh,
                    ),
                    buildAListOfStuff(),
                  ],
                ),
375
              ),
376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
            );

            await tester.drag(find.text('0'), const Offset(0.0, 150.0), touchSlopY: 0);
            await tester.pump();
            // Let it start snapping back.
            await tester.pump(const Duration(milliseconds: 50));

            verifyInOrder(<void>[
              mockHelper.builder(
                any,
                RefreshIndicatorMode.armed,
                150.0,
                100.0, // Default value.
                60.0, // Default value.
              ),
              mockHelper.refreshTask(),
              mockHelper.builder(
                any,
                RefreshIndicatorMode.armed,
                argThat(moreOrLessEquals(127.10396988577114)),
                100.0, // Default value.
                60.0, // Default value.
              ),
            ]);
400

401 402 403
            // Reaches refresh state and sliver's at 60.0 in height after a while.
            await tester.pump(const Duration(seconds: 1));
            verify(mockHelper.builder(
404
              any,
405 406
              RefreshIndicatorMode.refresh,
              60.0,
407 408
              100.0, // Default value.
              60.0, // Default value.
409
            ));
410

411 412 413 414 415 416 417
            // Stays in that state forever until future completes.
            await tester.pump(const Duration(seconds: 1000));
            verifyNoMoreInteractions(mockHelper);
            expect(
              tester.getTopLeft(find.widgetWithText(Container, '0')),
              const Offset(0.0, 60.0),
            );
418

419 420
            refreshCompleter.completeError(error);
            await tester.pump();
421

422 423 424 425 426 427 428 429 430 431 432 433 434 435 436
            verify(mockHelper.builder(
              any,
              RefreshIndicatorMode.done,
              60.0,
              100.0, // Default value.
              60.0, // Default value.
            ));
            verifyNoMoreInteractions(mockHelper);
          },
          onError: (dynamic e) {
            expect(e, error);
            expect(errorCount, 0);
            errorCount++;
          },
        );
Dan Field's avatar
Dan Field committed
437
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
438

439
    testWidgets('expanded refreshing sliver scrolls normally', (WidgetTester tester) async {
440
      refreshIndicator = const Center(child: Text('-1'));
441 442

      await tester.pumpWidget(
443
        Directionality(
444
          textDirection: TextDirection.ltr,
445
          child: CustomScrollView(
446
            slivers: <Widget>[
447
              CupertinoSliverRefreshControl(
448 449
                builder: builder,
                onRefresh: onRefresh,
450 451 452 453 454 455 456
              ),
              buildAListOfStuff(),
            ],
          ),
        ),
      );

457
      await tester.drag(find.text('0'), const Offset(0.0, 150.0), touchSlopY: 0);
458 459 460
      await tester.pump();

      verify(mockHelper.builder(
461
        any,
462 463 464 465 466 467 468 469 470
        RefreshIndicatorMode.armed,
        150.0,
        100.0, // Default value.
        60.0, // Default value.
      ));

      // Given a box constraint of 150, the Center will occupy all that height.
      expect(
        tester.getRect(find.widgetWithText(Center, '-1')),
Dan Field's avatar
Dan Field committed
471
        const Rect.fromLTRB(0.0, 0.0, 800.0, 150.0),
472 473
      );

474
      await tester.drag(find.text('0'), const Offset(0.0, -300.0), touchSlopY: 0);
475 476 477 478
      await tester.pump();

      // Refresh indicator still being told to layout the same way.
      verify(mockHelper.builder(
479
        any,
480 481 482 483 484 485 486 487
        RefreshIndicatorMode.refresh,
        60.0,
        100.0, // Default value.
        60.0, // Default value.
      ));

      // Now the sliver is scrolled off screen.
      expect(
488
        tester.getTopLeft(find.widgetWithText(Center, '-1', skipOffstage: false)).dy,
489 490 491
        moreOrLessEquals(-175.38461538461536),
      );
      expect(
492
        tester.getBottomLeft(find.widgetWithText(Center, '-1', skipOffstage: false)).dy,
493 494 495 496 497 498 499 500 501 502 503 504 505 506
        moreOrLessEquals(-115.38461538461536),
      );
      expect(
        tester.getTopLeft(find.widgetWithText(Center, '0')).dy,
        moreOrLessEquals(-115.38461538461536),
      );

      // Scroll the top of the refresh indicator back to overscroll, it will
      // snap to the size of the refresh indicator and stay there.
      await tester.drag(find.text('1'), const Offset(0.0, 200.0));
      await tester.pump();
      await tester.pump(const Duration(seconds: 2));
      expect(
        tester.getRect(find.widgetWithText(Center, '-1')),
Dan Field's avatar
Dan Field committed
507
        const Rect.fromLTRB(0.0, 0.0, 800.0, 60.0),
508 509 510
      );
      expect(
        tester.getRect(find.widgetWithText(Center, '0')),
Dan Field's avatar
Dan Field committed
511
        const Rect.fromLTRB(0.0, 60.0, 800.0, 260.0),
512
      );
Dan Field's avatar
Dan Field committed
513
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
514 515

    testWidgets('expanded refreshing sliver goes away when done', (WidgetTester tester) async {
516
      refreshIndicator = const Center(child: Text('-1'));
517 518

      await tester.pumpWidget(
519
        Directionality(
520
          textDirection: TextDirection.ltr,
521
          child: CustomScrollView(
522
            slivers: <Widget>[
523
              CupertinoSliverRefreshControl(
524 525
                builder: builder,
                onRefresh: onRefresh,
526 527 528 529 530 531 532
              ),
              buildAListOfStuff(),
            ],
          ),
        ),
      );

533
      await tester.drag(find.text('0'), const Offset(0.0, 150.0), touchSlopY: 0);
534 535
      await tester.pump();
      verify(mockHelper.builder(
536
        any,
537 538 539 540 541 542 543
        RefreshIndicatorMode.armed,
        150.0,
        100.0, // Default value.
        60.0, // Default value.
      ));
      expect(
        tester.getRect(find.widgetWithText(Center, '-1')),
Dan Field's avatar
Dan Field committed
544
        const Rect.fromLTRB(0.0, 0.0, 800.0, 150.0),
545 546 547 548 549 550 551 552
      );
      verify(mockHelper.refreshTask());

      // Rebuilds the sliver with a layout extent now.
      await tester.pump();
      // Let it snap back to occupy the indicator's final sliver space only.
      await tester.pump(const Duration(seconds: 2));
      verify(mockHelper.builder(
553
        any,
554 555 556 557 558 559 560
        RefreshIndicatorMode.refresh,
        60.0,
        100.0, // Default value.
        60.0, // Default value.
      ));
      expect(
        tester.getRect(find.widgetWithText(Center, '-1')),
Dan Field's avatar
Dan Field committed
561
        const Rect.fromLTRB(0.0, 0.0, 800.0, 60.0),
562 563 564
      );
      expect(
        tester.getRect(find.widgetWithText(Center, '0')),
Dan Field's avatar
Dan Field committed
565
        const Rect.fromLTRB(0.0, 60.0, 800.0, 260.0),
566 567 568 569 570
      );

      refreshCompleter.complete(null);
      await tester.pump();
      verify(mockHelper.builder(
571
        any,
572 573 574 575 576 577 578 579 580 581
        RefreshIndicatorMode.done,
        60.0,
        100.0, // Default value.
        60.0, // Default value.
      ));

      await tester.pump(const Duration(seconds: 5));
      expect(find.text('-1'), findsNothing);
      expect(
        tester.getRect(find.widgetWithText(Center, '0')),
Dan Field's avatar
Dan Field committed
582
        const Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
583
      );
Dan Field's avatar
Dan Field committed
584
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603

    testWidgets('builder still called when sliver snapped back more than 90%', (WidgetTester tester) async {
      refreshIndicator = const Center(child: Text('-1'));

      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: CustomScrollView(
            slivers: <Widget>[
              CupertinoSliverRefreshControl(
                builder: builder,
                onRefresh: onRefresh,
              ),
              buildAListOfStuff(),
            ],
          ),
        ),
      );

604
      await tester.drag(find.text('0'), const Offset(0.0, 150.0), touchSlopY: 0);
605 606 607 608 609 610 611 612 613 614
      await tester.pump();
      verify(mockHelper.builder(
        any,
        RefreshIndicatorMode.armed,
        150.0,
        100.0, // Default value.
        60.0, // Default value.
      ));
      expect(
        tester.getRect(find.widgetWithText(Center, '-1')),
Dan Field's avatar
Dan Field committed
615
        const Rect.fromLTRB(0.0, 0.0, 800.0, 150.0),
616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631
      );
      verify(mockHelper.refreshTask());

      // Rebuilds the sliver with a layout extent now.
      await tester.pump();
      // Let it snap back to occupy the indicator's final sliver space only.
      await tester.pump(const Duration(seconds: 2));
      verify(mockHelper.builder(
        any,
        RefreshIndicatorMode.refresh,
        60.0,
        100.0, // Default value.
        60.0, // Default value.
      ));
      expect(
        tester.getRect(find.widgetWithText(Center, '-1')),
Dan Field's avatar
Dan Field committed
632
        const Rect.fromLTRB(0.0, 0.0, 800.0, 60.0),
633 634 635
      );
      expect(
        tester.getRect(find.widgetWithText(Center, '0')),
Dan Field's avatar
Dan Field committed
636
        const Rect.fromLTRB(0.0, 60.0, 800.0, 260.0),
637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667
      );

      refreshCompleter.complete(null);
      await tester.pump();
      verify(mockHelper.builder(
        any,
        RefreshIndicatorMode.done,
        60.0,
        100.0, // Default value.
        60.0, // Default value.
      ));

      // Waiting for refresh control to reach approximately 5% of height
      await tester.pump(const Duration(milliseconds: 400));

      expect(
        tester.getRect(find.widgetWithText(Center, '0')).top,
        moreOrLessEquals(3.0, epsilon: 4e-1),
      );
      expect(
        tester.getRect(find.widgetWithText(Center, '-1')).height,
        moreOrLessEquals(3.0, epsilon: 4e-1),
      );
      verify(mockHelper.builder(
        any,
        RefreshIndicatorMode.inactive,
        2.6980688300546443, // ~5% of 60.0
        100.0, // Default value.
        60.0, // Default value.
      ));
      expect(find.text('-1'), findsOneWidget);
Dan Field's avatar
Dan Field committed
668
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
669 670 671 672

    testWidgets(
      'retracting sliver during done cannot be pulled to refresh again until fully retracted',
      (WidgetTester tester) async {
673
        refreshIndicator = const Center(child: Text('-1'));
674 675

        await tester.pumpWidget(
676
          Directionality(
677
            textDirection: TextDirection.ltr,
678
            child: CustomScrollView(
679
              slivers: <Widget>[
680
                CupertinoSliverRefreshControl(
681 682
                  builder: builder,
                  onRefresh: onRefresh,
683 684 685 686 687 688 689
                ),
                buildAListOfStuff(),
              ],
            ),
          ),
        );

690
        await tester.drag(find.text('0'), const Offset(0.0, 150.0), touchSlopY: 0.0);
691 692 693 694 695 696
        await tester.pump();
        verify(mockHelper.refreshTask());

        refreshCompleter.complete(null);
        await tester.pump();
        verify(mockHelper.builder(
697
          any,
698 699 700 701 702 703 704 705 706 707
          RefreshIndicatorMode.done,
          150.0, // Still overscrolled here.
          100.0, // Default value.
          60.0, // Default value.
        ));

        // Let it start going away but not fully.
        await tester.pump(const Duration(milliseconds: 100));
        // The refresh indicator is still building.
        verify(mockHelper.builder(
708
          any,
709 710 711 712 713 714 715 716 717 718 719 720
          RefreshIndicatorMode.done,
          91.31180913199277,
          100.0, // Default value.
          60.0, // Default value.
        ));
        expect(
          tester.getBottomLeft(find.widgetWithText(Center, '-1')).dy,
          moreOrLessEquals(91.311809131992776),
        );

        // Start another drag by an amount that would have been enough to
        // trigger another refresh if it were in the right state.
721
        await tester.drag(find.text('0'), const Offset(0.0, 150.0), touchSlopY: 0.0);
722 723 724 725 726
        await tester.pump();

        // Instead, it's still in the done state because the sliver never
        // fully retracted.
        verify(mockHelper.builder(
727
          any,
728 729 730 731 732 733 734 735 736 737 738
          RefreshIndicatorMode.done,
          147.3772721631821,
          100.0, // Default value.
          60.0, // Default value.
        ));

        // Now let it fully go away.
        await tester.pump(const Duration(seconds: 5));
        expect(find.text('-1'), findsNothing);
        expect(
          tester.getRect(find.widgetWithText(Center, '0')),
Dan Field's avatar
Dan Field committed
739
          const Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
740 741 742
        );

        // Start another drag. It's now in drag mode.
743
        await tester.drag(find.text('0'), const Offset(0.0, 40.0), touchSlopY: 0.0);
744 745
        await tester.pump();
        verify(mockHelper.builder(
746
          any,
747 748 749 750
          RefreshIndicatorMode.drag,
          40.0,
          100.0, // Default value.
          60.0, // Default value.
Dan Field's avatar
Dan Field committed
751 752
      ));
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
753 754 755 756

    testWidgets(
      'sliver held in overscroll when task finishes completes normally',
      (WidgetTester tester) async {
757
        refreshIndicator = const Center(child: Text('-1'));
758 759

        await tester.pumpWidget(
760
          Directionality(
761
            textDirection: TextDirection.ltr,
762
            child: CustomScrollView(
763
              slivers: <Widget>[
764
                CupertinoSliverRefreshControl(
765 766
                  builder: builder,
                  onRefresh: onRefresh,
767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783
                ),
                buildAListOfStuff(),
              ],
            ),
          ),
        );

        final TestGesture gesture = await tester.startGesture(const Offset(0.0, 0.0));
        // Start a refresh.
        await gesture.moveBy(const Offset(0.0, 150.0));
        await tester.pump();
        verify(mockHelper.refreshTask());

        // Complete the task while held down.
        refreshCompleter.complete(null);
        await tester.pump();
        verify(mockHelper.builder(
784
          any,
785 786 787 788 789 790 791
          RefreshIndicatorMode.done,
          150.0, // Still overscrolled here.
          100.0, // Default value.
          60.0, // Default value.
        ));
        expect(
          tester.getRect(find.widgetWithText(Center, '0')),
Dan Field's avatar
Dan Field committed
792
          const Rect.fromLTRB(0.0, 150.0, 800.0, 350.0),
793 794 795 796 797 798 799 800
        );

        await gesture.up();
        await tester.pump();
        await tester.pump(const Duration(seconds: 5));
        expect(find.text('-1'), findsNothing);
        expect(
          tester.getRect(find.widgetWithText(Center, '0')),
Dan Field's avatar
Dan Field committed
801
          const Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
802
        );
Dan Field's avatar
Dan Field committed
803
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
804 805 806 807

    testWidgets(
      'sliver scrolled away when task completes properly removes itself',
      (WidgetTester tester) async {
808 809 810 811 812
        if (testListLength < 4) {
          // This test only makes sense when the list is long enough that
          // the indicator can be scrolled away while refreshing.
          return;
        }
813
        refreshIndicator = const Center(child: Text('-1'));
814 815

        await tester.pumpWidget(
816
          Directionality(
817
            textDirection: TextDirection.ltr,
818
            child: CustomScrollView(
819
              slivers: <Widget>[
820
                CupertinoSliverRefreshControl(
821 822
                  builder: builder,
                  onRefresh: onRefresh,
823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839
                ),
                buildAListOfStuff(),
              ],
            ),
          ),
        );

        // Start a refresh.
        await tester.drag(find.text('0'), const Offset(0.0, 150.0));
        await tester.pump();
        verify(mockHelper.refreshTask());

        await tester.drag(find.text('0'), const Offset(0.0, -300.0));
        await tester.pump();

        // Refresh indicator still being told to layout the same way.
        verify(mockHelper.builder(
840
          any,
841 842 843 844 845 846 847 848
          RefreshIndicatorMode.refresh,
          60.0,
          100.0, // Default value.
          60.0, // Default value.
        ));

        // Now the sliver is scrolled off screen.
        expect(
849
          tester.getTopLeft(find.widgetWithText(Center, '-1', skipOffstage: false)).dy,
850 851 852
          moreOrLessEquals(-175.38461538461536),
        );
        expect(
853
          tester.getBottomLeft(find.widgetWithText(Center, '-1', skipOffstage: false)).dy,
854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874
          moreOrLessEquals(-115.38461538461536),
        );

        // Complete the task while scrolled away.
        refreshCompleter.complete(null);
        // The sliver is instantly gone since there is no overscroll physics
        // simulation.
        await tester.pump();

        // The next item's position is not disturbed.
        expect(
          tester.getTopLeft(find.widgetWithText(Center, '0')).dy,
          moreOrLessEquals(-115.38461538461536),
        );

        // Scrolling past the first item still results in a new overscroll.
        // The layout extent is gone.
        await tester.drag(find.text('1'), const Offset(0.0, 120.0));
        await tester.pump();

        verify(mockHelper.builder(
875
          any,
876 877 878 879 880 881 882 883 884 885 886 887
          RefreshIndicatorMode.drag,
          4.615384615384642,
          100.0, // Default value.
          60.0, // Default value.
        ));

        // Snaps away normally.
        await tester.pump();
        await tester.pump(const Duration(seconds: 2));
        expect(find.text('-1'), findsNothing);
        expect(
          tester.getRect(find.widgetWithText(Center, '0')),
Dan Field's avatar
Dan Field committed
888
          const Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
889
        );
Dan Field's avatar
Dan Field committed
890
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
891 892 893 894

    testWidgets(
      "don't do anything unless it can be overscrolled at the start of the list",
      (WidgetTester tester) async {
895
        refreshIndicator = const Center(child: Text('-1'));
896 897

        await tester.pumpWidget(
898
          Directionality(
899
            textDirection: TextDirection.ltr,
900
            child: CustomScrollView(
901 902
              slivers: <Widget>[
                buildAListOfStuff(),
903
                CupertinoSliverRefreshControl( // it's in the middle now.
904 905
                  builder: builder,
                  onRefresh: onRefresh,
906 907 908 909 910 911 912 913 914 915 916 917
                ),
                buildAListOfStuff(),
              ],
            ),
          ),
        );

        await tester.fling(find.byType(Container).first, const Offset(0.0, 200.0), 2000.0);

        await tester.fling(find.byType(Container).first, const Offset(0.0, -200.0), 3000.0);

        verifyNoMoreInteractions(mockHelper);
Dan Field's avatar
Dan Field committed
918
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
919 920 921 922

    testWidgets(
      'without an onRefresh, builder is called with arm for one frame then sliver goes away',
      (WidgetTester tester) async {
923
        refreshIndicator = const Center(child: Text('-1'));
924 925

        await tester.pumpWidget(
926
          Directionality(
927
            textDirection: TextDirection.ltr,
928
            child: CustomScrollView(
929
              slivers: <Widget>[
930
                CupertinoSliverRefreshControl(
931
                  builder: builder,
932 933 934 935 936 937 938
                ),
                buildAListOfStuff(),
              ],
            ),
          ),
        );

939
        await tester.drag(find.text('0'), const Offset(0.0, 150.0), touchSlopY: 0.0);
940 941
        await tester.pump();
        verify(mockHelper.builder(
942
          any,
943 944 945 946 947 948 949 950
          RefreshIndicatorMode.armed,
          150.0,
          100.0, // Default value.
          60.0, // Default value.
        ));

        await tester.pump(const Duration(milliseconds: 10));
        verify(mockHelper.builder(
951
          any,
952 953 954 955 956 957 958 959 960 961
          RefreshIndicatorMode.done, // Goes to done on the next frame.
          148.6463892921364,
          100.0, // Default value.
          60.0, // Default value.
        ));

        await tester.pump(const Duration(seconds: 5));
        expect(find.text('-1'), findsNothing);
        expect(
          tester.getRect(find.widgetWithText(Center, '0')),
Dan Field's avatar
Dan Field committed
962
          const Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
963
        );
Dan Field's avatar
Dan Field committed
964
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987

    testWidgets('Should not crash when dragged', (WidgetTester tester) async {
      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: CustomScrollView(
            physics: const BouncingScrollPhysics(),
            slivers: <Widget>[
              CupertinoSliverRefreshControl(
                onRefresh: () async => Future<void>.delayed(const Duration(days: 2000)),
              ),
            ],
          ),
        ),
      );

      await tester.dragFrom(const Offset(100, 10), const Offset(0.0, 50.0), touchSlopY: 0);
      await tester.pump();

      await tester.dragFrom(const Offset(100, 10), const Offset(0, 500), touchSlopY: 0);
      await tester.pump();

      expect(tester.takeException(), isNull);
Dan Field's avatar
Dan Field committed
988
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
989
  };
990

991
  final VoidCallback stateMachineTestGroup = () {
992 993
    testWidgets('starts in inactive state', (WidgetTester tester) async {
      await tester.pumpWidget(
994
        Directionality(
995
          textDirection: TextDirection.ltr,
996
          child: CustomScrollView(
997
            slivers: <Widget>[
998
              CupertinoSliverRefreshControl(
999
                builder: builder,
1000 1001 1002 1003 1004 1005 1006 1007
              ),
              buildAListOfStuff(),
            ],
          ),
        ),
      );

      expect(
1008
        CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder, skipOffstage: false))),
1009 1010
        RefreshIndicatorMode.inactive,
      );
Dan Field's avatar
Dan Field committed
1011
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
1012 1013 1014

    testWidgets('goes to drag and returns to inactive in a small drag', (WidgetTester tester) async {
      await tester.pumpWidget(
1015
        Directionality(
1016
          textDirection: TextDirection.ltr,
1017
          child: CustomScrollView(
1018
            slivers: <Widget>[
1019
              CupertinoSliverRefreshControl(
1020
                builder: builder,
1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031
              ),
              buildAListOfStuff(),
            ],
          ),
        ),
      );

      await tester.drag(find.text('0'), const Offset(0.0, 20.0));
      await tester.pump();

      expect(
1032
        CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))),
1033 1034 1035 1036 1037 1038
        RefreshIndicatorMode.drag,
      );

      await tester.pump(const Duration(seconds: 2));

      expect(
1039
        CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder, skipOffstage: false))),
1040 1041
        RefreshIndicatorMode.inactive,
      );
Dan Field's avatar
Dan Field committed
1042
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
1043 1044 1045

    testWidgets('goes to armed the frame it passes the threshold', (WidgetTester tester) async {
      await tester.pumpWidget(
1046
        Directionality(
1047
          textDirection: TextDirection.ltr,
1048
          child: CustomScrollView(
1049
            slivers: <Widget>[
1050
              CupertinoSliverRefreshControl(
1051
                builder: builder,
1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063
                refreshTriggerPullDistance: 80.0,
              ),
              buildAListOfStuff(),
            ],
          ),
        ),
      );

      final TestGesture gesture = await tester.startGesture(const Offset(0.0, 0.0));
      await gesture.moveBy(const Offset(0.0, 79.0));
      await tester.pump();
      expect(
1064
        CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))),
1065 1066 1067 1068 1069 1070
        RefreshIndicatorMode.drag,
      );

      await gesture.moveBy(const Offset(0.0, 3.0)); // Overscrolling, need to move more than 1px.
      await tester.pump();
      expect(
1071
        CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))),
1072 1073
        RefreshIndicatorMode.armed,
      );
Dan Field's avatar
Dan Field committed
1074
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
1075 1076 1077 1078 1079

    testWidgets(
      'goes to refresh the frame it crossed back the refresh threshold',
      (WidgetTester tester) async {
        await tester.pumpWidget(
1080
          Directionality(
1081
            textDirection: TextDirection.ltr,
1082
            child: CustomScrollView(
1083
              slivers: <Widget>[
1084
                CupertinoSliverRefreshControl(
1085 1086
                  builder: builder,
                  onRefresh: onRefresh,
1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099
                  refreshTriggerPullDistance: 90.0,
                  refreshIndicatorExtent: 50.0,
                ),
                buildAListOfStuff(),
              ],
            ),
          ),
        );

        final TestGesture gesture = await tester.startGesture(const Offset(0.0, 0.0));
        await gesture.moveBy(const Offset(0.0, 90.0)); // Arm it.
        await tester.pump();
        expect(
1100
          CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))),
1101 1102 1103 1104 1105 1106 1107 1108 1109 1110
          RefreshIndicatorMode.armed,
        );

        await gesture.moveBy(const Offset(0.0, -80.0)); // Overscrolling, need to move more than -40.
        await tester.pump();
        expect(
          tester.getTopLeft(find.widgetWithText(Container, '0')).dy,
          moreOrLessEquals(49.775111111111116), // Below 50 now.
        );
        expect(
1111
          CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))),
1112 1113
          RefreshIndicatorMode.refresh,
        );
Dan Field's avatar
Dan Field committed
1114
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
1115 1116 1117 1118 1119

    testWidgets(
      'goes to done internally as soon as the task finishes',
      (WidgetTester tester) async {
        await tester.pumpWidget(
1120
          Directionality(
1121
            textDirection: TextDirection.ltr,
1122
            child: CustomScrollView(
1123
              slivers: <Widget>[
1124
                CupertinoSliverRefreshControl(
1125 1126
                  builder: builder,
                  onRefresh: onRefresh,
1127 1128 1129 1130 1131 1132 1133
                ),
                buildAListOfStuff(),
              ],
            ),
          ),
        );

1134
        await tester.drag(find.text('0'), const Offset(0.0, 100.0), touchSlopY: 0.0);
1135 1136
        await tester.pump();
        expect(
1137
          CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))),
1138 1139 1140 1141 1142 1143 1144
          RefreshIndicatorMode.armed,
        );
        // The sliver scroll offset correction is applied on the next frame.
        await tester.pump();

        await tester.pump(const Duration(seconds: 2));
        expect(
1145
          CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))),
1146 1147 1148 1149
          RefreshIndicatorMode.refresh,
        );
        expect(
          tester.getRect(find.widgetWithText(Container, '0')),
Dan Field's avatar
Dan Field committed
1150
          const Rect.fromLTRB(0.0, 60.0, 800.0, 260.0),
1151 1152 1153 1154 1155 1156 1157
        );

        refreshCompleter.complete(null);
        // The task completed between frames. The internal state goes to done
        // right away even though the sliver gets a new offset correction the
        // next frame.
        expect(
1158
          CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))),
1159 1160
          RefreshIndicatorMode.done,
        );
Dan Field's avatar
Dan Field committed
1161
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
1162 1163 1164 1165 1166

    testWidgets(
      'goes back to inactive when retracting back past 10% of arming distance',
      (WidgetTester tester) async {
        await tester.pumpWidget(
1167
          Directionality(
1168
            textDirection: TextDirection.ltr,
1169
            child: CustomScrollView(
1170
              slivers: <Widget>[
1171
                CupertinoSliverRefreshControl(
1172 1173
                  builder: builder,
                  onRefresh: onRefresh,
1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184
                ),
                buildAListOfStuff(),
              ],
            ),
          ),
        );

        final TestGesture gesture = await tester.startGesture(const Offset(0.0, 0.0));
        await gesture.moveBy(const Offset(0.0, 150.0));
        await tester.pump();
        expect(
1185
          CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))),
1186 1187 1188 1189 1190
          RefreshIndicatorMode.armed,
        );

        refreshCompleter.complete(null);
        expect(
1191
          CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))),
1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204
          RefreshIndicatorMode.done,
        );
        await tester.pump();

        // Now back in overscroll mode.
        await gesture.moveBy(const Offset(0.0, -200.0));
        await tester.pump();
        expect(
          tester.getTopLeft(find.widgetWithText(Container, '0')).dy,
          moreOrLessEquals(27.944444444444457),
        );
        // Need to bring it to 100 * 0.1 to reset to inactive.
        expect(
1205
          CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))),
1206 1207 1208 1209 1210 1211 1212 1213 1214 1215
          RefreshIndicatorMode.done,
        );

        await gesture.moveBy(const Offset(0.0, -35.0));
        await tester.pump();
        expect(
          tester.getTopLeft(find.widgetWithText(Container, '0')).dy,
          moreOrLessEquals(9.313890708161875),
        );
        expect(
1216
          CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))),
1217 1218
          RefreshIndicatorMode.inactive,
        );
Dan Field's avatar
Dan Field committed
1219
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
1220 1221 1222 1223 1224

    testWidgets(
      'goes back to inactive if already scrolled away when task completes',
      (WidgetTester tester) async {
        await tester.pumpWidget(
1225
          Directionality(
1226
            textDirection: TextDirection.ltr,
1227
            child: CustomScrollView(
1228
              slivers: <Widget>[
1229
                CupertinoSliverRefreshControl(
1230 1231
                  builder: builder,
                  onRefresh: onRefresh,
1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242
                ),
                buildAListOfStuff(),
              ],
            ),
          ),
        );

        final TestGesture gesture = await tester.startGesture(const Offset(0.0, 0.0));
        await gesture.moveBy(const Offset(0.0, 150.0));
        await tester.pump();
        expect(
1243
          CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))),
1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255
          RefreshIndicatorMode.armed,
        );
        await tester.pump(); // Sliver scroll offset correction is applied one frame later.

        await gesture.moveBy(const Offset(0.0, -300.0));
        await tester.pump();
        // The refresh indicator is offscreen now.
        expect(
          tester.getTopLeft(find.widgetWithText(Container, '0')).dy,
          moreOrLessEquals(-145.0332383665717),
        );
        expect(
1256
          CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder, skipOffstage: false))),
1257 1258 1259 1260 1261 1262 1263
          RefreshIndicatorMode.refresh,
        );

        refreshCompleter.complete(null);
        // The sliver layout extent is removed on next frame.
        await tester.pump();
        expect(
1264
          CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder, skipOffstage: false))),
1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277
          RefreshIndicatorMode.inactive,
        );
        // Nothing moved.
        expect(
          tester.getTopLeft(find.widgetWithText(Container, '0')).dy,
          moreOrLessEquals(-145.0332383665717),
        );
        await tester.pump(const Duration(seconds: 2));
        // Everything stayed as is.
        expect(
          tester.getTopLeft(find.widgetWithText(Container, '0')).dy,
          moreOrLessEquals(-145.0332383665717),
        );
Dan Field's avatar
Dan Field committed
1278
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
1279 1280 1281 1282

    testWidgets(
      "don't have to build any indicators or occupy space during refresh",
      (WidgetTester tester) async {
1283
        refreshIndicator = const Center(child: Text('-1'));
1284 1285

        await tester.pumpWidget(
1286
          Directionality(
1287
            textDirection: TextDirection.ltr,
1288
            child: CustomScrollView(
1289
              slivers: <Widget>[
1290
                CupertinoSliverRefreshControl(
1291
                  builder: null,
1292
                  onRefresh: onRefresh,
1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303
                  refreshIndicatorExtent: 0.0,
                ),
                buildAListOfStuff(),
              ],
            ),
          ),
        );

        await tester.drag(find.text('0'), const Offset(0.0, 150.0));
        await tester.pump();
        expect(
1304
          CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))),
1305 1306 1307 1308 1309 1310 1311
          RefreshIndicatorMode.armed,
        );

        await tester.pump();
        await tester.pump(const Duration(seconds: 5));
        // In refresh mode but has no UI.
        expect(
1312
          CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder, skipOffstage: false))),
1313 1314 1315 1316
          RefreshIndicatorMode.refresh,
        );
        expect(
          tester.getRect(find.widgetWithText(Center, '0')),
Dan Field's avatar
Dan Field committed
1317
          const Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
1318 1319 1320 1321 1322 1323 1324
        );
        verify(mockHelper.refreshTask()); // The refresh function still called.

        refreshCompleter.complete(null);
        await tester.pump();
        // Goes to inactive right away since the sliver is already collapsed.
        expect(
1325
          CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder, skipOffstage: false))),
1326 1327
          RefreshIndicatorMode.inactive,
        );
Dan Field's avatar
Dan Field committed
1328
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372

    testWidgets('buildSimpleRefreshIndicator dark mode', (WidgetTester tester) async {
      const CupertinoDynamicColor color = CupertinoColors.inactiveGray;

      await tester.pumpWidget(
        MediaQuery(
          data: const MediaQueryData(platformBrightness: Brightness.light),
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Builder(
              builder: (BuildContext context) {
                return CupertinoSliverRefreshControl.buildSimpleRefreshIndicator(
                  context,
                  RefreshIndicatorMode.drag,
                  10, 10, 10,
                );
              },
            ),
          ),
        ),
      );

      expect(tester.widget<Icon>(find.byType(Icon)).color.value, color.color.value);

      await tester.pumpWidget(
        MediaQuery(
          data: const MediaQueryData(platformBrightness: Brightness.dark),
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Builder(
              builder: (BuildContext context) {
                return CupertinoSliverRefreshControl.buildSimpleRefreshIndicator(
                  context,
                  RefreshIndicatorMode.drag,
                  10, 10, 10,
                );
              },
            ),
          ),
        ),
      );

      expect(tester.widget<Icon>(find.byType(Icon)).color.value, color.darkColor.value);
    });
1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388
  };

  group('UI tests long list', uiTestGroup);

  // Test the internal state machine directly to make sure the UI aren't just
  // correct by coincidence.
  group('state machine test long list', stateMachineTestGroup);

  // Retest everything and make sure that it still works when the whole list
  // is smaller than the viewport size.
  testListLength = 2;
  group('UI tests short list', uiTestGroup);

  // Test the internal state machine directly to make sure the UI aren't just
  // correct by coincidence.
  group('state machine test short list', stateMachineTestGroup);
1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418

  testWidgets(
    'Does not crash when paintExtent > remainingPaintExtent',
    (WidgetTester tester) async {
      // Regression test for https://github.com/flutter/flutter/issues/46871.
      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: CustomScrollView(
            physics: const BouncingScrollPhysics(),
            slivers: <Widget>[
              const CupertinoSliverRefreshControl(),
              SliverList(
                delegate: SliverChildBuilderDelegate(
                  (BuildContext context, int index) => const SizedBox(height: 100),
                  childCount: 20,
                ),
              ),
            ],
          ),
        ),
      );

      // Drag the content down far enough so that
      // geometry.paintExent > constraints.maxPaintExtent
      await tester.dragFrom(const Offset(10, 10), const Offset(0, 500));
      await tester.pump();

      expect(tester.takeException(), isNull);
  });
1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430
}

class MockHelper extends Mock {
  Widget builder(
    BuildContext context,
    RefreshIndicatorMode refreshState,
    double pulledExtent,
    double refreshTriggerPullDistance,
    double refreshIndicatorExtent,
  );

  Future<void> refreshTask();
1431
}