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

5 6
// @dart = 2.8

7 8
import 'dart:async';

9
import 'package:flutter/foundation.dart';
10 11 12
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';

13
bool refreshCalled = false;
14

15
Future<void> refresh() {
16
  refreshCalled = true;
17
  return Future<void>.value();
18
}
19

20
Future<void> holdRefresh() {
21
  refreshCalled = true;
22
  return Completer<void>().future;
23 24 25
}

void main() {
26
  testWidgets('RefreshIndicator', (WidgetTester tester) async {
27
    refreshCalled = false;
28
    final SemanticsHandle handle = tester.ensureSemantics();
29
    await tester.pumpWidget(
30 31
      MaterialApp(
        home: RefreshIndicator(
32
          onRefresh: refresh,
33
          child: ListView(
34
            physics: const AlwaysScrollableScrollPhysics(),
35
            children: <String>['A', 'B', 'C', 'D', 'E', 'F'].map<Widget>((String item) {
36
              return SizedBox(
37
                height: 200.0,
38
                child: Text(item),
39 40 41
              );
            }).toList(),
          ),
42 43
        ),
      ),
44
    );
45

46
    await tester.fling(find.text('A'), const Offset(0.0, 300.0), 1000.0);
47
    await tester.pump();
48 49 50 51 52

    expect(tester.getSemantics(find.byType(RefreshProgressIndicator)), matchesSemantics(
      label: 'Refresh',
    ));

53 54 55
    await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
    await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation
    await tester.pump(const Duration(seconds: 1)); // finish the indicator hide animation
56
    expect(refreshCalled, true);
57
    handle.dispose();
58
  });
59

60 61 62
  testWidgets('Refresh Indicator - nested', (WidgetTester tester) async {
    refreshCalled = false;
    await tester.pumpWidget(
63 64
      MaterialApp(
        home: RefreshIndicator(
65 66
          notificationPredicate: (ScrollNotification notification) => notification.depth == 1,
          onRefresh: refresh,
67
          child: SingleChildScrollView(
68
            scrollDirection: Axis.horizontal,
69
            child: Container(
70
              width: 600.0,
71
              child: ListView(
72
                physics: const AlwaysScrollableScrollPhysics(),
73
                children: <String>['A', 'B', 'C', 'D', 'E', 'F'].map<Widget>((String item) {
74
                  return SizedBox(
75
                    height: 200.0,
76
                    child: Text(item),
77 78 79 80 81 82 83 84
                  );
                }).toList(),
              ),
            ),
          ),
        ),
      ),
    );
85

86 87 88 89 90
    await tester.fling(find.text('A'), const Offset(300.0, 0.0), 1000.0); // horizontal fling
    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
    await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation
    await tester.pump(const Duration(seconds: 1)); // finish the indicator hide animation
91 92
    expect(refreshCalled, false);

93 94 95 96 97 98

    await tester.fling(find.text('A'), const Offset(0.0, 300.0), 1000.0); // vertical fling
    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
    await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation
    await tester.pump(const Duration(seconds: 1)); // finish the indicator hide animation
99
    expect(refreshCalled, true);
100
  });
101 102 103 104

  testWidgets('RefreshIndicator - bottom', (WidgetTester tester) async {
    refreshCalled = false;
    await tester.pumpWidget(
105 106
      MaterialApp(
        home: RefreshIndicator(
107
          onRefresh: refresh,
108
          child: ListView(
109 110
            reverse: true,
            physics: const AlwaysScrollableScrollPhysics(),
111
            children: const <Widget>[
112
              SizedBox(
113
                height: 200.0,
114
                child: Text('X'),
115 116 117
              ),
            ],
          ),
118 119 120 121 122 123 124 125 126 127 128 129
        ),
      ),
    );

    await tester.fling(find.text('X'), const Offset(0.0, -300.0), 1000.0);
    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
    await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation
    await tester.pump(const Duration(seconds: 1)); // finish the indicator hide animation
    expect(refreshCalled, true);
  });

130 131 132
  testWidgets('RefreshIndicator - top - position', (WidgetTester tester) async {
    refreshCalled = false;
    await tester.pumpWidget(
133 134
      MaterialApp(
        home: RefreshIndicator(
135
          onRefresh: holdRefresh,
136
          child: ListView(
137
            physics: const AlwaysScrollableScrollPhysics(),
138
            children: const <Widget>[
139
              SizedBox(
140
                height: 200.0,
141
                child: Text('X'),
142 143 144
              ),
            ],
          ),
145 146 147 148
        ),
      ),
    );

149
    await tester.fling(find.text('X'), const Offset(0.0, 300.0), 1000.0);
150 151 152
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
153
    expect(tester.getCenter(find.byType(RefreshProgressIndicator)).dy, lessThan(300.0));
154 155 156 157 158
  });

  testWidgets('RefreshIndicator - bottom - position', (WidgetTester tester) async {
    refreshCalled = false;
    await tester.pumpWidget(
159 160
      MaterialApp(
        home: RefreshIndicator(
161
          onRefresh: holdRefresh,
162
          child: ListView(
163 164
            reverse: true,
            physics: const AlwaysScrollableScrollPhysics(),
165
            children: const <Widget>[
166
              SizedBox(
167
                height: 200.0,
168
                child: Text('X'),
169 170 171
              ),
            ],
          ),
172 173 174 175 176 177 178 179
        ),
      ),
    );

    await tester.fling(find.text('X'), const Offset(0.0, -300.0), 1000.0);
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
180
    expect(tester.getCenter(find.byType(RefreshProgressIndicator)).dy, greaterThan(300.0));
181 182 183 184 185
  });

  testWidgets('RefreshIndicator - no movement', (WidgetTester tester) async {
    refreshCalled = false;
    await tester.pumpWidget(
186 187
      MaterialApp(
        home: RefreshIndicator(
188
          onRefresh: refresh,
189
          child: ListView(
190
            physics: const AlwaysScrollableScrollPhysics(),
191
            children: const <Widget>[
192
              SizedBox(
193
                height: 200.0,
194
                child: Text('X'),
195 196 197
              ),
            ],
          ),
198 199 200 201 202 203 204 205 206 207 208 209 210
        ),
      ),
    );

    // this fling is horizontal, not up or down
    await tester.fling(find.text('X'), const Offset(1.0, 0.0), 1000.0);
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    expect(refreshCalled, false);
  });

211 212 213
  testWidgets('RefreshIndicator - not enough', (WidgetTester tester) async {
    refreshCalled = false;
    await tester.pumpWidget(
214 215
      MaterialApp(
        home: RefreshIndicator(
216
          onRefresh: refresh,
217
          child: ListView(
218
            physics: const AlwaysScrollableScrollPhysics(),
219
            children: const <Widget>[
220
              SizedBox(
221
                height: 200.0,
222
                child: Text('X'),
223 224 225
              ),
            ],
          ),
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
        ),
      ),
    );

    await tester.fling(find.text('X'), const Offset(0.0, 100.0), 1000.0);
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    expect(refreshCalled, false);
  });

  testWidgets('RefreshIndicator - show - slow', (WidgetTester tester) async {
    refreshCalled = false;
    await tester.pumpWidget(
241 242
      MaterialApp(
        home: RefreshIndicator(
243
          onRefresh: holdRefresh, // this one never returns
244
          child: ListView(
245
            physics: const AlwaysScrollableScrollPhysics(),
246
            children: const <Widget>[
247
              SizedBox(
248
                height: 200.0,
249
                child: Text('X'),
250 251 252
              ),
            ],
          ),
253 254 255 256 257 258 259
        ),
      ),
    );

    bool completed = false;
    tester.state<RefreshIndicatorState>(find.byType(RefreshIndicator))
      .show()
260
      .then<void>((void value) { completed = true; });
261 262 263 264 265 266 267 268 269 270 271
    await tester.pump();
    expect(completed, false);
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    expect(refreshCalled, true);
    expect(completed, false);
    completed = false;
    refreshCalled = false;
    tester.state<RefreshIndicatorState>(find.byType(RefreshIndicator))
      .show()
272
      .then<void>((void value) { completed = true; });
273 274 275 276 277 278 279 280 281 282 283
    await tester.pump();
    expect(completed, false);
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    expect(refreshCalled, false);
  });

  testWidgets('RefreshIndicator - show - fast', (WidgetTester tester) async {
    refreshCalled = false;
    await tester.pumpWidget(
284 285
      MaterialApp(
        home: RefreshIndicator(
286
          onRefresh: refresh,
287
          child: ListView(
288
            physics: const AlwaysScrollableScrollPhysics(),
289
            children: const <Widget>[
290
              SizedBox(
291
                height: 200.0,
292
                child: Text('X'),
293 294 295
              ),
            ],
          ),
296 297 298 299 300 301 302
        ),
      ),
    );

    bool completed = false;
    tester.state<RefreshIndicatorState>(find.byType(RefreshIndicator))
      .show()
303
      .then<void>((void value) { completed = true; });
304 305 306 307 308 309 310 311 312 313 314
    await tester.pump();
    expect(completed, false);
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    expect(refreshCalled, true);
    expect(completed, true);
    completed = false;
    refreshCalled = false;
    tester.state<RefreshIndicatorState>(find.byType(RefreshIndicator))
      .show()
315
      .then<void>((void value) { completed = true; });
316 317 318 319 320 321 322 323 324 325 326 327
    await tester.pump();
    expect(completed, false);
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    expect(refreshCalled, true);
    expect(completed, true);
  });

  testWidgets('RefreshIndicator - show - fast - twice', (WidgetTester tester) async {
    refreshCalled = false;
    await tester.pumpWidget(
328 329
      MaterialApp(
        home: RefreshIndicator(
330
          onRefresh: refresh,
331
          child: ListView(
332
            physics: const AlwaysScrollableScrollPhysics(),
333
            children: const <Widget>[
334
              SizedBox(
335
                height: 200.0,
336
                child: Text('X'),
337 338 339
              ),
            ],
          ),
340 341 342 343 344 345 346
        ),
      ),
    );

    bool completed1 = false;
    tester.state<RefreshIndicatorState>(find.byType(RefreshIndicator))
      .show()
347
      .then<void>((void value) { completed1 = true; });
348 349 350
    bool completed2 = false;
    tester.state<RefreshIndicatorState>(find.byType(RefreshIndicator))
      .show()
351
      .then<void>((void value) { completed2 = true; });
352 353 354 355 356 357 358 359 360 361
    await tester.pump();
    expect(completed1, false);
    expect(completed2, false);
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    expect(refreshCalled, true);
    expect(completed1, true);
    expect(completed2, true);
  });
362 363 364 365

  testWidgets('RefreshIndicator - onRefresh asserts', (WidgetTester tester) async {
    refreshCalled = false;
    await tester.pumpWidget(
366 367
      MaterialApp(
        home: RefreshIndicator(
368 369
          onRefresh: () {
            refreshCalled = true;
370
            return null; // Missing a returned Future value here, should cause framework to throw.
371
          },
372
          child: ListView(
373
            physics: const AlwaysScrollableScrollPhysics(),
374
            children: <String>['A', 'B', 'C', 'D', 'E', 'F'].map<Widget>((String item) {
375
              return SizedBox(
376
                height: 200.0,
377
                child: Text(item),
378 379 380 381 382 383 384 385 386 387 388 389 390
              );
            }).toList(),
          ),
        ),
      ),
    );

    await tester.fling(find.text('A'), const Offset(0.0, 300.0), 1000.0);
    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
    expect(refreshCalled, true);
    expect(tester.takeException(), isFlutterError);
  });
391

Dan Field's avatar
Dan Field committed
392
  testWidgets('Refresh starts while scroll view moves back to 0.0 after overscroll', (WidgetTester tester) async {
393 394
    refreshCalled = false;
    double lastScrollOffset;
395
    final ScrollController controller = ScrollController();
396 397

    await tester.pumpWidget(
398 399
      MaterialApp(
        home: RefreshIndicator(
400
          onRefresh: refresh,
401
          child: ListView(
402 403
            controller: controller,
            physics: const AlwaysScrollableScrollPhysics(),
404
            children: <String>['A', 'B', 'C', 'D', 'E', 'F'].map<Widget>((String item) {
405
              return SizedBox(
406
                height: 200.0,
407
                child: Text(item),
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
              );
            }).toList(),
          ),
        ),
      ),
    );

    await tester.fling(find.text('A'), const Offset(0.0, 300.0), 1000.0);
    await tester.pump(const Duration(milliseconds: 100));
    expect(lastScrollOffset = controller.offset, lessThan(0.0));
    expect(refreshCalled, isFalse);

    await tester.pump(const Duration(milliseconds: 100));
    expect(controller.offset, greaterThan(lastScrollOffset));
    expect(controller.offset, lessThan(0.0));
    expect(refreshCalled, isTrue);
Dan Field's avatar
Dan Field committed
424
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459

  testWidgets('RefreshIndicator does not force child to relayout', (WidgetTester tester) async {
    int layoutCount = 0;

    Widget layoutCallback(BuildContext context, BoxConstraints constraints) {
      layoutCount++;
      return ListView(
        physics: const AlwaysScrollableScrollPhysics(),
        children: <String>['A', 'B', 'C', 'D', 'E', 'F'].map<Widget>((String item) {
          return SizedBox(
            height: 200.0,
            child: Text(item),
          );
        }).toList(),
      );
    }

    await tester.pumpWidget(
      MaterialApp(
        home: RefreshIndicator(
          onRefresh: refresh,
          child: LayoutBuilder(builder: layoutCallback),
        ),
      ),
    );

    await tester.fling(find.text('A'), const Offset(0.0, 300.0), 1000.0); // trigger refresh
    await tester.pump();

    await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
    await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation
    await tester.pump(const Duration(seconds: 1)); // finish the indicator hide animation

    expect(layoutCount, 1);
  });
460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533

  testWidgets('strokeWidth cannot be null in RefreshIndicator', (WidgetTester tester) async {
    try {
      await tester.pumpWidget(
          MaterialApp(
            home: RefreshIndicator(
              onRefresh: () async {},
              strokeWidth: null,
              child: ListView(
                physics: const AlwaysScrollableScrollPhysics(),
                children: <String>['A', 'B', 'C', 'D', 'E', 'F'].map<Widget>((String item) {
                  return SizedBox(
                    height: 200.0,
                    child: Text(item),
                  );
                }).toList(),
              ),
            ),
          )
      );
    } on AssertionError catch(_) {
      return;
    }
    fail('The assertion was not thrown when strokeWidth was null');
  });

  testWidgets('RefreshIndicator responds to strokeWidth', (WidgetTester tester) async {
    await tester.pumpWidget(
        MaterialApp(
          home: RefreshIndicator(
            onRefresh: () async {},
            child: ListView(
              physics: const AlwaysScrollableScrollPhysics(),
              children: <String>['A', 'B', 'C', 'D', 'E', 'F'].map<Widget>((String item) {
                return SizedBox(
                  height: 200.0,
                  child: Text(item),
                );
              }).toList(),
            ),
          ),
        )
    );

    //By default the value of strokeWidth is 2.0
    expect(
        tester.widget<RefreshIndicator>(find.byType(RefreshIndicator)).strokeWidth,
        2.0,
    );

    await tester.pumpWidget(
        MaterialApp(
          home: RefreshIndicator(
            onRefresh: () async {},
            strokeWidth: 4.0,
            child: ListView(
              physics: const AlwaysScrollableScrollPhysics(),
              children: <String>['A', 'B', 'C', 'D', 'E', 'F'].map<Widget>((String item) {
                return SizedBox(
                  height: 200.0,
                  child: Text(item),
                );
              }).toList(),
            ),
          ),
        )
    );

    expect(
        tester.widget<RefreshIndicator>(find.byType(RefreshIndicator)).strokeWidth,
        4.0,
    );
  });
}