diff --git a/dev/bots/check_code_samples.dart b/dev/bots/check_code_samples.dart index b0590a89c6..44ce551b74 100644 --- a/dev/bots/check_code_samples.dart +++ b/dev/bots/check_code_samples.dart @@ -405,7 +405,6 @@ final Set _knownMissingTests = { 'examples/api/test/widgets/nested_scroll_view/nested_scroll_view.0_test.dart', 'examples/api/test/widgets/scroll_position/scroll_metrics_notification.0_test.dart', 'examples/api/test/widgets/media_query/media_query_data.system_gesture_insets.0_test.dart', - 'examples/api/test/widgets/async/stream_builder.0_test.dart', 'examples/api/test/widgets/async/future_builder.0_test.dart', 'examples/api/test/widgets/restoration_properties/restorable_value.0_test.dart', 'examples/api/test/widgets/animated_size/animated_size.0_test.dart', diff --git a/examples/api/lib/widgets/async/stream_builder.0.dart b/examples/api/lib/widgets/async/stream_builder.0.dart index 552763aaaf..ecaa9bbefb 100644 --- a/examples/api/lib/widgets/async/stream_builder.0.dart +++ b/examples/api/lib/widgets/async/stream_builder.0.dart @@ -13,34 +13,54 @@ void main() => runApp(const StreamBuilderExampleApp()); class StreamBuilderExampleApp extends StatelessWidget { const StreamBuilderExampleApp({super.key}); + static const Duration delay = Duration(seconds: 1); + @override Widget build(BuildContext context) { return const MaterialApp( - home: StreamBuilderExample(), + home: StreamBuilderExample(delay: delay), ); } } class StreamBuilderExample extends StatefulWidget { - const StreamBuilderExample({super.key}); + const StreamBuilderExample({ + required this.delay, + super.key, + }); + + final Duration delay; @override State createState() => _StreamBuilderExampleState(); } class _StreamBuilderExampleState extends State { - final Stream _bids = (() { - late final StreamController controller; - controller = StreamController( - onListen: () async { - await Future.delayed(const Duration(seconds: 1)); - controller.add(1); - await Future.delayed(const Duration(seconds: 1)); - await controller.close(); - }, - ); - return controller.stream; - })(); + late final StreamController _controller = StreamController( + onListen: () async { + await Future.delayed(widget.delay); + + if (!_controller.isClosed) { + _controller.add(1); + } + + await Future.delayed(widget.delay); + + if (!_controller.isClosed) { + _controller.close(); + } + }, + ); + + Stream get _bids => _controller.stream; + + @override + void dispose() { + if (!_controller.isClosed) { + _controller.close(); + } + super.dispose(); + } @override Widget build(BuildContext context) { @@ -50,86 +70,108 @@ class _StreamBuilderExampleState extends State { child: Container( alignment: FractionalOffset.center, color: Colors.white, - child: StreamBuilder( - stream: _bids, - builder: (BuildContext context, AsyncSnapshot snapshot) { - List children; - if (snapshot.hasError) { - children = [ - const Icon( - Icons.error_outline, - color: Colors.red, - size: 60, - ), - Padding( - padding: const EdgeInsets.only(top: 16), - child: Text('Error: ${snapshot.error}'), - ), - Padding( - padding: const EdgeInsets.only(top: 8), - child: Text('Stack trace: ${snapshot.stackTrace}'), - ), - ]; - } else { - switch (snapshot.connectionState) { - case ConnectionState.none: - children = const [ - Icon( - Icons.info, - color: Colors.blue, - size: 60, - ), - Padding( - padding: EdgeInsets.only(top: 16), - child: Text('Select a lot'), - ), - ]; - case ConnectionState.waiting: - children = const [ - SizedBox( - width: 60, - height: 60, - child: CircularProgressIndicator(), - ), - Padding( - padding: EdgeInsets.only(top: 16), - child: Text('Awaiting bids...'), - ), - ]; - case ConnectionState.active: - children = [ - const Icon( - Icons.check_circle_outline, - color: Colors.green, - size: 60, - ), - Padding( - padding: const EdgeInsets.only(top: 16), - child: Text('\$${snapshot.data}'), - ), - ]; - case ConnectionState.done: - children = [ - const Icon( - Icons.info, - color: Colors.blue, - size: 60, - ), - Padding( - padding: const EdgeInsets.only(top: 16), - child: Text('\$${snapshot.data} (closed)'), - ), - ]; - } - } - - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: children, - ); - }, - ), + child: BidsStatus(bids: _bids), ), ); } } + +class BidsStatus extends StatelessWidget { + const BidsStatus({ + required this.bids, + super.key, + }); + + final Stream? bids; + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: bids, + builder: (BuildContext context, AsyncSnapshot snapshot) { + List children; + if (snapshot.hasError) { + children = [ + const Icon( + Icons.error_outline, + color: Colors.red, + size: 60, + ), + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text('Error: ${snapshot.error}'), + ), + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + 'Stack trace: ${snapshot.stackTrace}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ]; + } else { + switch (snapshot.connectionState) { + case ConnectionState.none: + children = const [ + Icon( + Icons.info, + color: Colors.blue, + size: 60, + ), + Padding( + padding: EdgeInsets.only(top: 16), + child: Text('Select a lot'), + ), + ]; + case ConnectionState.waiting: + children = const [ + SizedBox( + width: 60, + height: 60, + child: CircularProgressIndicator(), + ), + Padding( + padding: EdgeInsets.only(top: 16), + child: Text('Awaiting bids...'), + ), + ]; + case ConnectionState.active: + children = [ + const Icon( + Icons.check_circle_outline, + color: Colors.green, + size: 60, + ), + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text('\$${snapshot.data}'), + ), + ]; + case ConnectionState.done: + children = [ + const Icon( + Icons.info, + color: Colors.blue, + size: 60, + ), + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text( + snapshot.hasData + ? '\$${snapshot.data} (closed)' + : '(closed)', + ), + ), + ]; + } + } + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: children, + ); + }, + ); + } +} diff --git a/examples/api/test/widgets/async/stream_builder.0_test.dart b/examples/api/test/widgets/async/stream_builder.0_test.dart new file mode 100644 index 0000000000..597f36680b --- /dev/null +++ b/examples/api/test/widgets/async/stream_builder.0_test.dart @@ -0,0 +1,110 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/widgets/async/stream_builder.0.dart' + as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('StreamBuilder listens to internal stream', (WidgetTester tester) async { + await tester.pumpWidget( + const example.StreamBuilderExampleApp(), + ); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + expect(find.text('Awaiting bids...'), findsOneWidget); + + await tester.pump(example.StreamBuilderExampleApp.delay); + + expect(find.byIcon(Icons.check_circle_outline), findsOneWidget); + expect(find.text(r'$1'), findsOneWidget); + + await tester.pump(example.StreamBuilderExampleApp.delay); + + expect(find.byIcon(Icons.info), findsOneWidget); + expect(find.text(r'$1 (closed)'), findsOneWidget); + }); + + testWidgets('BidsStatus correctly displays error state', (WidgetTester tester) async { + final StreamController controller = StreamController(); + addTearDown(controller.close); + + controller.onListen = () { + controller.addError('Unexpected error!', StackTrace.empty); + }; + + await tester.pumpWidget( + MaterialApp( + home: example.BidsStatus(bids: controller.stream), + ), + ); + await tester.pump(); + + expect(find.byIcon(Icons.error_outline), findsOneWidget); + expect(find.text('Error: Unexpected error!'), findsOneWidget); + expect(find.text('Stack trace: ${StackTrace.empty}'), findsOneWidget); + }); + + testWidgets('BidsStatus correctly displays none state', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: example.BidsStatus(bids: null), + ), + ); + + expect(find.byIcon(Icons.info), findsOneWidget); + expect(find.text('Select a lot'), findsOneWidget); + }); + + testWidgets('BidsStatus correctly displays waiting state', (WidgetTester tester) async { + final StreamController controller = StreamController(); + addTearDown(controller.close); + + await tester.pumpWidget( + MaterialApp( + home: example.BidsStatus(bids: controller.stream), + ), + ); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + expect(find.text('Awaiting bids...'), findsOneWidget); + }); + + testWidgets('BidsStatus correctly displays active state', (WidgetTester tester) async { + final StreamController controller = StreamController(); + addTearDown(controller.close); + + controller.onListen = () { + controller.add(1); + }; + + await tester.pumpWidget( + MaterialApp( + home: example.BidsStatus(bids: controller.stream), + ), + ); + await tester.pump(); + + expect(find.byIcon(Icons.check_circle_outline), findsOneWidget); + expect(find.text(r'$1'), findsOneWidget); + }); + + testWidgets('BidsStatus correctly displays done state', (WidgetTester tester) async { + final StreamController controller = StreamController(); + controller.close(); + + await tester.pumpWidget( + MaterialApp( + home: example.BidsStatus(bids: controller.stream), + ), + ); + await tester.pump(); + + expect(find.byIcon(Icons.info), findsOneWidget); + expect(find.text('(closed)'), findsOneWidget); + }); +}