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

import 'dart:async';

7
import 'package:flutter/foundation.dart';
8 9
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
10
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
11

12
void main() {
13
  Widget snapshotText(BuildContext context, AsyncSnapshot<String> snapshot) {
14
    return Text(snapshot.toString(), textDirection: TextDirection.ltr);
15
  }
16
  group('AsyncSnapshot', () {
17 18 19 20 21 22 23 24 25 26 27 28 29 30
    test('requiring data preserves the stackTrace', () {
      final StackTrace originalStackTrace = StackTrace.current;

      try {
        AsyncSnapshot<String>.withError(
          ConnectionState.done,
          Error(),
          originalStackTrace,
        ).requireData;
        fail('requireData did not throw');
      } catch (error, stackTrace) {
        expect(stackTrace, originalStackTrace);
      }
    });
31 32 33 34 35
    test('requiring data succeeds if data is present', () {
      expect(
        const AsyncSnapshot<String>.withData(ConnectionState.done, 'hello').requireData,
        'hello',
      );
36
    });
37 38 39 40 41
    test('requiring data fails if there is an error', () {
      expect(
        () => const AsyncSnapshot<String>.withError(ConnectionState.done, 'error').requireData,
        throwsA(equals('error')),
      );
42
    });
43 44 45 46 47
    test('requiring data fails if snapshot has neither data nor error', () {
      expect(
        () => const AsyncSnapshot<String>.nothing().requireData,
        throwsStateError,
      );
48
    });
49 50 51 52
    test('AsyncSnapshot basic constructors', () {
      expect(const AsyncSnapshot<int>.nothing().connectionState, ConnectionState.none);
      expect(const AsyncSnapshot<int>.nothing().data, isNull);
      expect(const AsyncSnapshot<int>.nothing().error, isNull);
53
      expect(const AsyncSnapshot<int>.nothing().stackTrace, isNull);
54 55 56
      expect(const AsyncSnapshot<int>.waiting().connectionState, ConnectionState.waiting);
      expect(const AsyncSnapshot<int>.waiting().data, isNull);
      expect(const AsyncSnapshot<int>.waiting().error, isNull);
57 58 59 60 61 62 63
      expect(const AsyncSnapshot<int>.waiting().stackTrace, isNull);
    });
    test('withError uses empty stack trace if no stack trace is specified', () {
      // We need to store the error as a local variable in order for the
      // equality check on the error to be true.
      final Error error = Error();
      expect(
64
        AsyncSnapshot<int>.withError(ConnectionState.done, error),
65
        AsyncSnapshot<int>.withError(ConnectionState.done, error),
66
      );
67
    });
68
  });
69
  group('Async smoke tests', () {
70
    testWidgetsWithLeakTracking('FutureBuilder', (WidgetTester tester) async {
71 72
      await tester.pumpWidget(FutureBuilder<String>(
        future: Future<String>.value('hello'),
73
        builder: snapshotText,
74 75 76
      ));
      await eventFiring(tester);
    });
77
    testWidgetsWithLeakTracking('StreamBuilder', (WidgetTester tester) async {
78
      await tester.pumpWidget(StreamBuilder<String>(
79
        stream: Stream<String>.fromIterable(<String>['hello', 'world']),
80
        builder: snapshotText,
81 82 83
      ));
      await eventFiring(tester);
    });
84
    testWidgetsWithLeakTracking('StreamFold', (WidgetTester tester) async {
85
      await tester.pumpWidget(StringCollector(
86
        stream: Stream<String>.fromIterable(<String>['hello', 'world']),
87 88 89 90 91
      ));
      await eventFiring(tester);
    });
  });
  group('FutureBuilder', () {
92
    testWidgetsWithLeakTracking('gives expected snapshot with SynchronousFuture', (WidgetTester tester) async {
93 94 95 96 97 98 99 100 101 102 103 104 105
      final SynchronousFuture<String> future = SynchronousFuture<String>('flutter');
      await tester.pumpWidget(FutureBuilder<String>(
        future: future,
        builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
          expect(snapshot.connectionState, ConnectionState.done);
          expect(snapshot.data, 'flutter');
          expect(snapshot.error, null);
          expect(snapshot.stackTrace, null);
          return const Placeholder();
        },
      ));
    });

106
    testWidgetsWithLeakTracking('gracefully handles transition from null future', (WidgetTester tester) async {
107 108
      final GlobalKey key = GlobalKey();
      await tester.pumpWidget(FutureBuilder<String>(
109
        key: key, builder: snapshotText, future: null,
110
      ));
111
      expect(find.text('AsyncSnapshot<String>(ConnectionState.none, null, null, null)'), findsOneWidget);
112 113
      final Completer<String> completer = Completer<String>();
      await tester.pumpWidget(FutureBuilder<String>(
114
        key: key, future: completer.future, builder: snapshotText,
115
      ));
116
      expect(find.text('AsyncSnapshot<String>(ConnectionState.waiting, null, null, null)'), findsOneWidget);
117
    });
118
    testWidgetsWithLeakTracking('gracefully handles transition to null future', (WidgetTester tester) async {
119 120 121
      final GlobalKey key = GlobalKey();
      final Completer<String> completer = Completer<String>();
      await tester.pumpWidget(FutureBuilder<String>(
122
        key: key, future: completer.future, builder: snapshotText,
123
      ));
124
      expect(find.text('AsyncSnapshot<String>(ConnectionState.waiting, null, null, null)'), findsOneWidget);
125
      await tester.pumpWidget(FutureBuilder<String>(
126
        key: key, builder: snapshotText, future: null,
127
      ));
128
      expect(find.text('AsyncSnapshot<String>(ConnectionState.none, null, null, null)'), findsOneWidget);
129 130
      completer.complete('hello');
      await eventFiring(tester);
131
      expect(find.text('AsyncSnapshot<String>(ConnectionState.none, null, null, null)'), findsOneWidget);
132
    });
133
    testWidgetsWithLeakTracking('gracefully handles transition to other future', (WidgetTester tester) async {
134 135 136 137
      final GlobalKey key = GlobalKey();
      final Completer<String> completerA = Completer<String>();
      final Completer<String> completerB = Completer<String>();
      await tester.pumpWidget(FutureBuilder<String>(
138
        key: key, future: completerA.future, builder: snapshotText,
139
      ));
140
      expect(find.text('AsyncSnapshot<String>(ConnectionState.waiting, null, null, null)'), findsOneWidget);
141
      await tester.pumpWidget(FutureBuilder<String>(
142
        key: key, future: completerB.future, builder: snapshotText,
143
      ));
144
      expect(find.text('AsyncSnapshot<String>(ConnectionState.waiting, null, null, null)'), findsOneWidget);
145 146 147
      completerB.complete('B');
      completerA.complete('A');
      await eventFiring(tester);
148
      expect(find.text('AsyncSnapshot<String>(ConnectionState.done, B, null, null)'), findsOneWidget);
149
    });
150
    testWidgetsWithLeakTracking('tracks life-cycle of Future to success', (WidgetTester tester) async {
151 152
      final Completer<String> completer = Completer<String>();
      await tester.pumpWidget(FutureBuilder<String>(
153
        future: completer.future, builder: snapshotText,
154
      ));
155
      expect(find.text('AsyncSnapshot<String>(ConnectionState.waiting, null, null, null)'), findsOneWidget);
156 157
      completer.complete('hello');
      await eventFiring(tester);
158
      expect(find.text('AsyncSnapshot<String>(ConnectionState.done, hello, null, null)'), findsOneWidget);
159
    });
160
    testWidgetsWithLeakTracking('tracks life-cycle of Future to error', (WidgetTester tester) async {
161 162
      final Completer<String> completer = Completer<String>();
      await tester.pumpWidget(FutureBuilder<String>(
163
        future: completer.future, builder: snapshotText,
164
      ));
165 166
      expect(find.text('AsyncSnapshot<String>(ConnectionState.waiting, null, null, null)'), findsOneWidget);
      completer.completeError('bad', StackTrace.fromString('trace'));
167
      await eventFiring(tester);
168
      expect(find.text('AsyncSnapshot<String>(ConnectionState.done, null, bad, trace)'), findsOneWidget);
169
    });
170
    testWidgetsWithLeakTracking('runs the builder using given initial data', (WidgetTester tester) async {
171
      final GlobalKey key = GlobalKey();
172
      await tester.pumpWidget(FutureBuilder<String>(
173
        key: key,
174
        future: null,
175 176
        builder: snapshotText,
        initialData: 'I',
177
      ));
178
      expect(find.text('AsyncSnapshot<String>(ConnectionState.none, I, null, null)'), findsOneWidget);
179
    });
180
    testWidgetsWithLeakTracking('ignores initialData when reconfiguring', (WidgetTester tester) async {
181 182 183
      final GlobalKey key = GlobalKey();
      await tester.pumpWidget(FutureBuilder<String>(
        key: key,
184
        future: null,
185 186
        builder: snapshotText,
        initialData: 'I',
187
      ));
188
      expect(find.text('AsyncSnapshot<String>(ConnectionState.none, I, null, null)'), findsOneWidget);
189 190 191 192 193 194 195
      final Completer<String> completer = Completer<String>();
      await tester.pumpWidget(FutureBuilder<String>(
        key: key,
        future: completer.future,
        builder: snapshotText,
        initialData: 'Ignored',
      ));
196
      expect(find.text('AsyncSnapshot<String>(ConnectionState.waiting, I, null, null)'), findsOneWidget);
197
    });
198
    testWidgetsWithLeakTracking('debugRethrowError rethrows caught error', (WidgetTester tester) async {
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
      FutureBuilder.debugRethrowError = true;
      final Completer<void> caughtError = Completer<void>();
      await runZonedGuarded(() async {
        final Completer<String> completer = Completer<String>();
        await tester.pumpWidget(FutureBuilder<String>(
          future: completer.future,
          builder: snapshotText,
        ), const Duration(seconds: 1));
        completer.completeError('bad');
      }, (Object error, StackTrace stack) {
        expectSync(error, equals('bad'));
        caughtError.complete();
      });
      await tester.pumpAndSettle();
      expectSync(caughtError.isCompleted, isTrue);
      FutureBuilder.debugRethrowError = false;
    });
216 217
  });
  group('StreamBuilder', () {
218
    testWidgetsWithLeakTracking('gracefully handles transition from null stream', (WidgetTester tester) async {
219
      final GlobalKey key = GlobalKey();
220
      await tester.pumpWidget(StreamBuilder<String>(
221
        key: key, builder: snapshotText, stream: null,
222
      ));
223
      expect(find.text('AsyncSnapshot<String>(ConnectionState.none, null, null, null)'), findsOneWidget);
224
      final StreamController<String> controller = StreamController<String>();
225
      await tester.pumpWidget(StreamBuilder<String>(
226
        key: key, stream: controller.stream, builder: snapshotText,
227
      ));
228
      expect(find.text('AsyncSnapshot<String>(ConnectionState.waiting, null, null, null)'), findsOneWidget);
229
    });
230
    testWidgetsWithLeakTracking('gracefully handles transition to null stream', (WidgetTester tester) async {
231 232
      final GlobalKey key = GlobalKey();
      final StreamController<String> controller = StreamController<String>();
233
      await tester.pumpWidget(StreamBuilder<String>(
234
        key: key, stream: controller.stream, builder: snapshotText,
235
      ));
236
      expect(find.text('AsyncSnapshot<String>(ConnectionState.waiting, null, null, null)'), findsOneWidget);
237
      await tester.pumpWidget(StreamBuilder<String>(
238
        key: key, builder: snapshotText, stream: null,
239
      ));
240
      expect(find.text('AsyncSnapshot<String>(ConnectionState.none, null, null, null)'), findsOneWidget);
241
    });
242
    testWidgetsWithLeakTracking('gracefully handles transition to other stream', (WidgetTester tester) async {
243 244 245
      final GlobalKey key = GlobalKey();
      final StreamController<String> controllerA = StreamController<String>();
      final StreamController<String> controllerB = StreamController<String>();
246
      await tester.pumpWidget(StreamBuilder<String>(
247
        key: key, stream: controllerA.stream, builder: snapshotText,
248
      ));
249
      expect(find.text('AsyncSnapshot<String>(ConnectionState.waiting, null, null, null)'), findsOneWidget);
250
      await tester.pumpWidget(StreamBuilder<String>(
251
        key: key, stream: controllerB.stream, builder: snapshotText,
252 253 254 255
      ));
      controllerB.add('B');
      controllerA.add('A');
      await eventFiring(tester);
256
      expect(find.text('AsyncSnapshot<String>(ConnectionState.active, B, null, null)'), findsOneWidget);
257
    });
258
    testWidgetsWithLeakTracking('tracks events and errors of stream until completion', (WidgetTester tester) async {
259 260
      final GlobalKey key = GlobalKey();
      final StreamController<String> controller = StreamController<String>();
261
      await tester.pumpWidget(StreamBuilder<String>(
262
        key: key, stream: controller.stream, builder: snapshotText,
263
      ));
264
      expect(find.text('AsyncSnapshot<String>(ConnectionState.waiting, null, null, null)'), findsOneWidget);
265 266 267
      controller.add('1');
      controller.add('2');
      await eventFiring(tester);
268
      expect(find.text('AsyncSnapshot<String>(ConnectionState.active, 2, null, null)'), findsOneWidget);
269
      controller.add('3');
270
      controller.addError('bad', StackTrace.fromString('trace'));
271
      await eventFiring(tester);
272
      expect(find.text('AsyncSnapshot<String>(ConnectionState.active, null, bad, trace)'), findsOneWidget);
273 274 275
      controller.add('4');
      controller.close();
      await eventFiring(tester);
276
      expect(find.text('AsyncSnapshot<String>(ConnectionState.done, 4, null, null)'), findsOneWidget);
277
    });
278
    testWidgetsWithLeakTracking('runs the builder using given initial data', (WidgetTester tester) async {
279 280
      final StreamController<String> controller = StreamController<String>();
      await tester.pumpWidget(StreamBuilder<String>(
281 282 283 284
        stream: controller.stream,
        builder: snapshotText,
        initialData: 'I',
      ));
285
      expect(find.text('AsyncSnapshot<String>(ConnectionState.waiting, I, null, null)'), findsOneWidget);
286
    });
287
    testWidgetsWithLeakTracking('ignores initialData when reconfiguring', (WidgetTester tester) async {
288 289
      final GlobalKey key = GlobalKey();
      await tester.pumpWidget(StreamBuilder<String>(
290
        key: key,
291
        stream: null,
292 293 294
        builder: snapshotText,
        initialData: 'I',
      ));
295
      expect(find.text('AsyncSnapshot<String>(ConnectionState.none, I, null, null)'), findsOneWidget);
296 297
      final StreamController<String> controller = StreamController<String>();
      await tester.pumpWidget(StreamBuilder<String>(
298 299 300 301 302
        key: key,
        stream: controller.stream,
        builder: snapshotText,
        initialData: 'Ignored',
      ));
303
      expect(find.text('AsyncSnapshot<String>(ConnectionState.waiting, I, null, null)'), findsOneWidget);
304
    });
305 306
  });
  group('FutureBuilder and StreamBuilder behave identically on Stream from Future', () {
307
    testWidgetsWithLeakTracking('when completing with data', (WidgetTester tester) async {
308 309 310
      final Completer<String> completer = Completer<String>();
      await tester.pumpWidget(Column(children: <Widget>[
        FutureBuilder<String>(future: completer.future, builder: snapshotText),
311
        StreamBuilder<String>(stream: completer.future.asStream(), builder: snapshotText),
312
      ]));
313
      expect(find.text('AsyncSnapshot<String>(ConnectionState.waiting, null, null, null)'), findsNWidgets(2));
314 315
      completer.complete('hello');
      await eventFiring(tester);
316 317
      expect(find.text('AsyncSnapshot<String>(ConnectionState.done, hello, null, null)'), findsNWidgets(2));
    });
318
    testWidgetsWithLeakTracking('when completing with error and with empty stack trace', (WidgetTester tester) async {
319 320 321 322 323 324 325 326 327
      final Completer<String> completer = Completer<String>();
      await tester.pumpWidget(Column(children: <Widget>[
        FutureBuilder<String>(future: completer.future, builder: snapshotText),
        StreamBuilder<String>(stream: completer.future.asStream(), builder: snapshotText),
      ]));
      expect(find.text('AsyncSnapshot<String>(ConnectionState.waiting, null, null, null)'), findsNWidgets(2));
      completer.completeError('bad', StackTrace.empty);
      await eventFiring(tester);
      expect(find.text('AsyncSnapshot<String>(ConnectionState.done, null, bad, )'), findsNWidgets(2));
328
    });
329
    testWidgetsWithLeakTracking('when completing with error and with stack trace', (WidgetTester tester) async {
330 331 332
      final Completer<String> completer = Completer<String>();
      await tester.pumpWidget(Column(children: <Widget>[
        FutureBuilder<String>(future: completer.future, builder: snapshotText),
333
        StreamBuilder<String>(stream: completer.future.asStream(), builder: snapshotText),
334
      ]));
335 336
      expect(find.text('AsyncSnapshot<String>(ConnectionState.waiting, null, null, null)'), findsNWidgets(2));
      completer.completeError('bad', StackTrace.fromString('trace'));
337
      await eventFiring(tester);
338
      expect(find.text('AsyncSnapshot<String>(ConnectionState.done, null, bad, trace)'), findsNWidgets(2));
339
    });
340
    testWidgetsWithLeakTracking('when Future is null', (WidgetTester tester) async {
341
      await tester.pumpWidget(Column(children: <Widget>[
342 343
        FutureBuilder<String>(builder: snapshotText, future: null),
        StreamBuilder<String>(builder: snapshotText, stream: null,),
344
      ]));
345
      expect(find.text('AsyncSnapshot<String>(ConnectionState.none, null, null, null)'), findsNWidgets(2));
346
    });
347
    testWidgetsWithLeakTracking('when initialData is used with null Future and Stream', (WidgetTester tester) async {
348
      await tester.pumpWidget(Column(children: <Widget>[
349 350
        FutureBuilder<String>(builder: snapshotText, initialData: 'I', future: null),
        StreamBuilder<String>(builder: snapshotText, initialData: 'I', stream: null),
351
      ]));
352
      expect(find.text('AsyncSnapshot<String>(ConnectionState.none, I, null, null)'), findsNWidgets(2));
353
    });
354
    testWidgetsWithLeakTracking('when using initialData and completing with data', (WidgetTester tester) async {
355 356 357 358 359
      final Completer<String> completer = Completer<String>();
      await tester.pumpWidget(Column(children: <Widget>[
        FutureBuilder<String>(future: completer.future, builder: snapshotText, initialData: 'I'),
        StreamBuilder<String>(stream: completer.future.asStream(), builder: snapshotText, initialData: 'I'),
      ]));
360
      expect(find.text('AsyncSnapshot<String>(ConnectionState.waiting, I, null, null)'), findsNWidgets(2));
361 362
      completer.complete('hello');
      await eventFiring(tester);
363
      expect(find.text('AsyncSnapshot<String>(ConnectionState.done, hello, null, null)'), findsNWidgets(2));
364
    });
365 366
  });
  group('StreamBuilderBase', () {
367
    testWidgetsWithLeakTracking('gracefully handles transition from null stream', (WidgetTester tester) async {
368
      final GlobalKey key = GlobalKey();
369
      await tester.pumpWidget(StringCollector(key: key));
370
      expect(find.text(''), findsOneWidget);
371 372
      final StreamController<String> controller = StreamController<String>();
      await tester.pumpWidget(StringCollector(key: key, stream: controller.stream));
373 374
      expect(find.text('conn'), findsOneWidget);
    });
375
    testWidgetsWithLeakTracking('gracefully handles transition to null stream', (WidgetTester tester) async {
376 377 378
      final GlobalKey key = GlobalKey();
      final StreamController<String> controller = StreamController<String>();
      await tester.pumpWidget(StringCollector(key: key, stream: controller.stream));
379
      expect(find.text('conn'), findsOneWidget);
380
      await tester.pumpWidget(StringCollector(key: key));
381 382
      expect(find.text('conn, disc'), findsOneWidget);
    });
383
    testWidgetsWithLeakTracking('gracefully handles transition to other stream', (WidgetTester tester) async {
384 385 386 387 388
      final GlobalKey key = GlobalKey();
      final StreamController<String> controllerA = StreamController<String>();
      final StreamController<String> controllerB = StreamController<String>();
      await tester.pumpWidget(StringCollector(key: key, stream: controllerA.stream));
      await tester.pumpWidget(StringCollector(key: key, stream: controllerB.stream));
389 390 391 392 393
      controllerA.add('A');
      controllerB.add('B');
      await eventFiring(tester);
      expect(find.text('conn, disc, conn, data:B'), findsOneWidget);
    });
394
    testWidgetsWithLeakTracking('tracks events and errors until completion', (WidgetTester tester) async {
395 396 397
      final GlobalKey key = GlobalKey();
      final StreamController<String> controller = StreamController<String>();
      await tester.pumpWidget(StringCollector(key: key, stream: controller.stream));
398
      controller.add('1');
399
      controller.addError('bad', StackTrace.fromString('trace'));
400 401 402
      controller.add('2');
      controller.close();
      await eventFiring(tester);
403
      expect(find.text('conn, data:1, error:bad stackTrace:trace, data:2, done'), findsOneWidget);
404 405 406 407
    });
  });
}

408
Future<void> eventFiring(WidgetTester tester) async {
409
  await tester.pump(Duration.zero);
410 411 412
}

class StringCollector extends StreamBuilderBase<String, List<String>> {
413
  const StringCollector({ super.key, super.stream });
414 415 416 417 418 419 420 421 422 423 424

  @override
  List<String> initial() => <String>[];

  @override
  List<String> afterConnected(List<String> current) => current..add('conn');

  @override
  List<String> afterData(List<String> current, String data) => current..add('data:$data');

  @override
425
  List<String> afterError(List<String> current, dynamic error, StackTrace stackTrace) => current..add('error:$error stackTrace:$stackTrace');
426 427 428 429 430 431 432 433

  @override
  List<String> afterDone(List<String> current) => current..add('done');

  @override
  List<String> afterDisconnected(List<String> current) => current..add('disc');

  @override
434
  Widget build(BuildContext context, List<String> currentSummary) => Text(currentSummary.join(', '), textDirection: TextDirection.ltr);
435
}