async_test.dart 20.4 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 10
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';

11
void main() {
12
  Widget snapshotText(BuildContext context, AsyncSnapshot<String> snapshot) {
13
    return Text(snapshot.toString(), textDirection: TextDirection.ltr);
14
  }
15
  group('AsyncSnapshot', () {
16 17 18 19 20 21 22 23 24 25 26 27 28 29
    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);
      }
    });
30 31 32 33 34
    test('requiring data succeeds if data is present', () {
      expect(
        const AsyncSnapshot<String>.withData(ConnectionState.done, 'hello').requireData,
        'hello',
      );
35
    });
36 37 38 39 40
    test('requiring data fails if there is an error', () {
      expect(
        () => const AsyncSnapshot<String>.withError(ConnectionState.done, 'error').requireData,
        throwsA(equals('error')),
      );
41
    });
42 43 44 45 46
    test('requiring data fails if snapshot has neither data nor error', () {
      expect(
        () => const AsyncSnapshot<String>.nothing().requireData,
        throwsStateError,
      );
47
    });
48 49 50 51
    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);
52
      expect(const AsyncSnapshot<int>.nothing().stackTrace, isNull);
53 54 55
      expect(const AsyncSnapshot<int>.waiting().connectionState, ConnectionState.waiting);
      expect(const AsyncSnapshot<int>.waiting().data, isNull);
      expect(const AsyncSnapshot<int>.waiting().error, isNull);
56 57 58 59 60 61 62
      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(
63
        AsyncSnapshot<int>.withError(ConnectionState.done, error),
64
        AsyncSnapshot<int>.withError(ConnectionState.done, error),
65
      );
66
    });
67
  });
68 69
  group('Async smoke tests', () {
    testWidgets('FutureBuilder', (WidgetTester tester) async {
70 71
      await tester.pumpWidget(FutureBuilder<String>(
        future: Future<String>.value('hello'),
72
        builder: snapshotText,
73 74 75 76
      ));
      await eventFiring(tester);
    });
    testWidgets('StreamBuilder', (WidgetTester tester) async {
77
      await tester.pumpWidget(StreamBuilder<String>(
78
        stream: Stream<String>.fromIterable(<String>['hello', 'world']),
79
        builder: snapshotText,
80 81 82 83
      ));
      await eventFiring(tester);
    });
    testWidgets('StreamFold', (WidgetTester tester) async {
84
      await tester.pumpWidget(StringCollector(
85
        stream: Stream<String>.fromIterable(<String>['hello', 'world']),
86 87 88 89 90
      ));
      await eventFiring(tester);
    });
  });
  group('FutureBuilder', () {
91 92 93 94 95 96 97 98 99 100 101 102 103 104
    testWidgets('gives expected snapshot with SynchronousFuture', (WidgetTester tester) async {
      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();
        },
      ));
    });

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

404
Future<void> eventFiring(WidgetTester tester) async {
405
  await tester.pump(Duration.zero);
406 407 408
}

class StringCollector extends StreamBuilderBase<String, List<String>> {
409
  const StringCollector({ super.key, super.stream });
410 411 412 413 414 415 416 417 418 419 420

  @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
421
  List<String> afterError(List<String> current, dynamic error, StackTrace stackTrace) => current..add('error:$error stackTrace:$stackTrace');
422 423 424 425 426 427 428 429

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

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

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