From afd7f412b60a88dd7adc2bda5e3938ec2bae4eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petrus=20Nguy=E1=BB=85n=20Th=C3=A1i=20H=E1=BB=8Dc?= Date: Sat, 1 Feb 2025 15:10:41 +0700 Subject: [PATCH] ci: add rxdart_flutter tests to GHA (#783) * Update pubspec.yaml * Update pubspec.yaml * Update packages/rxdart_flutter/pubspec.yaml * local ValueSubject * local ValueSubject --- .github/workflows/rxdart-test.yml | 5 +- melos.yaml | 6 +- packages/rxdart_flutter/lib/src/errors.dart | 4 +- .../lib/src/value_stream_listener.dart | 12 +- packages/rxdart_flutter/pubspec.yaml | 1 - .../not_replay_value_stream_builder_test.dart | 3 +- ...not_replay_value_stream_consumer_test.dart | 3 +- ...not_replay_value_stream_listener_test.dart | 3 +- .../test/src/rxdart_ext/value_subject.dart | 222 ++++++++++++++++++ 9 files changed, 251 insertions(+), 8 deletions(-) create mode 100644 packages/rxdart_flutter/test/src/rxdart_ext/value_subject.dart diff --git a/.github/workflows/rxdart-test.yml b/.github/workflows/rxdart-test.yml index 1dab92f6..1850f4d1 100644 --- a/.github/workflows/rxdart-test.yml +++ b/.github/workflows/rxdart-test.yml @@ -59,8 +59,11 @@ jobs: - name: Active coverage run: dart pub global activate coverage - - name: Run tests + - name: Run rxdart tests run: melos run test-rxdart - uses: codecov/codecov-action@v3.1.6 if: ${{ matrix.flutter == 'stable' }} + + - name: Run rxdart_flutter tests + run: melos run test-rxdart-flutter diff --git a/melos.yaml b/melos.yaml index a3242e11..0cb55243 100644 --- a/melos.yaml +++ b/melos.yaml @@ -24,4 +24,8 @@ scripts: dart pub global run coverage:format_coverage --lcov --in=coverage.json --out=lcov.info --report-on=lib generate: run: melos exec --depends-on=build_runner -- "dart run build_runner build -d" - description: Build all generated files for Dart & Flutter packages in this project. \ No newline at end of file + description: Build all generated files for Dart & Flutter packages in this project. + test-rxdart-flutter: + run: | + cd \$MELOS_ROOT_PATH/packages/rxdart_flutter + flutter test --no-pub diff --git a/packages/rxdart_flutter/lib/src/errors.dart b/packages/rxdart_flutter/lib/src/errors.dart index c8923b61..800191d8 100644 --- a/packages/rxdart_flutter/lib/src/errors.dart +++ b/packages/rxdart_flutter/lib/src/errors.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:rxdart/rxdart.dart'; +import 'package:meta/meta.dart'; const _bullet = ' • '; const _indent = ' '; @@ -71,6 +72,7 @@ ${_indent}https://github.com/ReactiveX/rxdart/issues/new } } +@internal void reportError(ErrorAndStackTrace error) { FlutterError.reportError( FlutterErrorDetails( @@ -81,7 +83,7 @@ void reportError(ErrorAndStackTrace error) { ); } -// @pragma('vm:notify-debugger-on-exception') +@internal ErrorAndStackTrace? validateValueStreamInitialValue(ValueStream stream) { ErrorAndStackTrace? error; diff --git a/packages/rxdart_flutter/lib/src/value_stream_listener.dart b/packages/rxdart_flutter/lib/src/value_stream_listener.dart index e4b32868..307484dd 100644 --- a/packages/rxdart_flutter/lib/src/value_stream_listener.dart +++ b/packages/rxdart_flutter/lib/src/value_stream_listener.dart @@ -134,7 +134,7 @@ class _ValueStreamListenerState extends State> { } else { skipCount = 0; if (_initialized) { - WidgetsBinding.instance.addPostFrameCallback((_) { + _ambiguate(WidgetsBinding.instance)!.addPostFrameCallback((_) { _notifyListener(stream.value); }); } @@ -174,3 +174,13 @@ class _ValueStreamListenerState extends State> { return widget.child; } } + +/// Reference: https://docs.flutter.dev/release/release-notes/release-notes-3.0.0#your-code +/// +/// This allows a value of type T or T? +/// to be treated as a value of type T?. +/// +/// We use this so that APIs that have become +/// non-nullable can still be used with `!` and `?` +/// to support older versions of the API as well. +T? _ambiguate(T? value) => value; diff --git a/packages/rxdart_flutter/pubspec.yaml b/packages/rxdart_flutter/pubspec.yaml index 2941f592..aa111ab1 100644 --- a/packages/rxdart_flutter/pubspec.yaml +++ b/packages/rxdart_flutter/pubspec.yaml @@ -16,7 +16,6 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^1.0.4 - rxdart_ext: ^0.3.0 topics: - rxdart diff --git a/packages/rxdart_flutter/test/src/not_replay_value_stream_builder_test.dart b/packages/rxdart_flutter/test/src/not_replay_value_stream_builder_test.dart index 6f225452..ec3cf6ce 100644 --- a/packages/rxdart_flutter/test/src/not_replay_value_stream_builder_test.dart +++ b/packages/rxdart_flutter/test/src/not_replay_value_stream_builder_test.dart @@ -3,10 +3,11 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:rxdart_ext/rxdart_ext.dart'; import 'package:rxdart_flutter/rxdart_flutter.dart'; import 'package:rxdart_flutter/src/errors.dart'; +import 'rxdart_ext/value_subject.dart'; + class BuilderApp extends StatefulWidget { const BuilderApp({ required this.stream1, diff --git a/packages/rxdart_flutter/test/src/not_replay_value_stream_consumer_test.dart b/packages/rxdart_flutter/test/src/not_replay_value_stream_consumer_test.dart index 26cdeacc..970e06d2 100644 --- a/packages/rxdart_flutter/test/src/not_replay_value_stream_consumer_test.dart +++ b/packages/rxdart_flutter/test/src/not_replay_value_stream_consumer_test.dart @@ -3,10 +3,11 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:rxdart_ext/rxdart_ext.dart'; import 'package:rxdart_flutter/rxdart_flutter.dart'; import 'package:rxdart_flutter/src/errors.dart'; +import 'rxdart_ext/value_subject.dart'; + class ConsumerApp extends StatefulWidget { const ConsumerApp({ required this.stream1, diff --git a/packages/rxdart_flutter/test/src/not_replay_value_stream_listener_test.dart b/packages/rxdart_flutter/test/src/not_replay_value_stream_listener_test.dart index de5d59b6..73570b12 100644 --- a/packages/rxdart_flutter/test/src/not_replay_value_stream_listener_test.dart +++ b/packages/rxdart_flutter/test/src/not_replay_value_stream_listener_test.dart @@ -3,10 +3,11 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:rxdart_ext/rxdart_ext.dart'; import 'package:rxdart_flutter/rxdart_flutter.dart'; import 'package:rxdart_flutter/src/errors.dart'; +import 'rxdart_ext/value_subject.dart'; + typedef Value = void Function(T previous, T current); class ListenerApp extends StatefulWidget { diff --git a/packages/rxdart_flutter/test/src/rxdart_ext/value_subject.dart b/packages/rxdart_flutter/test/src/rxdart_ext/value_subject.dart new file mode 100644 index 00000000..f788da7a --- /dev/null +++ b/packages/rxdart_flutter/test/src/rxdart_ext/value_subject.dart @@ -0,0 +1,222 @@ +// COPIED from: https://github.com/hoc081098/rxdart_ext/blob/ed5ad736ac0b531348cebef9c1bf5a3130045e89/lib/src/not_replay_value_stream/value_subject.dart + +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:rxdart/rxdart.dart'; + +/// A special [StreamController] that captures the latest item that has been +/// added to the controller. +/// +/// [ValueSubject] is the same as [PublishSubject], with the ability to capture +/// the latest item has been added to the controller. +/// This [ValueSubject] always has the value, ie. [hasValue] is always true. +/// +/// [ValueSubject] is, by default, a broadcast (aka hot) controller, in order +/// to fulfill the Rx Subject contract. This means the Subject's `stream` can +/// be listened to multiple times. +/// +/// ### Example +/// +/// final subject = ValueSubject(1); +/// +/// print(subject.value); // prints 1 +/// +/// // observers will receive 3 and done events. +/// subject.stream.listen(print); // prints 2 +/// subject.stream.listen(print); // prints 2 +/// subject.stream.listen(print); // prints 2 +/// +/// subject.add(2); +/// subject.close(); +@sealed +class ValueSubject extends Subject implements ValueStream { + final _StreamEvent _event; + + ValueSubject._( + StreamController controller, + this._event, + ) : super(controller, controller.stream); + + /// Constructs a [ValueSubject], optionally pass handlers for + /// [onListen], [onCancel] and a flag to handle events [sync]. + /// + /// [seedValue] becomes the current [value] of Subject. + /// + /// See also [StreamController.broadcast]. + factory ValueSubject( + T seedValue, { + void Function()? onListen, + void Function()? onCancel, + bool sync = false, + }) { + final controller = StreamController.broadcast( + onListen: onListen, + onCancel: onCancel, + sync: sync, + ); + + return ValueSubject._( + controller, + _StreamEvent.data(seedValue), + ); + } + + @override + void onAdd(T event) => _event.onData(event); + + @override + void onAddError(Object error, [StackTrace? stackTrace]) => + _event.onError(ErrorAndStackTrace(error, stackTrace)); + + @override + ValueStream get stream => _ValueSubjectStream(this); + + @nonVirtual + @override + Object get error { + final errorAndSt = _event.errorAndStackTrace; + if (errorAndSt != null) { + return errorAndSt.error; + } + throw ValueStreamError.hasNoError(); + } + + @nonVirtual + @override + Object? get errorOrNull => _event.errorAndStackTrace?.error; + + @nonVirtual + @override + bool get hasError => _event.errorAndStackTrace != null; + + @nonVirtual + @override + StackTrace? get stackTrace => _event.errorAndStackTrace?.stackTrace; + + @nonVirtual + @override + T get value => _event.value; + + @nonVirtual + @override + T get valueOrNull => _event.value; + + @nonVirtual + @override + bool get hasValue => true; + + @nonVirtual + @override + StreamNotification? get lastEventOrNull { + // data event + if (_event.lastEventIsData) { + return DataNotification(value); + } + + // error event + final errorAndSt = _event.errorAndStackTrace; + if (errorAndSt != null) { + return ErrorNotification(errorAndSt); + } + + // no event + return null; + } +} + +class _ValueSubjectStream extends Stream implements ValueStream { + final ValueSubject _subject; + + _ValueSubjectStream(this._subject); + + @override + bool get isBroadcast => true; + + // Override == and hashCode so that new streams returned by the same + // subject are considered equal. + // The subject returns a new stream each time it's queried, + // but doesn't have to cache the result. + + @override + int get hashCode => _subject.hashCode ^ 0x35323532; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is _ValueSubjectStream && identical(other._subject, _subject); + } + + @override + StreamSubscription listen( + void Function(T event)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, + }) => + _subject.listen( + onData, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ); + + @override + Object get error => _subject.error; + + @override + Object? get errorOrNull => _subject.errorOrNull; + + @override + bool get hasError => _subject.hasError; + + @override + bool get hasValue => _subject.hasValue; + + @override + StreamNotification? get lastEventOrNull => _subject.lastEventOrNull; + + @override + StackTrace? get stackTrace => _subject.stackTrace; + + @override + T get value => _subject.value; + + @override + T? get valueOrNull => _subject.valueOrNull; +} + +/// Class that holds latest value and lasted error emitted from Stream. +class _StreamEvent { + T _value; + ErrorAndStackTrace? _errorAndStacktrace; + var _lastEventIsData = false; + + /// Construct a [_StreamEvent] with data event. + _StreamEvent.data(T seedValue) + : _value = seedValue, + _lastEventIsData = true; + + /// Keep error state. + void onError(ErrorAndStackTrace errorAndStacktrace) { + _errorAndStacktrace = errorAndStacktrace; + _lastEventIsData = false; + } + + /// Keep data state. + void onData(T value) { + _value = value; + _lastEventIsData = true; + } + + /// Last emitted value + /// or null if no data added. + T get value => _value; + + /// Last emitted error and the corresponding stack trace, + /// or null if no error added. + ErrorAndStackTrace? get errorAndStackTrace => _errorAndStacktrace; + + /// Check if the last emitted event is data event. + bool get lastEventIsData => _lastEventIsData; +}