From d5feb0b091aa338411b9d60b985a782f115e5095 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sun, 14 Jul 2024 23:16:29 +0530 Subject: [PATCH 01/71] wip: history request groups --- lib/consts.dart | 3 +- lib/models/history_meta_model.dart | 21 ++ lib/models/history_meta_model.freezed.dart | 261 ++++++++++++++++++ lib/models/history_meta_model.g.dart | 38 +++ lib/models/history_request_model.dart | 23 ++ lib/models/history_request_model.freezed.dart | 258 +++++++++++++++++ lib/models/history_request_model.g.dart | 27 ++ lib/models/models.dart | 2 + lib/providers/collection_providers.dart | 16 ++ lib/providers/history_providers.dart | 91 ++++++ lib/providers/providers.dart | 1 + lib/screens/about_dialog.dart | 29 -- lib/screens/dashboard.dart | 6 +- lib/screens/envvar/environment_editor.dart | 56 ++-- lib/screens/history/history_details.dart | 10 + lib/screens/history/history_page.dart | 41 +++ lib/screens/history/history_pane.dart | 96 +++++++ lib/screens/history/history_requests.dart | 10 + lib/screens/history/history_viewer.dart | 35 +++ .../home_page/editor_pane/editor_request.dart | 2 +- lib/screens/mobile/dashboard.dart | 15 +- lib/screens/settings_page.dart | 1 - lib/services/hive_services.dart | 30 ++ lib/utils/convert_utils.dart | 8 + lib/utils/history_utils.dart | 86 ++++++ lib/utils/utils.dart | 1 + lib/widgets/card_sidebar_history.dart | 110 ++++++++ lib/widgets/splitview_history.dart | 65 +++++ lib/widgets/widgets.dart | 2 + pubspec.lock | 8 + pubspec.yaml | 3 +- 31 files changed, 1284 insertions(+), 71 deletions(-) create mode 100644 lib/models/history_meta_model.dart create mode 100644 lib/models/history_meta_model.freezed.dart create mode 100644 lib/models/history_meta_model.g.dart create mode 100644 lib/models/history_request_model.dart create mode 100644 lib/models/history_request_model.freezed.dart create mode 100644 lib/models/history_request_model.g.dart create mode 100644 lib/providers/history_providers.dart delete mode 100644 lib/screens/about_dialog.dart create mode 100644 lib/screens/history/history_details.dart create mode 100644 lib/screens/history/history_page.dart create mode 100644 lib/screens/history/history_pane.dart create mode 100644 lib/screens/history/history_requests.dart create mode 100644 lib/screens/history/history_viewer.dart create mode 100644 lib/utils/history_utils.dart create mode 100644 lib/widgets/card_sidebar_history.dart create mode 100644 lib/widgets/splitview_history.dart diff --git a/lib/consts.dart b/lib/consts.dart index 481a0665..63e1946f 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -68,6 +68,7 @@ const kFormDataButtonLabelTextStyle = TextStyle( const kTextStylePopupMenuItem = TextStyle(fontSize: 16); const kBorderRadius4 = BorderRadius.all(Radius.circular(4)); +const kBorderRadius6 = BorderRadius.all(Radius.circular(6)); const kBorderRadius8 = BorderRadius.all(Radius.circular(8)); final kBorderRadius10 = BorderRadius.circular(10); const kBorderRadius12 = BorderRadius.all(Radius.circular(12)); @@ -88,7 +89,7 @@ const kPv8 = EdgeInsets.symmetric(vertical: 8); const kPv6 = EdgeInsets.symmetric(vertical: 6); const kPv2 = EdgeInsets.symmetric(vertical: 2); const kPh2 = EdgeInsets.symmetric(horizontal: 2); -const kPt24o8 = EdgeInsets.only(top: 24, left: 8.0, right: 8.0, bottom: 8.0); +const kPt28o8 = EdgeInsets.only(top: 28, left: 8.0, right: 8.0, bottom: 8.0); const kPt5o10 = EdgeInsets.only(left: 10.0, right: 10.0, top: 5.0, bottom: 10.0); const kPh4 = EdgeInsets.symmetric(horizontal: 4); diff --git a/lib/models/history_meta_model.dart b/lib/models/history_meta_model.dart new file mode 100644 index 00000000..58e68aee --- /dev/null +++ b/lib/models/history_meta_model.dart @@ -0,0 +1,21 @@ +import 'package:apidash/consts.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'history_meta_model.freezed.dart'; + +part 'history_meta_model.g.dart'; + +@freezed +class HistoryMetaModel with _$HistoryMetaModel { + const factory HistoryMetaModel({ + required String historyId, + @Default("") String name, + required String url, + required HTTPVerb method, + required int responseStatus, + required DateTime timeStamp, + }) = _HistoryMetaModel; + + factory HistoryMetaModel.fromJson(Map json) => + _$HistoryMetaModelFromJson(json); +} diff --git a/lib/models/history_meta_model.freezed.dart b/lib/models/history_meta_model.freezed.dart new file mode 100644 index 00000000..fb7ba2eb --- /dev/null +++ b/lib/models/history_meta_model.freezed.dart @@ -0,0 +1,261 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'history_meta_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +HistoryMetaModel _$HistoryMetaModelFromJson(Map json) { + return _HistoryMetaModel.fromJson(json); +} + +/// @nodoc +mixin _$HistoryMetaModel { + String get historyId => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String get url => throw _privateConstructorUsedError; + HTTPVerb get method => throw _privateConstructorUsedError; + int get responseStatus => throw _privateConstructorUsedError; + DateTime get timeStamp => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $HistoryMetaModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $HistoryMetaModelCopyWith<$Res> { + factory $HistoryMetaModelCopyWith( + HistoryMetaModel value, $Res Function(HistoryMetaModel) then) = + _$HistoryMetaModelCopyWithImpl<$Res, HistoryMetaModel>; + @useResult + $Res call( + {String historyId, + String name, + String url, + HTTPVerb method, + int responseStatus, + DateTime timeStamp}); +} + +/// @nodoc +class _$HistoryMetaModelCopyWithImpl<$Res, $Val extends HistoryMetaModel> + implements $HistoryMetaModelCopyWith<$Res> { + _$HistoryMetaModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? historyId = null, + Object? name = null, + Object? url = null, + Object? method = null, + Object? responseStatus = null, + Object? timeStamp = null, + }) { + return _then(_value.copyWith( + historyId: null == historyId + ? _value.historyId + : historyId // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + method: null == method + ? _value.method + : method // ignore: cast_nullable_to_non_nullable + as HTTPVerb, + responseStatus: null == responseStatus + ? _value.responseStatus + : responseStatus // ignore: cast_nullable_to_non_nullable + as int, + timeStamp: null == timeStamp + ? _value.timeStamp + : timeStamp // ignore: cast_nullable_to_non_nullable + as DateTime, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$HistoryMetaModelImplCopyWith<$Res> + implements $HistoryMetaModelCopyWith<$Res> { + factory _$$HistoryMetaModelImplCopyWith(_$HistoryMetaModelImpl value, + $Res Function(_$HistoryMetaModelImpl) then) = + __$$HistoryMetaModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String historyId, + String name, + String url, + HTTPVerb method, + int responseStatus, + DateTime timeStamp}); +} + +/// @nodoc +class __$$HistoryMetaModelImplCopyWithImpl<$Res> + extends _$HistoryMetaModelCopyWithImpl<$Res, _$HistoryMetaModelImpl> + implements _$$HistoryMetaModelImplCopyWith<$Res> { + __$$HistoryMetaModelImplCopyWithImpl(_$HistoryMetaModelImpl _value, + $Res Function(_$HistoryMetaModelImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? historyId = null, + Object? name = null, + Object? url = null, + Object? method = null, + Object? responseStatus = null, + Object? timeStamp = null, + }) { + return _then(_$HistoryMetaModelImpl( + historyId: null == historyId + ? _value.historyId + : historyId // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + method: null == method + ? _value.method + : method // ignore: cast_nullable_to_non_nullable + as HTTPVerb, + responseStatus: null == responseStatus + ? _value.responseStatus + : responseStatus // ignore: cast_nullable_to_non_nullable + as int, + timeStamp: null == timeStamp + ? _value.timeStamp + : timeStamp // ignore: cast_nullable_to_non_nullable + as DateTime, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$HistoryMetaModelImpl implements _HistoryMetaModel { + const _$HistoryMetaModelImpl( + {required this.historyId, + this.name = "", + required this.url, + required this.method, + required this.responseStatus, + required this.timeStamp}); + + factory _$HistoryMetaModelImpl.fromJson(Map json) => + _$$HistoryMetaModelImplFromJson(json); + + @override + final String historyId; + @override + @JsonKey() + final String name; + @override + final String url; + @override + final HTTPVerb method; + @override + final int responseStatus; + @override + final DateTime timeStamp; + + @override + String toString() { + return 'HistoryMetaModel(historyId: $historyId, name: $name, url: $url, method: $method, responseStatus: $responseStatus, timeStamp: $timeStamp)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$HistoryMetaModelImpl && + (identical(other.historyId, historyId) || + other.historyId == historyId) && + (identical(other.name, name) || other.name == name) && + (identical(other.url, url) || other.url == url) && + (identical(other.method, method) || other.method == method) && + (identical(other.responseStatus, responseStatus) || + other.responseStatus == responseStatus) && + (identical(other.timeStamp, timeStamp) || + other.timeStamp == timeStamp)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, historyId, name, url, method, responseStatus, timeStamp); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$HistoryMetaModelImplCopyWith<_$HistoryMetaModelImpl> get copyWith => + __$$HistoryMetaModelImplCopyWithImpl<_$HistoryMetaModelImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$HistoryMetaModelImplToJson( + this, + ); + } +} + +abstract class _HistoryMetaModel implements HistoryMetaModel { + const factory _HistoryMetaModel( + {required final String historyId, + final String name, + required final String url, + required final HTTPVerb method, + required final int responseStatus, + required final DateTime timeStamp}) = _$HistoryMetaModelImpl; + + factory _HistoryMetaModel.fromJson(Map json) = + _$HistoryMetaModelImpl.fromJson; + + @override + String get historyId; + @override + String get name; + @override + String get url; + @override + HTTPVerb get method; + @override + int get responseStatus; + @override + DateTime get timeStamp; + @override + @JsonKey(ignore: true) + _$$HistoryMetaModelImplCopyWith<_$HistoryMetaModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/history_meta_model.g.dart b/lib/models/history_meta_model.g.dart new file mode 100644 index 00000000..67975edc --- /dev/null +++ b/lib/models/history_meta_model.g.dart @@ -0,0 +1,38 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'history_meta_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$HistoryMetaModelImpl _$$HistoryMetaModelImplFromJson( + Map json) => + _$HistoryMetaModelImpl( + historyId: json['historyId'] as String, + name: json['name'] as String? ?? "", + url: json['url'] as String, + method: $enumDecode(_$HTTPVerbEnumMap, json['method']), + responseStatus: (json['responseStatus'] as num).toInt(), + timeStamp: DateTime.parse(json['timeStamp'] as String), + ); + +Map _$$HistoryMetaModelImplToJson( + _$HistoryMetaModelImpl instance) => + { + 'historyId': instance.historyId, + 'name': instance.name, + 'url': instance.url, + 'method': _$HTTPVerbEnumMap[instance.method]!, + 'responseStatus': instance.responseStatus, + 'timeStamp': instance.timeStamp.toIso8601String(), + }; + +const _$HTTPVerbEnumMap = { + HTTPVerb.get: 'get', + HTTPVerb.head: 'head', + HTTPVerb.post: 'post', + HTTPVerb.put: 'put', + HTTPVerb.patch: 'patch', + HTTPVerb.delete: 'delete', +}; diff --git a/lib/models/history_request_model.dart b/lib/models/history_request_model.dart new file mode 100644 index 00000000..5a9a2b53 --- /dev/null +++ b/lib/models/history_request_model.dart @@ -0,0 +1,23 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'models.dart'; + +part 'history_request_model.freezed.dart'; + +part 'history_request_model.g.dart'; + +@freezed +class HistoryRequestModel with _$HistoryRequestModel { + @JsonSerializable( + explicitToJson: true, + anyMap: true, + ) + const factory HistoryRequestModel({ + required String historyId, + required HistoryMetaModel metaData, + required HttpRequestModel httpRequestModel, + required HttpResponseModel httpResponseModel, + }) = _HistoryRequestModel; + + factory HistoryRequestModel.fromJson(Map json) => + _$HistoryRequestModelFromJson(json); +} diff --git a/lib/models/history_request_model.freezed.dart b/lib/models/history_request_model.freezed.dart new file mode 100644 index 00000000..afdacfdd --- /dev/null +++ b/lib/models/history_request_model.freezed.dart @@ -0,0 +1,258 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'history_request_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +HistoryRequestModel _$HistoryRequestModelFromJson(Map json) { + return _HistoryRequestModel.fromJson(json); +} + +/// @nodoc +mixin _$HistoryRequestModel { + String get historyId => throw _privateConstructorUsedError; + HistoryMetaModel get metaData => throw _privateConstructorUsedError; + HttpRequestModel get httpRequestModel => throw _privateConstructorUsedError; + HttpResponseModel get httpResponseModel => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $HistoryRequestModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $HistoryRequestModelCopyWith<$Res> { + factory $HistoryRequestModelCopyWith( + HistoryRequestModel value, $Res Function(HistoryRequestModel) then) = + _$HistoryRequestModelCopyWithImpl<$Res, HistoryRequestModel>; + @useResult + $Res call( + {String historyId, + HistoryMetaModel metaData, + HttpRequestModel httpRequestModel, + HttpResponseModel httpResponseModel}); + + $HistoryMetaModelCopyWith<$Res> get metaData; + $HttpRequestModelCopyWith<$Res> get httpRequestModel; + $HttpResponseModelCopyWith<$Res> get httpResponseModel; +} + +/// @nodoc +class _$HistoryRequestModelCopyWithImpl<$Res, $Val extends HistoryRequestModel> + implements $HistoryRequestModelCopyWith<$Res> { + _$HistoryRequestModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? historyId = null, + Object? metaData = null, + Object? httpRequestModel = null, + Object? httpResponseModel = null, + }) { + return _then(_value.copyWith( + historyId: null == historyId + ? _value.historyId + : historyId // ignore: cast_nullable_to_non_nullable + as String, + metaData: null == metaData + ? _value.metaData + : metaData // ignore: cast_nullable_to_non_nullable + as HistoryMetaModel, + httpRequestModel: null == httpRequestModel + ? _value.httpRequestModel + : httpRequestModel // ignore: cast_nullable_to_non_nullable + as HttpRequestModel, + httpResponseModel: null == httpResponseModel + ? _value.httpResponseModel + : httpResponseModel // ignore: cast_nullable_to_non_nullable + as HttpResponseModel, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $HistoryMetaModelCopyWith<$Res> get metaData { + return $HistoryMetaModelCopyWith<$Res>(_value.metaData, (value) { + return _then(_value.copyWith(metaData: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $HttpRequestModelCopyWith<$Res> get httpRequestModel { + return $HttpRequestModelCopyWith<$Res>(_value.httpRequestModel, (value) { + return _then(_value.copyWith(httpRequestModel: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $HttpResponseModelCopyWith<$Res> get httpResponseModel { + return $HttpResponseModelCopyWith<$Res>(_value.httpResponseModel, (value) { + return _then(_value.copyWith(httpResponseModel: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$HistoryRequestModelImplCopyWith<$Res> + implements $HistoryRequestModelCopyWith<$Res> { + factory _$$HistoryRequestModelImplCopyWith(_$HistoryRequestModelImpl value, + $Res Function(_$HistoryRequestModelImpl) then) = + __$$HistoryRequestModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String historyId, + HistoryMetaModel metaData, + HttpRequestModel httpRequestModel, + HttpResponseModel httpResponseModel}); + + @override + $HistoryMetaModelCopyWith<$Res> get metaData; + @override + $HttpRequestModelCopyWith<$Res> get httpRequestModel; + @override + $HttpResponseModelCopyWith<$Res> get httpResponseModel; +} + +/// @nodoc +class __$$HistoryRequestModelImplCopyWithImpl<$Res> + extends _$HistoryRequestModelCopyWithImpl<$Res, _$HistoryRequestModelImpl> + implements _$$HistoryRequestModelImplCopyWith<$Res> { + __$$HistoryRequestModelImplCopyWithImpl(_$HistoryRequestModelImpl _value, + $Res Function(_$HistoryRequestModelImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? historyId = null, + Object? metaData = null, + Object? httpRequestModel = null, + Object? httpResponseModel = null, + }) { + return _then(_$HistoryRequestModelImpl( + historyId: null == historyId + ? _value.historyId + : historyId // ignore: cast_nullable_to_non_nullable + as String, + metaData: null == metaData + ? _value.metaData + : metaData // ignore: cast_nullable_to_non_nullable + as HistoryMetaModel, + httpRequestModel: null == httpRequestModel + ? _value.httpRequestModel + : httpRequestModel // ignore: cast_nullable_to_non_nullable + as HttpRequestModel, + httpResponseModel: null == httpResponseModel + ? _value.httpResponseModel + : httpResponseModel // ignore: cast_nullable_to_non_nullable + as HttpResponseModel, + )); + } +} + +/// @nodoc + +@JsonSerializable(explicitToJson: true, anyMap: true) +class _$HistoryRequestModelImpl implements _HistoryRequestModel { + const _$HistoryRequestModelImpl( + {required this.historyId, + required this.metaData, + required this.httpRequestModel, + required this.httpResponseModel}); + + factory _$HistoryRequestModelImpl.fromJson(Map json) => + _$$HistoryRequestModelImplFromJson(json); + + @override + final String historyId; + @override + final HistoryMetaModel metaData; + @override + final HttpRequestModel httpRequestModel; + @override + final HttpResponseModel httpResponseModel; + + @override + String toString() { + return 'HistoryRequestModel(historyId: $historyId, metaData: $metaData, httpRequestModel: $httpRequestModel, httpResponseModel: $httpResponseModel)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$HistoryRequestModelImpl && + (identical(other.historyId, historyId) || + other.historyId == historyId) && + (identical(other.metaData, metaData) || + other.metaData == metaData) && + (identical(other.httpRequestModel, httpRequestModel) || + other.httpRequestModel == httpRequestModel) && + (identical(other.httpResponseModel, httpResponseModel) || + other.httpResponseModel == httpResponseModel)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, historyId, metaData, httpRequestModel, httpResponseModel); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$HistoryRequestModelImplCopyWith<_$HistoryRequestModelImpl> get copyWith => + __$$HistoryRequestModelImplCopyWithImpl<_$HistoryRequestModelImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$HistoryRequestModelImplToJson( + this, + ); + } +} + +abstract class _HistoryRequestModel implements HistoryRequestModel { + const factory _HistoryRequestModel( + {required final String historyId, + required final HistoryMetaModel metaData, + required final HttpRequestModel httpRequestModel, + required final HttpResponseModel httpResponseModel}) = + _$HistoryRequestModelImpl; + + factory _HistoryRequestModel.fromJson(Map json) = + _$HistoryRequestModelImpl.fromJson; + + @override + String get historyId; + @override + HistoryMetaModel get metaData; + @override + HttpRequestModel get httpRequestModel; + @override + HttpResponseModel get httpResponseModel; + @override + @JsonKey(ignore: true) + _$$HistoryRequestModelImplCopyWith<_$HistoryRequestModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/history_request_model.g.dart b/lib/models/history_request_model.g.dart new file mode 100644 index 00000000..830d7a4c --- /dev/null +++ b/lib/models/history_request_model.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'history_request_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$HistoryRequestModelImpl _$$HistoryRequestModelImplFromJson(Map json) => + _$HistoryRequestModelImpl( + historyId: json['historyId'] as String, + metaData: HistoryMetaModel.fromJson( + Map.from(json['metaData'] as Map)), + httpRequestModel: HttpRequestModel.fromJson( + Map.from(json['httpRequestModel'] as Map)), + httpResponseModel: HttpResponseModel.fromJson( + Map.from(json['httpResponseModel'] as Map)), + ); + +Map _$$HistoryRequestModelImplToJson( + _$HistoryRequestModelImpl instance) => + { + 'historyId': instance.historyId, + 'metaData': instance.metaData.toJson(), + 'httpRequestModel': instance.httpRequestModel.toJson(), + 'httpResponseModel': instance.httpResponseModel.toJson(), + }; diff --git a/lib/models/models.dart b/lib/models/models.dart index 63949949..2471d12c 100644 --- a/lib/models/models.dart +++ b/lib/models/models.dart @@ -1,5 +1,7 @@ export 'environment_model.dart'; export 'form_data_model.dart'; +export 'history_meta_model.dart'; +export 'history_request_model.dart'; export 'http_request_model.dart'; export 'http_response_model.dart'; export 'name_value_model.dart'; diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 5230e39f..d86d0836 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -240,12 +240,28 @@ class CollectionStateNotifier httpResponseModel: responseModel, isWorking: false, ); + String newHistoryId = getNewUuid(); + HistoryRequestModel model = HistoryRequestModel( + historyId: newHistoryId, + metaData: HistoryMetaModel( + historyId: newHistoryId, + name: requestModel.name, + url: substitutedHttpRequestModel.url, + method: substitutedHttpRequestModel.method, + responseStatus: statusCode, + timeStamp: DateTime.now(), + ), + httpRequestModel: substitutedHttpRequestModel, + httpResponseModel: responseModel, + ); + ref.read(historyMetaStateNotifier.notifier).addHistoryRequest(model); } // update state with response data map = {...state!}; map[id] = newRequestModel; state = map; + ref.read(hasUnsavedChangesProvider.notifier).state = true; } diff --git a/lib/providers/history_providers.dart b/lib/providers/history_providers.dart new file mode 100644 index 00000000..3f7d7c95 --- /dev/null +++ b/lib/providers/history_providers.dart @@ -0,0 +1,91 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/models/models.dart'; +import '../services/services.dart' show hiveHandler, HiveHandler; +import '../utils/history_utils.dart'; + +final selectedHistoryIdStateProvider = StateProvider((ref) => null); + +final selectedRequestGroupStateProvider = StateProvider((ref) { + final selectedHistoryId = ref.watch(selectedHistoryIdStateProvider); + if (selectedHistoryId == null) { + return null; + } + final historyMetaState = ref.read(historyMetaStateNotifier); + return getHistoryRequestKey(historyMetaState![selectedHistoryId]!); +}); + +final selectedHistoryRequestModelProvider = + StateProvider((ref) => null); + +final historySequenceProvider = + StateProvider>?>((ref) { + final historyMetas = ref.watch(historyMetaStateNotifier); + return getTemporalGroups(historyMetas?.values.toList()); +}); + +final StateNotifierProvider?> historyMetaStateNotifier = + StateNotifierProvider((ref) => HistoryMetaStateNotifier(ref, hiveHandler)); + +class HistoryMetaStateNotifier + extends StateNotifier?> { + HistoryMetaStateNotifier(this.ref, this.hiveHandler) : super(null) { + var status = loadHistoryMetas(); + Future.microtask(() { + if (status) { + final temporalGroups = getTemporalGroups(state?.values.toList()); + final latestRequestId = getLatestRequestId(temporalGroups); + if (latestRequestId != null) { + loadHistoryRequest(latestRequestId); + } + } + }); + } + + final Ref ref; + final HiveHandler hiveHandler; + + bool loadHistoryMetas() { + List? historyIds = hiveHandler.getHistoryIds(); + if (historyIds == null || historyIds.isEmpty) { + state = null; + return false; + } else { + Map historyMetaMap = {}; + for (var historyId in historyIds) { + var jsonModel = hiveHandler.getHistoryMeta(historyId); + if (jsonModel != null) { + var jsonMap = Map.from(jsonModel); + var historyMetaModelFromJson = HistoryMetaModel.fromJson(jsonMap); + historyMetaMap[historyId] = historyMetaModelFromJson; + } + } + state = historyMetaMap; + return true; + } + } + + Future loadHistoryRequest(String id) async { + var jsonModel = await hiveHandler.getHistoryRequest(id); + if (jsonModel != null) { + var jsonMap = Map.from(jsonModel); + var historyRequestModelFromJson = HistoryRequestModel.fromJson(jsonMap); + ref.read(selectedHistoryRequestModelProvider.notifier).state = + historyRequestModelFromJson; + ref.read(selectedHistoryIdStateProvider.notifier).state = id; + } + } + + void addHistoryRequest(HistoryRequestModel model) async { + final id = model.historyId; + state = { + ...state ?? {}, + id: model.metaData, + }; + final List updatedHistoryKeys = + state == null ? [id] : [...state!.keys, id]; + hiveHandler.setHistoryIds(updatedHistoryKeys); + hiveHandler.setHistoryMeta(id, model.metaData.toJson()); + await hiveHandler.setHistoryRequest(id, model.toJson()); + } +} diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart index 32f808ae..29fc6e59 100644 --- a/lib/providers/providers.dart +++ b/lib/providers/providers.dart @@ -1,4 +1,5 @@ export 'collection_providers.dart'; export 'environment_providers.dart'; +export 'history_providers.dart'; export 'settings_providers.dart'; export 'ui_providers.dart'; diff --git a/lib/screens/about_dialog.dart b/lib/screens/about_dialog.dart deleted file mode 100644 index 692b6743..00000000 --- a/lib/screens/about_dialog.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:apidash/consts.dart'; -import 'package:flutter/material.dart'; -import 'package:apidash/widgets/widgets.dart'; - -showAboutAppDialog( - BuildContext context, -) { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - contentPadding: kPt20 + kPh20, - content: Container( - width: double.infinity, - height: double.infinity, - constraints: const BoxConstraints(maxWidth: 540, maxHeight: 544), - child: const IntroMessage(), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text("Close"), - ), - ], - ); - }); -} diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index a841ca6f..c9b57475 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -1,4 +1,4 @@ -import 'package:apidash/screens/about_dialog.dart'; +import 'package:apidash/screens/history/history_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; @@ -118,7 +118,9 @@ class Dashboard extends ConsumerWidget { EnvironmentPage( scaffoldKey: mobileScaffoldKey, ), - const SizedBox(), + HistoryPage( + scaffoldKey: mobileScaffoldKey, + ), const SettingsPage(), ], ), diff --git a/lib/screens/envvar/environment_editor.dart b/lib/screens/envvar/environment_editor.dart index 53b17206..b4591e35 100644 --- a/lib/screens/envvar/environment_editor.dart +++ b/lib/screens/envvar/environment_editor.dart @@ -19,7 +19,7 @@ class EnvironmentEditor extends ConsumerWidget { padding: context.isMediumWindow ? kPb10 : (kIsMacOS || kIsWindows) - ? kPt24o8 + ? kPt28o8 : kP8, child: Column( children: [ @@ -65,34 +65,40 @@ class EnvironmentEditor extends ConsumerWidget { kVSpacer5, Expanded( child: Container( - padding: context.isMediumWindow ? null : kPv6, margin: context.isMediumWindow ? null : kP4, - decoration: context.isMediumWindow - ? null - : BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.outlineVariant, - width: 1, - ), - borderRadius: kBorderRadius12, - ), - child: const Column( - children: [ - kHSpacer40, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Card( + margin: EdgeInsets.zero, + color: kColorTransparent, + surfaceTintColor: kColorTransparent, + shape: RoundedRectangleBorder( + side: BorderSide( + color: + Theme.of(context).colorScheme.surfaceContainerHighest, + ), + borderRadius: kBorderRadius12, + ), + elevation: 0, + child: const Padding( + padding: kPv6, + child: Column( children: [ - SizedBox(width: 30), - Text("Variable"), - SizedBox(width: 30), - Text("Value"), - SizedBox(width: 40), + kHSpacer40, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(width: 30), + Text("Variable"), + SizedBox(width: 30), + Text("Value"), + SizedBox(width: 40), + ], + ), + kHSpacer40, + Divider(), + Expanded(child: EditEnvironmentVariables()) ], ), - kHSpacer40, - Divider(), - Expanded(child: EditEnvironmentVariables()) - ], + ), ), ), ), diff --git a/lib/screens/history/history_details.dart b/lib/screens/history/history_details.dart new file mode 100644 index 00000000..e32ca2db --- /dev/null +++ b/lib/screens/history/history_details.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class HistoryDetails extends StatelessWidget { + const HistoryDetails({super.key}); + + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/lib/screens/history/history_page.dart b/lib/screens/history/history_page.dart new file mode 100644 index 00000000..69f105ab --- /dev/null +++ b/lib/screens/history/history_page.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/extensions/extensions.dart'; +import 'package:apidash/providers/providers.dart'; +import 'history_pane.dart'; +import 'history_viewer.dart'; + +class HistoryPage extends ConsumerWidget { + const HistoryPage({ + super.key, + required this.scaffoldKey, + }); + + final GlobalKey scaffoldKey; + @override + Widget build(BuildContext context, WidgetRef ref) { + final historyModel = ref.watch(selectedHistoryRequestModelProvider); + if (context.isMediumWindow) { + return DrawerSplitView( + scaffoldKey: scaffoldKey, + mainContent: const HistoryViewer(), + title: Text(historyModel?.historyId ?? 'History'), + leftDrawerContent: const HistoryPane(), + actions: const [SizedBox(width: 16)], + onDrawerChanged: (value) => + ref.read(leftDrawerStateProvider.notifier).state = value, + ); + } + return const Column( + children: [ + Expanded( + child: DashboardSplitView( + sidebarWidget: HistoryPane(), + mainWidget: HistoryViewer(), + ), + ), + ], + ); + } +} diff --git a/lib/screens/history/history_pane.dart b/lib/screens/history/history_pane.dart new file mode 100644 index 00000000..7b32149c --- /dev/null +++ b/lib/screens/history/history_pane.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:apidash/extensions/extensions.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; + +class HistoryPane extends ConsumerWidget { + const HistoryPane({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Padding( + padding: (!context.isMediumWindow && kIsMacOS + ? kP24CollectionPane + : kP8CollectionPane) + + (context.isMediumWindow ? kPb70 : EdgeInsets.zero), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [kVSpacer10, Expanded(child: HistoryList()), kVSpacer5], + ), + ); + } +} + +class HistoryList extends HookConsumerWidget { + const HistoryList({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final historySequence = ref.watch(historySequenceProvider); + final alwaysShowHistoryPaneScrollbar = ref.watch(settingsProvider + .select((value) => value.alwaysShowCollectionPaneScrollbar)); + final List? sortedHistoryKeys = historySequence?.keys.toList(); + sortedHistoryKeys?.sort((a, b) => b.compareTo(a)); + ScrollController scrollController = useScrollController(); + return Scrollbar( + controller: scrollController, + thumbVisibility: alwaysShowHistoryPaneScrollbar, + radius: const Radius.circular(12), + child: ListView( + padding: context.isMediumWindow + ? EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom, + right: 8, + ) + : kPe8, + controller: scrollController, + children: sortedHistoryKeys != null + ? sortedHistoryKeys.map((date) { + var items = historySequence![date]!; + final requestGroups = getRequestGroups(items); + return Column( + children: [ + ExpansionTile( + title: Text( + humanizeDate(date), + ), + children: requestGroups.values.map((item) { + return Padding( + padding: kPv2 + kPh4, + child: SidebarHistoryCard( + id: item.first.historyId, + models: item, + method: item.first.method, + selectedId: + ref.watch(selectedRequestGroupStateProvider), + requestGroupSize: item.length, + onTap: () { + ref + .read(historyMetaStateNotifier.notifier) + .loadHistoryRequest(item.first.historyId); + }, + ), + ); + }).toList(), + ), + ], + ); + }).toList() + : [ + const Text( + 'No history', + style: TextStyle(color: Colors.grey), + ) + ], + ), + ); + } +} diff --git a/lib/screens/history/history_requests.dart b/lib/screens/history/history_requests.dart new file mode 100644 index 00000000..a38b09f1 --- /dev/null +++ b/lib/screens/history/history_requests.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class HistoryRequests extends StatelessWidget { + const HistoryRequests({super.key}); + + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/lib/screens/history/history_viewer.dart b/lib/screens/history/history_viewer.dart new file mode 100644 index 00000000..4dbb5faf --- /dev/null +++ b/lib/screens/history/history_viewer.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/extensions/extensions.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/consts.dart'; +import 'history_details.dart'; +import 'history_requests.dart'; + +class HistoryViewer extends StatelessWidget { + const HistoryViewer({super.key}); + + @override + Widget build(BuildContext context) { + if (context.isMediumWindow) { + return const HistoryDetails(); + } + return Padding( + padding: kIsMacOS || kIsWindows ? kPt28o8 : kP8, + child: Card( + color: kColorTransparent, + surfaceTintColor: kColorTransparent, + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + ), + borderRadius: kBorderRadius12, + ), + elevation: 0, + child: const HistorySplitView( + sidebarWidget: HistoryRequests(), + mainWidget: HistoryDetails(), + ), + ), + ); + } +} diff --git a/lib/screens/home_page/editor_pane/editor_request.dart b/lib/screens/home_page/editor_pane/editor_request.dart index 9d01fe2f..52f49a3f 100644 --- a/lib/screens/home_page/editor_pane/editor_request.dart +++ b/lib/screens/home_page/editor_pane/editor_request.dart @@ -27,7 +27,7 @@ class RequestEditor extends StatelessWidget { ), ) : Padding( - padding: kIsMacOS || kIsWindows ? kPt24o8 : kP8, + padding: kIsMacOS || kIsWindows ? kPt28o8 : kP8, child: const Column( children: [ RequestEditorTopBar(), diff --git a/lib/screens/mobile/dashboard.dart b/lib/screens/mobile/dashboard.dart index 9dbcae9c..a74e6094 100644 --- a/lib/screens/mobile/dashboard.dart +++ b/lib/screens/mobile/dashboard.dart @@ -1,3 +1,4 @@ +import 'package:apidash/screens/history/history_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -39,7 +40,7 @@ class _MobileDashboardState extends ConsumerState { ), if (context.isMediumWindow) AnimatedPositioned( - bottom: railIdx > 1 + bottom: railIdx > 2 ? 0 : isLeftDrawerOpen ? 0 @@ -72,17 +73,9 @@ class PageBranch extends ConsumerWidget { return EnvironmentPage( scaffoldKey: scaffoldKey, ); - // case 2: - // // TODO: Implement history page - // return const PageBase( - // title: 'History', - // scaffoldBody: SizedBox(), - // ); case 2: - // TODO: Implement history page - return const PageBase( - title: 'History', - scaffoldBody: SizedBox(), + return HistoryPage( + scaffoldKey: scaffoldKey, ); case 3: return const PageBase( diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index 98c9f529..c7426524 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -1,4 +1,3 @@ -import 'package:apidash/screens/about_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/providers.dart'; diff --git a/lib/services/hive_services.dart b/lib/services/hive_services.dart index 0b5a86fc..c6a3cc40 100644 --- a/lib/services/hive_services.dart +++ b/lib/services/hive_services.dart @@ -3,9 +3,14 @@ import 'package:hive_flutter/hive_flutter.dart'; const String kDataBox = "apidash-data"; const String kKeyDataBoxIds = "ids"; + const String kEnvironmentBox = "apidash-environments"; const String kKeyEnvironmentBoxIds = "environmentIds"; +const String kHistoryMetaBox = "apidash-history-meta"; +const String kHistoryBoxIds = "historyIds"; +const String kHistoryLazyBox = "apidash-history-lazy"; + const String kSettingsBox = "apidash-settings"; Future openBoxes() async { @@ -13,6 +18,8 @@ Future openBoxes() async { await Hive.openBox(kDataBox); await Hive.openBox(kSettingsBox); await Hive.openBox(kEnvironmentBox); + await Hive.openBox(kHistoryMetaBox); + await Hive.openLazyBox(kHistoryLazyBox); } (Size?, Offset?) getInitialSize() { @@ -38,11 +45,15 @@ class HiveHandler { late final Box dataBox; late final Box settingsBox; late final Box environmentBox; + late final Box historyMetaBox; + late final LazyBox historyLazyBox; HiveHandler() { dataBox = Hive.box(kDataBox); settingsBox = Hive.box(kSettingsBox); environmentBox = Hive.box(kEnvironmentBox); + historyMetaBox = Hive.box(kHistoryMetaBox); + historyLazyBox = Hive.lazyBox(kHistoryLazyBox); } Map get settings => settingsBox.toMap(); @@ -69,6 +80,25 @@ class HiveHandler { Future deleteEnvironment(String id) => environmentBox.delete(id); + dynamic getHistoryIds() => historyMetaBox.get(kHistoryBoxIds); + Future setHistoryIds(List? ids) => + historyMetaBox.put(kHistoryBoxIds, ids); + + dynamic getHistoryMeta(String id) => historyMetaBox.get(id); + Future setHistoryMeta( + String id, Map? historyMetaJson) => + historyMetaBox.put(id, historyMetaJson); + + Future deleteHistoryMeta(String id) => historyMetaBox.delete(id); + + Future getHistoryRequest(String id) async => + await historyLazyBox.get(id); + Future setHistoryRequest( + String id, Map? historyRequestJsoon) => + historyLazyBox.put(id, historyRequestJsoon); + + Future deleteHistoryReqyest(String id) => historyLazyBox.delete(id); + Future clear() async { await dataBox.clear(); await environmentBox.clear(); diff --git a/lib/utils/convert_utils.dart b/lib/utils/convert_utils.dart index e855568b..735ace25 100644 --- a/lib/utils/convert_utils.dart +++ b/lib/utils/convert_utils.dart @@ -1,10 +1,18 @@ import 'dart:typed_data'; import 'dart:convert'; import 'package:collection/collection.dart'; +import 'package:intl/intl.dart'; import '../models/models.dart'; import '../consts.dart'; import 'package:http/http.dart' as http; +String humanizeDate(DateTime? date) { + if (date == null) { + return ""; + } + return DateFormat('MMMM d, yyyy').format(date); +} + String humanizeDuration(Duration? duration) { if (duration == null) { return ""; diff --git a/lib/utils/history_utils.dart b/lib/utils/history_utils.dart new file mode 100644 index 00000000..b1d62fb5 --- /dev/null +++ b/lib/utils/history_utils.dart @@ -0,0 +1,86 @@ +import 'package:apidash/models/models.dart'; +import 'package:apidash/utils/convert_utils.dart'; + +DateTime stripTime(DateTime dateTime) { + return DateTime(dateTime.year, dateTime.month, dateTime.day); +} + +String getHistoryRequestName(HistoryMetaModel model) { + if (model.name.isNotEmpty) { + return model.name; + } else { + return model.url; + } +} + +String getHistoryRequestKey(HistoryMetaModel model) { + String timeStamp = humanizeDate(model.timeStamp); + if (model.name.isNotEmpty) { + return model.name + model.method.name + timeStamp; + } else { + return model.url + model.method.name + timeStamp; + } +} + +String? getLatestRequestId( + Map> temporalGroups) { + if (temporalGroups.isEmpty) { + return null; + } + List keys = temporalGroups.keys.toList(); + keys.sort((a, b) => b.compareTo(a)); + return temporalGroups[keys.first]!.first.historyId; +} + +DateTime getDateTimeKey(List keys, DateTime currentKey) { + if (keys.isEmpty) return currentKey; + for (DateTime key in keys) { + if (key.year == currentKey.year && + key.month == currentKey.month && + key.day == currentKey.day) { + return key; + } + } + return stripTime(currentKey); +} + +Map> getTemporalGroups( + List? models) { + Map> temporalGroups = {}; + if (models?.isEmpty ?? true) { + return temporalGroups; + } + for (HistoryMetaModel model in models!) { + List existingKeys = temporalGroups.keys.toList(); + DateTime key = getDateTimeKey(existingKeys, model.timeStamp); + if (existingKeys.contains(key)) { + temporalGroups[key]!.add(model); + } else { + temporalGroups[stripTime(key)] = [model]; + } + } + temporalGroups.forEach((key, value) { + value.sort((a, b) => b.timeStamp.compareTo(a.timeStamp)); + }); + return temporalGroups; +} + +Map> getRequestGroups( + List? models) { + Map> historyGroups = {}; + if (models?.isEmpty ?? true) { + return historyGroups; + } + for (HistoryMetaModel model in models!) { + String key = getHistoryRequestKey(model); + if (historyGroups.containsKey(key)) { + historyGroups[key]!.add(model); + } else { + historyGroups[key] = [model]; + } + } + historyGroups.forEach((key, value) { + value.sort((a, b) => b.timeStamp.compareTo(a.timeStamp)); + }); + return historyGroups; +} diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 1857825d..d38e34b8 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1,6 +1,7 @@ export 'ui_utils.dart'; export 'convert_utils.dart'; export 'header_utils.dart'; +export 'history_utils.dart'; export 'http_utils.dart'; export 'file_utils.dart'; export 'window_utils.dart'; diff --git a/lib/widgets/card_sidebar_history.dart b/lib/widgets/card_sidebar_history.dart new file mode 100644 index 00000000..4a5e638d --- /dev/null +++ b/lib/widgets/card_sidebar_history.dart @@ -0,0 +1,110 @@ +import 'package:apidash/models/history_meta_model.dart'; +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/utils/utils.dart'; +import 'texts.dart' show MethodBox; + +class SidebarHistoryCard extends StatelessWidget { + const SidebarHistoryCard({ + super.key, + required this.id, + required this.models, + required this.method, + this.selectedId, + this.requestGroupSize = 1, + this.onTap, + }); + + final String id; + final List models; + final HTTPVerb method; + final String? selectedId; + final int requestGroupSize; + final Function()? onTap; + + @override + Widget build(BuildContext context) { + final Color color = Theme.of(context).colorScheme.surface; + final Color colorVariant = + Theme.of(context).colorScheme.surfaceContainerHighest.withOpacity(0.5); + final model = models.first; + final Color surfaceTint = Theme.of(context).colorScheme.primary; + bool isSelected = selectedId == getHistoryRequestKey(model); + final String name = getHistoryRequestName(model); + return Tooltip( + message: name, + triggerMode: TooltipTriggerMode.manual, + waitDuration: const Duration(seconds: 1), + child: Card( + shape: const RoundedRectangleBorder( + borderRadius: kBorderRadius8, + ), + elevation: isSelected ? 1 : 0, + surfaceTintColor: isSelected ? surfaceTint : null, + color: isSelected + ? Theme.of(context).colorScheme.brightness == Brightness.dark + ? colorVariant + : color + : color, + margin: EdgeInsets.zero, + child: InkWell( + onTap: onTap, + borderRadius: kBorderRadius8, + hoverColor: colorVariant, + focusColor: colorVariant.withOpacity(0.5), + child: Padding( + padding: const EdgeInsets.only( + left: 6, + right: 6, + top: 5, + bottom: 5, + ), + child: SizedBox( + height: 20, + child: Row( + children: [ + MethodBox(method: method), + kHSpacer4, + Expanded( + child: Text( + name, + softWrap: false, + overflow: TextOverflow.fade, + ), + ), + requestGroupSize > 1 ? kHSpacer4 : const SizedBox.shrink(), + Visibility( + visible: requestGroupSize > 1, + child: Container( + padding: kPh4, + constraints: const BoxConstraints(minWidth: 24), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: kBorderRadius6, + ), + child: Center( + child: Text( + requestGroupSize == 2 + ? requestGroupSize.toString() + : "9+", + style: Theme.of(context) + .textTheme + .labelSmall + ?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + )), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/splitview_history.dart b/lib/widgets/splitview_history.dart new file mode 100644 index 00000000..698a76db --- /dev/null +++ b/lib/widgets/splitview_history.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:multi_split_view/multi_split_view.dart'; +import 'package:apidash/consts.dart'; + +class HistorySplitView extends StatefulWidget { + const HistorySplitView({ + super.key, + required this.sidebarWidget, + required this.mainWidget, + }); + + final Widget sidebarWidget; + final Widget mainWidget; + + @override + HistorySplitViewState createState() => HistorySplitViewState(); +} + +class HistorySplitViewState extends State { + final MultiSplitViewController _controller = MultiSplitViewController( + areas: [ + Area(id: "sidebar", min: 200, size: 220, max: 300), + Area(id: "main"), + ], + ); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return MultiSplitViewTheme( + data: MultiSplitViewThemeData( + dividerThickness: 3, + dividerPainter: DividerPainters.background( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + highlightedColor: Theme.of(context).colorScheme.outline.withOpacity( + kHintOpacity, + ), + animationEnabled: false, + ), + ), + child: MultiSplitView( + controller: _controller, + sizeOverflowPolicy: SizeOverflowPolicy.shrinkFirst, + sizeUnderflowPolicy: SizeUnderflowPolicy.stretchLast, + builder: (context, area) { + return switch (area.id) { + "sidebar" => widget.sidebarWidget, + "main" => widget.mainWidget, + _ => Container(), + }; + }, + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 15f36376..cf8c84fe 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -6,6 +6,7 @@ export 'button_save_download.dart'; export 'button_send.dart'; export 'card_request_details.dart'; export 'card_sidebar_environment.dart'; +export 'card_sidebar_history.dart'; export 'card_sidebar_request.dart'; export 'checkbox.dart'; export 'code_previewer.dart'; @@ -42,6 +43,7 @@ export 'snackbars.dart'; export 'splitview_drawer.dart'; export 'splitview_dashboard.dart'; export 'splitview_equal.dart'; +export 'splitview_history.dart'; export 'suggestions_menu.dart'; export 'tables.dart'; export 'tabs.dart'; diff --git a/pubspec.lock b/pubspec.lock index 0e8ed727..47f739eb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -648,6 +648,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" io: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3d7b371b..1199ac67 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,7 +38,7 @@ dependencies: just_audio_mpv: ^0.1.7 just_audio_windows: ^0.2.0 freezed_annotation: ^2.4.1 - json_annotation: ^4.8.1 + json_annotation: ^4.9.0 printing: ^5.12.0 package_info_plus: ^8.0.0 flutter_typeahead: ^5.2.0 @@ -64,6 +64,7 @@ dependencies: flutter_hooks: ^0.20.5 flutter_portal: ^1.1.4 mention_tag_text_field: ^0.0.5 + intl: ^0.19.0 dependency_overrides: web: ^0.5.0 From cad6c97f89ce126d07ac3b3b1a56c70006f46c05 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Fri, 19 Jul 2024 18:11:37 +0530 Subject: [PATCH 02/71] wip: history details pane --- lib/consts.dart | 3 + lib/providers/history_providers.dart | 4 +- lib/screens/common_widgets/button_navbar.dart | 4 +- lib/screens/envvar/environment_editor.dart | 17 +++-- .../history/details_pane/url_card.dart | 66 +++++++++++++++++ lib/screens/history/history_details.dart | 36 +++++++++- lib/screens/history/history_pane.dart | 16 ++--- lib/screens/history/history_requests.dart | 28 +++++++- lib/screens/mobile/navbar.dart | 9 --- .../requests_page/request_response_tabs.dart | 72 +++---------------- lib/utils/history_utils.dart | 17 +++++ lib/widgets/card_sidebar_history.dart | 11 ++- lib/widgets/error_message.dart | 66 ++++++++--------- lib/widgets/field_raw.dart | 3 + lib/widgets/splitview_history.dart | 2 +- lib/widgets/tabbar_request_response.dart | 56 +++++++++++++++ lib/widgets/widgets.dart | 1 + 17 files changed, 275 insertions(+), 136 deletions(-) create mode 100644 lib/screens/history/details_pane/url_card.dart create mode 100644 lib/widgets/tabbar_request_response.dart diff --git a/lib/consts.dart b/lib/consts.dart index 63e1946f..4d87cdf7 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -128,6 +128,9 @@ const kPt8 = EdgeInsets.only( const kPt20 = EdgeInsets.only( top: 20, ); +const kPt24 = EdgeInsets.only( + top: 24, +); const kPt28 = EdgeInsets.only( top: 28, ); diff --git a/lib/providers/history_providers.dart b/lib/providers/history_providers.dart index 3f7d7c95..6f5d23df 100644 --- a/lib/providers/history_providers.dart +++ b/lib/providers/history_providers.dart @@ -5,12 +5,12 @@ import '../utils/history_utils.dart'; final selectedHistoryIdStateProvider = StateProvider((ref) => null); -final selectedRequestGroupStateProvider = StateProvider((ref) { +final selectedRequestGroupIdStateProvider = StateProvider((ref) { final selectedHistoryId = ref.watch(selectedHistoryIdStateProvider); + final historyMetaState = ref.read(historyMetaStateNotifier); if (selectedHistoryId == null) { return null; } - final historyMetaState = ref.read(historyMetaStateNotifier); return getHistoryRequestKey(historyMetaState![selectedHistoryId]!); }); diff --git a/lib/screens/common_widgets/button_navbar.dart b/lib/screens/common_widgets/button_navbar.dart index 1daf31cd..2cefc201 100644 --- a/lib/screens/common_widgets/button_navbar.dart +++ b/lib/screens/common_widgets/button_navbar.dart @@ -37,7 +37,7 @@ class NavbarButton extends ConsumerWidget { if (buttonIdx != null) { ref.read(navRailIndexStateProvider.notifier).state = buttonIdx!; - if (railIdx > 1 && buttonIdx! <= 1) { + if (railIdx > 2 && buttonIdx! <= 2) { ref.read(leftDrawerStateProvider.notifier).state = false; } } @@ -62,7 +62,7 @@ class NavbarButton extends ConsumerWidget { if (buttonIdx != null) { ref.read(navRailIndexStateProvider.notifier).state = buttonIdx!; - if (railIdx > 1 && buttonIdx! <= 1) { + if (railIdx > 2 && buttonIdx! <= 2) { ref.read(leftDrawerStateProvider.notifier).state = false; } diff --git a/lib/screens/envvar/environment_editor.dart b/lib/screens/envvar/environment_editor.dart index b4591e35..934d7ad8 100644 --- a/lib/screens/envvar/environment_editor.dart +++ b/lib/screens/envvar/environment_editor.dart @@ -70,13 +70,16 @@ class EnvironmentEditor extends ConsumerWidget { margin: EdgeInsets.zero, color: kColorTransparent, surfaceTintColor: kColorTransparent, - shape: RoundedRectangleBorder( - side: BorderSide( - color: - Theme.of(context).colorScheme.surfaceContainerHighest, - ), - borderRadius: kBorderRadius12, - ), + shape: context.isMediumWindow + ? null + : RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context) + .colorScheme + .surfaceContainerHighest, + ), + borderRadius: kBorderRadius12, + ), elevation: 0, child: const Padding( padding: kPv6, diff --git a/lib/screens/history/details_pane/url_card.dart b/lib/screens/history/details_pane/url_card.dart new file mode 100644 index 00000000..af5d4a41 --- /dev/null +++ b/lib/screens/history/details_pane/url_card.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; + +class HistoryURLCard extends StatelessWidget { + const HistoryURLCard({ + super.key, + required this.method, + required this.url, + }); + + final HTTPVerb method; + final String url; + + @override + Widget build(BuildContext context) { + final fontSize = Theme.of(context).textTheme.titleMedium?.fontSize; + return LayoutBuilder(builder: (context, constraints) { + final isCompact = constraints.maxWidth <= kMinWindowSize.width; + return Card( + color: kColorTransparent, + surfaceTintColor: kColorTransparent, + elevation: 0, + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + ), + borderRadius: kBorderRadius8, + ), + child: Padding( + padding: EdgeInsets.symmetric( + vertical: 12, + horizontal: isCompact ? 10 : 16, + ), + child: Row( + children: [ + isCompact ? const SizedBox.shrink() : kHSpacer10, + Text( + method.name.toUpperCase(), + style: kCodeStyle.copyWith( + fontSize: fontSize, + fontWeight: FontWeight.bold, + color: getHTTPMethodColor( + method, + brightness: Theme.of(context).brightness, + ), + ), + ), + isCompact ? kHSpacer10 : kHSpacer20, + Expanded( + child: RawTextField( + readOnly: true, + controller: TextEditingController(text: url), + style: kCodeStyle.copyWith( + fontSize: fontSize, + ), + ), + ) + ], + ), + ), + ); + }); + } +} diff --git a/lib/screens/history/history_details.dart b/lib/screens/history/history_details.dart index e32ca2db..cee748ba 100644 --- a/lib/screens/history/history_details.dart +++ b/lib/screens/history/history_details.dart @@ -1,10 +1,42 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/consts.dart'; +import './details_pane/url_card.dart'; -class HistoryDetails extends StatelessWidget { +class HistoryDetails extends StatefulHookConsumerWidget { const HistoryDetails({super.key}); + @override + ConsumerState createState() => _HistoryDetailsState(); +} + +class _HistoryDetailsState extends ConsumerState + with TickerProviderStateMixin { @override Widget build(BuildContext context) { - return Container(); + final selectedHistoryRequest = + ref.watch(selectedHistoryRequestModelProvider); + final metaData = selectedHistoryRequest?.metaData; + + final TabController controller = + useTabController(initialLength: 2, vsync: this); + + return selectedHistoryRequest != null + ? Column( + children: [ + Padding( + padding: kP4, + child: HistoryURLCard( + method: metaData!.method, url: metaData.url)), + kVSpacer10, + RequestResponseTabbar( + controller: controller, + ), + ], + ) + : const Text("No Request Selected"); } } diff --git a/lib/screens/history/history_pane.dart b/lib/screens/history/history_pane.dart index 7b32149c..00620f92 100644 --- a/lib/screens/history/history_pane.dart +++ b/lib/screens/history/history_pane.dart @@ -15,9 +15,7 @@ class HistoryPane extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return Padding( - padding: (!context.isMediumWindow && kIsMacOS - ? kP24CollectionPane - : kP8CollectionPane) + + padding: (!context.isMediumWindow && kIsMacOS ? kPt24 : kPt8) + (context.isMediumWindow ? kPb70 : EdgeInsets.zero), child: const Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -34,6 +32,7 @@ class HistoryList extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final selectedGroupId = ref.watch(selectedRequestGroupIdStateProvider); final historySequence = ref.watch(historySequenceProvider); final alwaysShowHistoryPaneScrollbar = ref.watch(settingsProvider .select((value) => value.alwaysShowCollectionPaneScrollbar)); @@ -45,12 +44,7 @@ class HistoryList extends HookConsumerWidget { thumbVisibility: alwaysShowHistoryPaneScrollbar, radius: const Radius.circular(12), child: ListView( - padding: context.isMediumWindow - ? EdgeInsets.only( - bottom: MediaQuery.paddingOf(context).bottom, - right: 8, - ) - : kPe8, + padding: EdgeInsets.only(bottom: MediaQuery.paddingOf(context).bottom), controller: scrollController, children: sortedHistoryKeys != null ? sortedHistoryKeys.map((date) { @@ -69,8 +63,8 @@ class HistoryList extends HookConsumerWidget { id: item.first.historyId, models: item, method: item.first.method, - selectedId: - ref.watch(selectedRequestGroupStateProvider), + isSelected: selectedGroupId == + getHistoryRequestKey(item.first), requestGroupSize: item.length, onTap: () { ref diff --git a/lib/screens/history/history_requests.dart b/lib/screens/history/history_requests.dart index a38b09f1..1a80c2be 100644 --- a/lib/screens/history/history_requests.dart +++ b/lib/screens/history/history_requests.dart @@ -1,10 +1,32 @@ import 'package:flutter/material.dart'; +import 'package:apidash/providers/history_providers.dart'; +import 'package:apidash/utils/history_utils.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; -class HistoryRequests extends StatelessWidget { +class HistoryRequests extends ConsumerWidget { const HistoryRequests({super.key}); @override - Widget build(BuildContext context) { - return Container(); + Widget build(BuildContext context, WidgetRef ref) { + final selectedRequestId = ref.watch(selectedHistoryIdStateProvider); + final selectedRequest = ref.read(selectedHistoryRequestModelProvider); + final historyMetas = ref.read(historyMetaStateNotifier); + final requestGroup = getRequestGroup( + historyMetas?.values.toList(), selectedRequest?.metaData); + return Column( + children: requestGroup + .map((request) => SidebarHistoryCard( + id: request.historyId, + method: request.method, + isSelected: selectedRequestId == request.historyId, + onTap: () { + ref.read(selectedHistoryIdStateProvider.notifier).state = + request.historyId; + }, + models: [request], + )) + .toList(), + ); } } diff --git a/lib/screens/mobile/navbar.dart b/lib/screens/mobile/navbar.dart index 04caf61a..5449f45f 100644 --- a/lib/screens/mobile/navbar.dart +++ b/lib/screens/mobile/navbar.dart @@ -57,15 +57,6 @@ class BottomNavBar extends ConsumerWidget { label: 'History', ), ), - // Expanded( - // child: NavbarButton( - // railIdx: railIdx, - // buttonIdx: 2, - // selectedIcon: Icons.history, - // icon: Icons.history_outlined, - // label: 'History', - // ), - // ), Expanded( child: NavbarButton( railIdx: railIdx, diff --git a/lib/screens/mobile/requests_page/request_response_tabs.dart b/lib/screens/mobile/requests_page/request_response_tabs.dart index 38fab16a..36ad6c11 100644 --- a/lib/screens/mobile/requests_page/request_response_tabs.dart +++ b/lib/screens/mobile/requests_page/request_response_tabs.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; import '../../home_page/editor_pane/details_card/response_pane.dart'; import '../../home_page/editor_pane/editor_request.dart'; @@ -30,72 +31,21 @@ class RequestResponseTabs extends StatelessWidget { } } -class RequestResponseTabbar extends StatelessWidget { - const RequestResponseTabbar({ - super.key, - required this.controller, - }); - - final TabController controller; - - @override - Widget build(BuildContext context) { - return Center( - child: Container( - width: kReqResTabWidth, - height: kReqResTabHeight, - decoration: BoxDecoration( - borderRadius: kBorderRadius20, - border: Border.all( - color: Theme.of(context).colorScheme.outlineVariant, - ), - ), - child: ClipRRect( - borderRadius: kBorderRadius20, - child: TabBar( - dividerColor: Colors.transparent, - indicatorWeight: 0.0, - indicatorSize: TabBarIndicatorSize.tab, - unselectedLabelColor: - Theme.of(context).colorScheme.onSurface.withOpacity(0.4), - labelStyle: kTextStyleTab.copyWith( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onPrimary, - ), - unselectedLabelStyle: kTextStyleTab, - splashBorderRadius: kBorderRadius20, - indicator: BoxDecoration( - borderRadius: kBorderRadius20, - color: Theme.of(context).colorScheme.primary, - ), - controller: controller, - tabs: const [ - Tab( - text: kLabelRequest, - ), - Tab( - text: kLabelResponse, - ), - ], - ), - ), - ), - ); - } -} - class RequestResponseTabviews extends StatelessWidget { const RequestResponseTabviews({super.key, required this.controller}); final TabController controller; @override Widget build(BuildContext context) { - return TabBarView(controller: controller, children: const [ - RequestEditor(), - Padding( - padding: kPt8, - child: ResponsePane(), - ), - ]); + return TabBarView( + controller: controller, + children: const [ + RequestEditor(), + Padding( + padding: kPt8, + child: ResponsePane(), + ), + ], + ); } } diff --git a/lib/utils/history_utils.dart b/lib/utils/history_utils.dart index b1d62fb5..de04cd24 100644 --- a/lib/utils/history_utils.dart +++ b/lib/utils/history_utils.dart @@ -84,3 +84,20 @@ Map> getRequestGroups( }); return historyGroups; } + +List getRequestGroup( + List? models, HistoryMetaModel? selectedModel) { + List requestGroup = []; + if (selectedModel == null || (models?.isEmpty ?? true)) { + return requestGroup; + } + String selectedModelKey = getHistoryRequestKey(selectedModel); + for (HistoryMetaModel model in models!) { + String key = getHistoryRequestKey(model); + if (key == selectedModelKey) { + requestGroup.add(model); + } + } + requestGroup.sort((a, b) => b.timeStamp.compareTo(a.timeStamp)); + return requestGroup; +} diff --git a/lib/widgets/card_sidebar_history.dart b/lib/widgets/card_sidebar_history.dart index 4a5e638d..be6387b3 100644 --- a/lib/widgets/card_sidebar_history.dart +++ b/lib/widgets/card_sidebar_history.dart @@ -10,7 +10,7 @@ class SidebarHistoryCard extends StatelessWidget { required this.id, required this.models, required this.method, - this.selectedId, + this.isSelected = false, this.requestGroupSize = 1, this.onTap, }); @@ -18,7 +18,7 @@ class SidebarHistoryCard extends StatelessWidget { final String id; final List models; final HTTPVerb method; - final String? selectedId; + final bool isSelected; final int requestGroupSize; final Function()? onTap; @@ -29,7 +29,6 @@ class SidebarHistoryCard extends StatelessWidget { Theme.of(context).colorScheme.surfaceContainerHighest.withOpacity(0.5); final model = models.first; final Color surfaceTint = Theme.of(context).colorScheme.primary; - bool isSelected = selectedId == getHistoryRequestKey(model); final String name = getHistoryRequestName(model); return Tooltip( message: name, @@ -84,9 +83,9 @@ class SidebarHistoryCard extends StatelessWidget { ), child: Center( child: Text( - requestGroupSize == 2 - ? requestGroupSize.toString() - : "9+", + requestGroupSize > 9 + ? "9+" + : requestGroupSize.toString(), style: Theme.of(context) .textTheme .labelSmall diff --git a/lib/widgets/error_message.dart b/lib/widgets/error_message.dart index b6e9cf46..ed8a2cb8 100644 --- a/lib/widgets/error_message.dart +++ b/lib/widgets/error_message.dart @@ -20,38 +20,40 @@ class ErrorMessage extends StatelessWidget { return Padding( padding: kPh20v10, child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - showIcon - ? Icon( - Icons.warning_rounded, - size: 40, - color: color, - ) - : const SizedBox(), - SelectableText( - message ?? 'An error occurred. $kUnexpectedRaiseIssue', - textAlign: TextAlign.center, - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith(color: color), - ), - kVSpacer20, - showIssueButton - ? FilledButton.tonalIcon( - onPressed: () { - launchUrl(Uri.parse(kGitUrl)); - }, - icon: const Icon(Icons.arrow_outward_rounded), - label: Text( - 'Raise Issue', - style: Theme.of(context).textTheme.titleMedium, - ), - ) - : const SizedBox(), - ], + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + showIcon + ? Icon( + Icons.warning_rounded, + size: 40, + color: color, + ) + : const SizedBox(), + SelectableText( + message ?? 'An error occurred. $kUnexpectedRaiseIssue', + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(color: color), + ), + kVSpacer20, + showIssueButton + ? FilledButton.tonalIcon( + onPressed: () { + launchUrl(Uri.parse(kGitUrl)); + }, + icon: const Icon(Icons.arrow_outward_rounded), + label: Text( + 'Raise Issue', + style: Theme.of(context).textTheme.titleMedium, + ), + ) + : const SizedBox(), + ], + ), ), ), ); diff --git a/lib/widgets/field_raw.dart b/lib/widgets/field_raw.dart index 8adf6fd0..1f216b74 100644 --- a/lib/widgets/field_raw.dart +++ b/lib/widgets/field_raw.dart @@ -8,16 +8,19 @@ class RawTextField extends StatelessWidget { this.controller, this.hintText, this.style, + this.readOnly = false, }); final void Function(String)? onChanged; final TextEditingController? controller; final String? hintText; final TextStyle? style; + final bool readOnly; @override Widget build(BuildContext context) { return TextField( + readOnly: readOnly, controller: controller, onChanged: onChanged, style: style, diff --git a/lib/widgets/splitview_history.dart b/lib/widgets/splitview_history.dart index 698a76db..b955eaeb 100644 --- a/lib/widgets/splitview_history.dart +++ b/lib/widgets/splitview_history.dart @@ -19,7 +19,7 @@ class HistorySplitView extends StatefulWidget { class HistorySplitViewState extends State { final MultiSplitViewController _controller = MultiSplitViewController( areas: [ - Area(id: "sidebar", min: 200, size: 220, max: 300), + Area(id: "sidebar", min: 200, size: 250, max: 300), Area(id: "main"), ], ); diff --git a/lib/widgets/tabbar_request_response.dart b/lib/widgets/tabbar_request_response.dart new file mode 100644 index 00000000..5a117120 --- /dev/null +++ b/lib/widgets/tabbar_request_response.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +class RequestResponseTabbar extends StatelessWidget { + const RequestResponseTabbar({ + super.key, + required this.controller, + }); + + final TabController controller; + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + width: kReqResTabWidth, + height: kReqResTabHeight, + decoration: BoxDecoration( + borderRadius: kBorderRadius20, + border: Border.all( + color: Theme.of(context).colorScheme.outlineVariant, + ), + ), + child: ClipRRect( + borderRadius: kBorderRadius20, + child: TabBar( + dividerColor: Colors.transparent, + indicatorWeight: 0.0, + indicatorSize: TabBarIndicatorSize.tab, + unselectedLabelColor: + Theme.of(context).colorScheme.onSurface.withOpacity(0.4), + labelStyle: kTextStyleTab.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onPrimary, + ), + unselectedLabelStyle: kTextStyleTab, + splashBorderRadius: kBorderRadius20, + indicator: BoxDecoration( + borderRadius: kBorderRadius20, + color: Theme.of(context).colorScheme.primary, + ), + controller: controller, + tabs: const [ + Tab( + text: kLabelRequest, + ), + Tab( + text: kLabelResponse, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index cf8c84fe..81a7018d 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -45,6 +45,7 @@ export 'splitview_dashboard.dart'; export 'splitview_equal.dart'; export 'splitview_history.dart'; export 'suggestions_menu.dart'; +export 'tabbar_request_response.dart'; export 'tables.dart'; export 'tabs.dart'; export 'texts.dart'; From f8ede1edc876ba309be8a486a9cf55e8a2b46f6c Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sat, 20 Jul 2024 22:05:08 +0530 Subject: [PATCH 03/71] wip: history panes --- lib/providers/ui_providers.dart | 1 + .../code_pane.dart | 14 ++- .../common_widgets/common_widgets.dart | 1 + .../details_pane/his_request_pane.dart | 60 +++++++++++ .../details_pane/his_response_pane.dart | 48 +++++++++ .../history/details_pane/url_card.dart | 5 +- lib/screens/history/history_details.dart | 69 +++++++++--- lib/screens/history/history_page.dart | 6 +- lib/screens/history/history_pane.dart | 9 +- lib/screens/history/history_requests.dart | 23 ++-- .../details_card/details_card.dart | 2 +- .../mobile/requests_page/requests_page.dart | 1 - lib/utils/convert_utils.dart | 7 ++ lib/utils/history_utils.dart | 14 ++- lib/widgets/card_history_request.dart | 65 +++++++++++ lib/widgets/field_read_only.dart | 30 ++++++ lib/widgets/request_widgets.dart | 41 +++---- lib/widgets/response_widgets.dart | 9 +- lib/widgets/{tables.dart => table_map.dart} | 0 lib/widgets/table_request.dart | 101 ++++++++++++++++++ lib/widgets/texts.dart | 21 ++++ lib/widgets/widgets.dart | 6 +- .../csharp_rest_sharp_codgen_test.dart | 2 +- test/codegen/go_http_codegen_test.dart | 2 +- test/providers/ui_providers_test.dart | 3 - test/widgets/tables_test.dart | 2 +- 26 files changed, 472 insertions(+), 70 deletions(-) rename lib/screens/{home_page/editor_pane/details_card => common_widgets}/code_pane.dart (79%) create mode 100644 lib/screens/history/details_pane/his_request_pane.dart create mode 100644 lib/screens/history/details_pane/his_response_pane.dart create mode 100644 lib/widgets/card_history_request.dart create mode 100644 lib/widgets/field_read_only.dart rename lib/widgets/{tables.dart => table_map.dart} (100%) create mode 100644 lib/widgets/table_request.dart diff --git a/lib/providers/ui_providers.dart b/lib/providers/ui_providers.dart index 9b6cc3e0..0478b040 100644 --- a/lib/providers/ui_providers.dart +++ b/lib/providers/ui_providers.dart @@ -9,6 +9,7 @@ final navRailIndexStateProvider = StateProvider((ref) => 0); final selectedIdEditStateProvider = StateProvider((ref) => null); final environmentFieldEditStateProvider = StateProvider((ref) => null); final codePaneVisibleStateProvider = StateProvider((ref) => false); +final historyCodePaneVisibleStateProvider = StateProvider((ref) => false); final saveDataStateProvider = StateProvider((ref) => false); final clearDataStateProvider = StateProvider((ref) => false); final hasUnsavedChangesProvider = StateProvider((ref) => false); diff --git a/lib/screens/home_page/editor_pane/details_card/code_pane.dart b/lib/screens/common_widgets/code_pane.dart similarity index 79% rename from lib/screens/home_page/editor_pane/details_card/code_pane.dart rename to lib/screens/common_widgets/code_pane.dart index f61c10a6..1da84542 100644 --- a/lib/screens/home_page/editor_pane/details_card/code_pane.dart +++ b/lib/screens/common_widgets/code_pane.dart @@ -9,14 +9,24 @@ import 'package:apidash/consts.dart'; final Codegen codegen = Codegen(); class CodePane extends ConsumerWidget { - const CodePane({super.key}); + const CodePane({ + super.key, + this.isHistoryRequest = false, + }); + + final bool isHistoryRequest; @override Widget build(BuildContext context, WidgetRef ref) { final CodegenLanguage codegenLanguage = ref.watch(codegenLanguageStateProvider); - final selectedRequestModel = ref.watch(selectedRequestModelProvider); + final selectedHistoryRequestModel = + ref.watch(selectedHistoryRequestModelProvider); + + final selectedRequestModel = isHistoryRequest + ? getRequestModelFromHistoryModel(selectedHistoryRequestModel!) + : ref.watch(selectedRequestModelProvider); final defaultUriScheme = ref.watch(settingsProvider.select((value) => value.defaultUriScheme)); diff --git a/lib/screens/common_widgets/common_widgets.dart b/lib/screens/common_widgets/common_widgets.dart index 6c08887a..02a1f583 100644 --- a/lib/screens/common_widgets/common_widgets.dart +++ b/lib/screens/common_widgets/common_widgets.dart @@ -1,4 +1,5 @@ export 'button_navbar.dart'; +export 'code_pane.dart'; export 'editor_title.dart'; export 'editor_title_actions.dart'; export 'envfield_url.dart'; diff --git a/lib/screens/history/details_pane/his_request_pane.dart b/lib/screens/history/details_pane/his_request_pane.dart new file mode 100644 index 00000000..6378a871 --- /dev/null +++ b/lib/screens/history/details_pane/his_request_pane.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/consts.dart'; + +class HistoryRequestPane extends ConsumerWidget { + const HistoryRequestPane({ + super.key, + this.isCompact = false, + }); + + final bool isCompact; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedHistoryIdStateProvider); + final codePaneVisible = ref.watch(historyCodePaneVisibleStateProvider); + + final headersMap = ref.watch(selectedHistoryRequestModelProvider + .select((value) => value?.httpRequestModel.headersMap)) ?? + {}; + final headerLength = headersMap.length; + + final paramsMap = ref.watch(selectedHistoryRequestModelProvider + .select((value) => value?.httpRequestModel.paramsMap)) ?? + {}; + final paramLength = paramsMap.length; + + final hasBody = ref.watch(selectedHistoryRequestModelProvider + .select((value) => value?.httpRequestModel.hasBody)) ?? + false; + + return RequestPane( + selectedId: selectedId, + codePaneVisible: codePaneVisible, + onPressedCodeButton: () { + ref.read(historyCodePaneVisibleStateProvider.notifier).state = + !codePaneVisible; + }, + showViewCodeButton: !isCompact, + showIndicators: [ + paramLength > 0, + headerLength > 0, + hasBody, + ], + children: [ + RequestDataTable( + rows: paramsMap, + keyName: kNameURLParam, + ), + RequestDataTable( + rows: headersMap, + keyName: kNameHeader, + ), + const SizedBox(), + ], + ); + } +} diff --git a/lib/screens/history/details_pane/his_response_pane.dart b/lib/screens/history/details_pane/his_response_pane.dart new file mode 100644 index 00000000..f90390cb --- /dev/null +++ b/lib/screens/history/details_pane/his_response_pane.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; + +class HistoryResponsePane extends ConsumerWidget { + const HistoryResponsePane({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedHistoryIdStateProvider); + final selectedHistoryRequest = + ref.watch(selectedHistoryRequestModelProvider); + final historyHttpResponseModel = selectedHistoryRequest?.httpResponseModel; + + if (selectedId != null) { + final requestModel = + getRequestModelFromHistoryModel(selectedHistoryRequest!); + return Column( + children: [ + ResponsePaneHeader( + responseStatus: historyHttpResponseModel?.statusCode, + message: kResponseCodeReasons[historyHttpResponseModel?.statusCode], + time: historyHttpResponseModel?.time, + ), + Expanded( + child: ResponseTabView( + selectedId: selectedId, + children: [ + ResponseBody( + selectedRequestModel: requestModel, + ), + ResponseHeaders( + responseHeaders: historyHttpResponseModel?.headers ?? {}, + requestHeaders: + historyHttpResponseModel?.requestHeaders ?? {}, + ), + ], + ), + ), + ], + ); + } + return const Text("No Request Selected"); + } +} diff --git a/lib/screens/history/details_pane/url_card.dart b/lib/screens/history/details_pane/url_card.dart index af5d4a41..cb8ec0da 100644 --- a/lib/screens/history/details_pane/url_card.dart +++ b/lib/screens/history/details_pane/url_card.dart @@ -49,9 +49,8 @@ class HistoryURLCard extends StatelessWidget { ), isCompact ? kHSpacer10 : kHSpacer20, Expanded( - child: RawTextField( - readOnly: true, - controller: TextEditingController(text: url), + child: ReadOnlyTextField( + initialValue: url, style: kCodeStyle.copyWith( fontSize: fontSize, ), diff --git a/lib/screens/history/history_details.dart b/lib/screens/history/history_details.dart index cee748ba..7498fa5f 100644 --- a/lib/screens/history/history_details.dart +++ b/lib/screens/history/history_details.dart @@ -4,7 +4,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; -import './details_pane/url_card.dart'; +import 'package:apidash/screens/common_widgets/common_widgets.dart'; +import 'details_pane/url_card.dart'; +import 'details_pane/his_request_pane.dart'; +import 'details_pane/his_response_pane.dart'; class HistoryDetails extends StatefulHookConsumerWidget { const HistoryDetails({super.key}); @@ -21,22 +24,62 @@ class _HistoryDetailsState extends ConsumerState ref.watch(selectedHistoryRequestModelProvider); final metaData = selectedHistoryRequest?.metaData; + final codePaneVisible = ref.watch(historyCodePaneVisibleStateProvider); + final TabController controller = useTabController(initialLength: 2, vsync: this); return selectedHistoryRequest != null - ? Column( - children: [ - Padding( - padding: kP4, - child: HistoryURLCard( - method: metaData!.method, url: metaData.url)), - kVSpacer10, - RequestResponseTabbar( - controller: controller, - ), - ], + ? LayoutBuilder( + builder: (context, constraints) { + final isCompact = constraints.maxWidth < kMediumWindowWidth; + return Column( + children: [ + kVSpacer5, + Padding( + padding: kPh4, + child: HistoryURLCard( + method: metaData!.method, + url: metaData.url, + )), + kVSpacer10, + if (isCompact) ...[ + RequestResponseTabbar( + controller: controller, + ), + kVSpacer10, + Expanded( + child: TabBarView( + controller: controller, + children: [ + HistoryRequestPane( + isCompact: isCompact, + ), + const HistoryResponsePane(), + ], + )) + ] else ...[ + Expanded( + child: Padding( + padding: kPh4, + child: RequestDetailsCard( + child: EqualSplitView( + leftWidget: HistoryRequestPane( + isCompact: isCompact, + ), + rightWidget: codePaneVisible + ? const CodePane(isHistoryRequest: true) + : const HistoryResponsePane(), + ), + ), + ), + ), + kVSpacer8, + ] + ], + ); + }, ) - : const Text("No Request Selected"); + : const SizedBox.shrink(); } } diff --git a/lib/screens/history/history_page.dart b/lib/screens/history/history_page.dart index 69f105ab..0f288fb7 100644 --- a/lib/screens/history/history_page.dart +++ b/lib/screens/history/history_page.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/providers/providers.dart'; +import 'package:apidash/utils/utils.dart'; import 'history_pane.dart'; import 'history_viewer.dart'; @@ -16,11 +17,14 @@ class HistoryPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final historyModel = ref.watch(selectedHistoryRequestModelProvider); + final title = historyModel != null + ? getHistoryRequestName(historyModel.metaData) + : 'History'; if (context.isMediumWindow) { return DrawerSplitView( scaffoldKey: scaffoldKey, mainContent: const HistoryViewer(), - title: Text(historyModel?.historyId ?? 'History'), + title: Text(title), leftDrawerContent: const HistoryPane(), actions: const [SizedBox(width: 16)], onDrawerChanged: (value) => diff --git a/lib/screens/history/history_pane.dart b/lib/screens/history/history_pane.dart index 00620f92..be7e36d7 100644 --- a/lib/screens/history/history_pane.dart +++ b/lib/screens/history/history_pane.dart @@ -56,6 +56,7 @@ class HistoryList extends HookConsumerWidget { title: Text( humanizeDate(date), ), + initiallyExpanded: true, children: requestGroups.values.map((item) { return Padding( padding: kPv2 + kPh4, @@ -79,9 +80,11 @@ class HistoryList extends HookConsumerWidget { ); }).toList() : [ - const Text( - 'No history', - style: TextStyle(color: Colors.grey), + const Center( + child: Text( + 'No history', + style: TextStyle(color: Colors.grey), + ), ) ], ), diff --git a/lib/screens/history/history_requests.dart b/lib/screens/history/history_requests.dart index 1a80c2be..f0bc10cf 100644 --- a/lib/screens/history/history_requests.dart +++ b/lib/screens/history/history_requests.dart @@ -1,3 +1,4 @@ +import 'package:apidash/consts.dart'; import 'package:flutter/material.dart'; import 'package:apidash/providers/history_providers.dart'; import 'package:apidash/utils/history_utils.dart'; @@ -10,23 +11,29 @@ class HistoryRequests extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final selectedRequestId = ref.watch(selectedHistoryIdStateProvider); - final selectedRequest = ref.read(selectedHistoryRequestModelProvider); - final historyMetas = ref.read(historyMetaStateNotifier); + final selectedRequest = ref.watch(selectedHistoryRequestModelProvider); + final historyMetas = ref.watch(historyMetaStateNotifier); final requestGroup = getRequestGroup( historyMetas?.values.toList(), selectedRequest?.metaData); return Column( - children: requestGroup - .map((request) => SidebarHistoryCard( + children: [ + kVSpacer20, + ...requestGroup.map((request) => Padding( + padding: kPv2 + kPh4, + child: HistoryRequestCard( id: request.historyId, - method: request.method, + model: request, isSelected: selectedRequestId == request.historyId, onTap: () { ref.read(selectedHistoryIdStateProvider.notifier).state = request.historyId; + ref + .read(historyMetaStateNotifier.notifier) + .loadHistoryRequest(request.historyId); }, - models: [request], - )) - .toList(), + ), + )) + ], ); } } diff --git a/lib/screens/home_page/editor_pane/details_card/details_card.dart b/lib/screens/home_page/editor_pane/details_card/details_card.dart index e14c139c..739f7475 100644 --- a/lib/screens/home_page/editor_pane/details_card/details_card.dart +++ b/lib/screens/home_page/editor_pane/details_card/details_card.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/screens/common_widgets/common_widgets.dart'; import 'request_pane/request_pane.dart'; import 'response_pane.dart'; -import 'code_pane.dart'; class EditorPaneRequestDetailsCard extends ConsumerWidget { const EditorPaneRequestDetailsCard({super.key}); diff --git a/lib/screens/mobile/requests_page/requests_page.dart b/lib/screens/mobile/requests_page/requests_page.dart index 86d4d70d..4164356c 100644 --- a/lib/screens/mobile/requests_page/requests_page.dart +++ b/lib/screens/mobile/requests_page/requests_page.dart @@ -7,7 +7,6 @@ import 'package:apidash/widgets/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../home_page/collection_pane.dart'; import '../../home_page/editor_pane/url_card.dart'; -import '../../home_page/editor_pane/details_card/code_pane.dart'; import '../../home_page/editor_pane/editor_default.dart'; import '../../common_widgets/common_widgets.dart'; import '../widgets/page_base.dart'; diff --git a/lib/utils/convert_utils.dart b/lib/utils/convert_utils.dart index 735ace25..c772134e 100644 --- a/lib/utils/convert_utils.dart +++ b/lib/utils/convert_utils.dart @@ -13,6 +13,13 @@ String humanizeDate(DateTime? date) { return DateFormat('MMMM d, yyyy').format(date); } +String humanizeTime(DateTime? time) { + if (time == null) { + return ""; + } + return DateFormat('hh:mm:ss a').format(time); +} + String humanizeDuration(Duration? duration) { if (duration == null) { return ""; diff --git a/lib/utils/history_utils.dart b/lib/utils/history_utils.dart index de04cd24..acbea853 100644 --- a/lib/utils/history_utils.dart +++ b/lib/utils/history_utils.dart @@ -1,10 +1,22 @@ -import 'package:apidash/models/models.dart'; import 'package:apidash/utils/convert_utils.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/consts.dart'; DateTime stripTime(DateTime dateTime) { return DateTime(dateTime.year, dateTime.month, dateTime.day); } +RequestModel getRequestModelFromHistoryModel(HistoryRequestModel model) { + return RequestModel( + id: model.historyId, + name: model.metaData.name, + responseStatus: model.httpResponseModel.statusCode, + message: kResponseCodeReasons[model.httpResponseModel.statusCode], + httpRequestModel: model.httpRequestModel, + httpResponseModel: model.httpResponseModel, + ); +} + String getHistoryRequestName(HistoryMetaModel model) { if (model.name.isNotEmpty) { return model.name; diff --git a/lib/widgets/card_history_request.dart b/lib/widgets/card_history_request.dart new file mode 100644 index 00000000..a76e477c --- /dev/null +++ b/lib/widgets/card_history_request.dart @@ -0,0 +1,65 @@ +import 'package:apidash/models/history_meta_model.dart'; +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/utils/utils.dart'; +import 'texts.dart'; + +class HistoryRequestCard extends StatelessWidget { + const HistoryRequestCard({ + super.key, + required this.id, + required this.model, + this.isSelected = false, + this.onTap, + }); + + final String id; + final HistoryMetaModel model; + final bool isSelected; + final Function()? onTap; + + @override + Widget build(BuildContext context) { + final Color color = Theme.of(context).colorScheme.surface; + final Color colorVariant = + Theme.of(context).colorScheme.surfaceContainerHighest.withOpacity(0.5); + final Color surfaceTint = Theme.of(context).colorScheme.primary; + return Card( + shape: const ContinuousRectangleBorder(borderRadius: kBorderRadius12), + elevation: isSelected ? 1 : 0, + surfaceTintColor: isSelected ? surfaceTint : null, + color: isSelected + ? Theme.of(context).colorScheme.brightness == Brightness.dark + ? colorVariant + : color + : color, + margin: EdgeInsets.zero, + child: InkWell( + onTap: onTap, + borderRadius: kBorderRadius6, + hoverColor: colorVariant, + focusColor: colorVariant.withOpacity(0.5), + child: Padding( + padding: kPv6 + kPh8, + child: SizedBox( + height: 20, + child: Row( + children: [ + Expanded( + child: Text( + humanizeTime(model.timeStamp), + softWrap: false, + overflow: TextOverflow.fade, + style: kCodeStyle, + ), + ), + kHSpacer4, + StatusCode(statusCode: model.responseStatus), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/field_read_only.dart b/lib/widgets/field_read_only.dart new file mode 100644 index 00000000..abdb0b4e --- /dev/null +++ b/lib/widgets/field_read_only.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +class ReadOnlyTextField extends StatelessWidget { + const ReadOnlyTextField({ + super.key, + this.initialValue, + this.style, + this.decoration, + }); + + final String? initialValue; + final TextStyle? style; + final InputDecoration? decoration; + + @override + Widget build(BuildContext context) { + return TextField( + readOnly: true, + controller: TextEditingController(text: initialValue), + style: style, + decoration: decoration ?? + const InputDecoration( + isDense: true, + border: InputBorder.none, + contentPadding: kPv8, + ), + ); + } +} diff --git a/lib/widgets/request_widgets.dart b/lib/widgets/request_widgets.dart index 368bba5f..1e7b7ccf 100644 --- a/lib/widgets/request_widgets.dart +++ b/lib/widgets/request_widgets.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/consts.dart'; import 'tabs.dart'; -import 'package:apidash/extensions/extensions.dart'; -class RequestPane extends StatefulWidget { +class RequestPane extends StatefulHookWidget { const RequestPane({ super.key, required this.selectedId, @@ -13,6 +14,7 @@ class RequestPane extends StatefulWidget { this.onTapTabBar, required this.children, this.showIndicators = const [false, false, false], + this.showViewCodeButton, }); final String? selectedId; @@ -22,6 +24,7 @@ class RequestPane extends StatefulWidget { final void Function(int)? onTapTabBar; final List children; final List showIndicators; + final bool? showViewCodeButton; @override State createState() => _RequestPaneState(); @@ -29,28 +32,19 @@ class RequestPane extends StatefulWidget { class _RequestPaneState extends State with TickerProviderStateMixin { - late final TabController _controller; - @override - void initState() { - super.initState(); - _controller = TabController( - length: 3, - animationDuration: kTabAnimationDuration, + Widget build(BuildContext context) { + final TabController controller = useTabController( + initialLength: 3, vsync: this, ); - } - - @override - Widget build(BuildContext context) { if (widget.tabIndex != null) { - _controller.index = widget.tabIndex!; + controller.index = widget.tabIndex!; } return Column( children: [ - context.isMediumWindow - ? const SizedBox.shrink() - : Padding( + (widget.showViewCodeButton ?? !context.isMediumWindow) + ? Padding( padding: kP8, child: SizedBox( height: kHeaderHeight, @@ -76,10 +70,11 @@ class _RequestPaneState extends State ], ), ), - ), + ) + : const SizedBox.shrink(), TabBar( key: Key(widget.selectedId!), - controller: _controller, + controller: controller, overlayColor: kColorTransparentState, labelPadding: kPh2, onTap: widget.onTapTabBar, @@ -101,7 +96,7 @@ class _RequestPaneState extends State kVSpacer5, Expanded( child: TabBarView( - controller: _controller, + controller: controller, physics: const NeverScrollableScrollPhysics(), children: widget.children, ), @@ -109,10 +104,4 @@ class _RequestPaneState extends State ], ); } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } } diff --git a/lib/widgets/response_widgets.dart b/lib/widgets/response_widgets.dart index ce07c100..0df9343b 100644 --- a/lib/widgets/response_widgets.dart +++ b/lib/widgets/response_widgets.dart @@ -128,6 +128,7 @@ class ResponsePaneHeader extends StatelessWidget { @override Widget build(BuildContext context) { + final bool showClearButton = onClearResponse != null; return Padding( padding: kPv8, child: SizedBox( @@ -159,9 +160,11 @@ class ResponsePaneHeader extends StatelessWidget { ), ), kHSpacer10, - ClearResponseButton( - onPressed: onClearResponse, - ) + showClearButton + ? ClearResponseButton( + onPressed: onClearResponse, + ) + : const SizedBox.shrink(), ], ), ), diff --git a/lib/widgets/tables.dart b/lib/widgets/table_map.dart similarity index 100% rename from lib/widgets/tables.dart rename to lib/widgets/table_map.dart diff --git a/lib/widgets/table_request.dart b/lib/widgets/table_request.dart new file mode 100644 index 00000000..bdde7c47 --- /dev/null +++ b/lib/widgets/table_request.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:data_table_2/data_table_2.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/consts.dart'; + +class RequestDataTable extends StatelessWidget { + const RequestDataTable({ + super.key, + required this.rows, + this.keyName, + this.valueName, + }); + + final Map rows; + final String? keyName; + final String? valueName; + + @override + Widget build(BuildContext context) { + final clrScheme = Theme.of(context).colorScheme; + + final List columns = [ + DataColumn2( + label: Text(keyName ?? kNameField), + ), + const DataColumn2( + label: Text('='), + fixedWidth: 30, + ), + DataColumn2( + label: Text(valueName ?? kNameValue), + ), + ]; + + final fieldDecoration = InputDecoration( + contentPadding: const EdgeInsets.only(bottom: 12), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: clrScheme.primary.withOpacity( + kHintOpacity, + ), + ), + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: clrScheme.surfaceContainerHighest, + ), + ), + ); + + final List dataRows = rows.entries + .map( + (MapEntry entry) => DataRow( + cells: [ + DataCell( + ReadOnlyTextField( + initialValue: entry.key, + decoration: fieldDecoration, + ), + ), + const DataCell( + Text('='), + ), + DataCell( + ReadOnlyTextField( + initialValue: entry.value, + decoration: fieldDecoration, + ), + ), + ], + ), + ) + .toList(); + + return Container( + margin: kP10, + child: Column( + children: [ + Expanded( + child: Theme( + data: Theme.of(context) + .copyWith(scrollbarTheme: kDataTableScrollbarTheme), + child: DataTable2( + columnSpacing: 12, + dividerThickness: 0, + horizontalMargin: 0, + headingRowHeight: 0, + dataRowHeight: kDataTableRowHeight, + bottomMargin: kDataTableBottomPadding, + isVerticalScrollBarVisible: true, + columns: columns, + rows: dataRows, + ), + ), + ), + kVSpacer40, + ], + ), + ); + } +} diff --git a/lib/widgets/texts.dart b/lib/widgets/texts.dart index f2cf5c92..71753bfc 100644 --- a/lib/widgets/texts.dart +++ b/lib/widgets/texts.dart @@ -32,3 +32,24 @@ class MethodBox extends StatelessWidget { ); } } + +class StatusCode extends StatelessWidget { + const StatusCode({super.key, required this.statusCode, this.style}); + final int statusCode; + final TextStyle? style; + + @override + Widget build(BuildContext context) { + final brightness = Theme.of(context).brightness; + final Color color = + getResponseStatusCodeColor(statusCode, brightness: brightness); + return Text( + statusCode.toString(), + style: style?.copyWith(color: color) ?? + Theme.of(context).textTheme.bodyMedium?.copyWith( + fontFamily: kCodeStyle.fontFamily, + color: color, + ), + ); + } +} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index b80b6cf9..9f0323aa 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -4,6 +4,7 @@ export 'button_discord.dart'; export 'button_repo.dart'; export 'button_save_download.dart'; export 'button_send.dart'; +export 'card_history_request.dart'; export 'card_request_details.dart'; export 'card_sidebar_environment.dart'; export 'card_sidebar_history.dart'; @@ -28,6 +29,7 @@ export 'field_cell.dart'; export 'field_header.dart'; export 'field_json_search.dart'; export 'field_raw.dart'; +export 'field_read_only.dart'; export 'field_url.dart'; export 'intro_message.dart'; export 'json_previewer.dart'; @@ -46,9 +48,9 @@ export 'splitview_drawer.dart'; export 'splitview_dashboard.dart'; export 'splitview_equal.dart'; export 'splitview_history.dart'; -export 'suggestions_menu.dart'; export 'tabbar_request_response.dart'; -export 'tables.dart'; +export 'table_map.dart'; +export 'table_request.dart'; export 'tabs.dart'; export 'texts.dart'; export 'uint8_audio_player.dart'; diff --git a/test/codegen/csharp_rest_sharp_codgen_test.dart b/test/codegen/csharp_rest_sharp_codgen_test.dart index 6522cbee..df16f928 100644 --- a/test/codegen/csharp_rest_sharp_codgen_test.dart +++ b/test/codegen/csharp_rest_sharp_codgen_test.dart @@ -1,5 +1,5 @@ import 'package:apidash/consts.dart'; -import 'package:apidash/screens/home_page/editor_pane/details_card/code_pane.dart'; +import 'package:apidash/screens/common_widgets/common_widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import '../models/request_models.dart'; diff --git a/test/codegen/go_http_codegen_test.dart b/test/codegen/go_http_codegen_test.dart index 08492cf1..89e4c010 100644 --- a/test/codegen/go_http_codegen_test.dart +++ b/test/codegen/go_http_codegen_test.dart @@ -1,6 +1,6 @@ import 'package:apidash/codegen/codegen.dart'; import 'package:apidash/consts.dart'; -import 'package:apidash/screens/home_page/editor_pane/details_card/code_pane.dart'; +import 'package:apidash/screens/common_widgets/common_widgets.dart'; import 'package:test/test.dart'; import '../models/request_models.dart'; diff --git a/test/providers/ui_providers_test.dart b/test/providers/ui_providers_test.dart index 6ebf7745..5854f768 100644 --- a/test/providers/ui_providers_test.dart +++ b/test/providers/ui_providers_test.dart @@ -1,12 +1,9 @@ import 'dart:io'; -import 'package:spot/spot.dart'; -import 'package:apidash/consts.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/screens/common_widgets/common_widgets.dart'; import 'package:apidash/screens/dashboard.dart'; import 'package:apidash/screens/envvar/environment_page.dart'; import 'package:apidash/screens/home_page/collection_pane.dart'; -import 'package:apidash/screens/home_page/editor_pane/details_card/code_pane.dart'; import 'package:apidash/screens/home_page/editor_pane/details_card/response_pane.dart'; import 'package:apidash/screens/home_page/editor_pane/editor_default.dart'; import 'package:apidash/screens/home_page/editor_pane/editor_pane.dart'; diff --git a/test/widgets/tables_test.dart b/test/widgets/tables_test.dart index 70102cfe..46b4bc1a 100644 --- a/test/widgets/tables_test.dart +++ b/test/widgets/tables_test.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:apidash/widgets/tables.dart'; +import 'package:apidash/widgets/table_map.dart'; void main() { Map mapInput = { From d9d60961f76d8272626a291852a90bedf930264c Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sun, 21 Jul 2024 04:34:12 +0530 Subject: [PATCH 04/71] wip: history requests sheet --- lib/consts.dart | 6 +- .../common_widgets/sidebar_header.dart | 2 +- lib/screens/history/history_details.dart | 4 +- lib/screens/history/history_page.dart | 17 +- lib/screens/history/history_pane.dart | 150 +++++++++++++----- lib/screens/history/history_requests.dart | 136 +++++++++++++++- .../history_widgets/his_bottombar.dart | 75 +++++++++ .../his_request_pane.dart | 0 .../his_response_pane.dart | 0 .../history_widgets/his_sidebar_header.dart | 50 ++++++ .../his_url_card.dart} | 0 .../history_widgets/history_widgets.dart | 5 + 12 files changed, 380 insertions(+), 65 deletions(-) create mode 100644 lib/screens/history/history_widgets/his_bottombar.dart rename lib/screens/history/{details_pane => history_widgets}/his_request_pane.dart (100%) rename lib/screens/history/{details_pane => history_widgets}/his_response_pane.dart (100%) create mode 100644 lib/screens/history/history_widgets/his_sidebar_header.dart rename lib/screens/history/{details_pane/url_card.dart => history_widgets/his_url_card.dart} (100%) create mode 100644 lib/screens/history/history_widgets/history_widgets.dart diff --git a/lib/consts.dart b/lib/consts.dart index 03172793..63be0705 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -100,13 +100,15 @@ const kP6 = EdgeInsets.all(6); const kP8 = EdgeInsets.all(8); const kPs8 = EdgeInsets.only(left: 8); const kPs2 = EdgeInsets.only(left: 2); +const kPe4 = EdgeInsets.only(right: 4); const kPe8 = EdgeInsets.only(right: 8.0); const kPh20v5 = EdgeInsets.symmetric(horizontal: 20, vertical: 5); const kPh20v10 = EdgeInsets.symmetric(horizontal: 20, vertical: 10); const kP10 = EdgeInsets.all(10); -const kPv8 = EdgeInsets.symmetric(vertical: 8); -const kPv6 = EdgeInsets.symmetric(vertical: 6); const kPv2 = EdgeInsets.symmetric(vertical: 2); +const kPv6 = EdgeInsets.symmetric(vertical: 6); +const kPv8 = EdgeInsets.symmetric(vertical: 8); +const kPv10 = EdgeInsets.symmetric(vertical: 10); const kPh2 = EdgeInsets.symmetric(horizontal: 2); const kPt28o8 = EdgeInsets.only(top: 28, left: 8.0, right: 8.0, bottom: 8.0); const kPt5o10 = diff --git a/lib/screens/common_widgets/sidebar_header.dart b/lib/screens/common_widgets/sidebar_header.dart index 4e6b16f7..1a26f0ea 100644 --- a/lib/screens/common_widgets/sidebar_header.dart +++ b/lib/screens/common_widgets/sidebar_header.dart @@ -47,7 +47,7 @@ class SidebarHeader extends ConsumerWidget { ? IconButton( style: IconButton.styleFrom( padding: const EdgeInsets.all(4), - minimumSize: const Size(30, 30), + minimumSize: const Size(36, 36), ), onPressed: () { mobileScaffoldKey.currentState?.closeDrawer(); diff --git a/lib/screens/history/history_details.dart b/lib/screens/history/history_details.dart index 7498fa5f..10f02251 100644 --- a/lib/screens/history/history_details.dart +++ b/lib/screens/history/history_details.dart @@ -5,9 +5,7 @@ import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; import 'package:apidash/screens/common_widgets/common_widgets.dart'; -import 'details_pane/url_card.dart'; -import 'details_pane/his_request_pane.dart'; -import 'details_pane/his_response_pane.dart'; +import 'history_widgets/history_widgets.dart'; class HistoryDetails extends StatefulHookConsumerWidget { const HistoryDetails({super.key}); diff --git a/lib/screens/history/history_page.dart b/lib/screens/history/history_page.dart index 0f288fb7..3787ce4f 100644 --- a/lib/screens/history/history_page.dart +++ b/lib/screens/history/history_page.dart @@ -6,6 +6,7 @@ import 'package:apidash/providers/providers.dart'; import 'package:apidash/utils/utils.dart'; import 'history_pane.dart'; import 'history_viewer.dart'; +import 'history_widgets/history_widgets.dart'; class HistoryPage extends ConsumerWidget { const HistoryPage({ @@ -22,14 +23,14 @@ class HistoryPage extends ConsumerWidget { : 'History'; if (context.isMediumWindow) { return DrawerSplitView( - scaffoldKey: scaffoldKey, - mainContent: const HistoryViewer(), - title: Text(title), - leftDrawerContent: const HistoryPane(), - actions: const [SizedBox(width: 16)], - onDrawerChanged: (value) => - ref.read(leftDrawerStateProvider.notifier).state = value, - ); + scaffoldKey: scaffoldKey, + mainContent: const HistoryViewer(), + title: Text(title), + leftDrawerContent: const HistoryPane(), + actions: const [SizedBox(width: 16)], + onDrawerChanged: (value) => + ref.read(leftDrawerStateProvider.notifier).state = value, + bottomNavigationBar: const HistoryPageBottombar()); } return const Column( children: [ diff --git a/lib/screens/history/history_pane.dart b/lib/screens/history/history_pane.dart index be7e36d7..d206dabb 100644 --- a/lib/screens/history/history_pane.dart +++ b/lib/screens/history/history_pane.dart @@ -4,8 +4,10 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/models/models.dart'; import 'package:apidash/utils/utils.dart'; import 'package:apidash/consts.dart'; +import 'history_widgets/history_widgets.dart'; class HistoryPane extends ConsumerWidget { const HistoryPane({ @@ -19,7 +21,11 @@ class HistoryPane extends ConsumerWidget { (context.isMediumWindow ? kPb70 : EdgeInsets.zero), child: const Column( crossAxisAlignment: CrossAxisAlignment.stretch, - children: [kVSpacer10, Expanded(child: HistoryList()), kVSpacer5], + children: [ + HistorySidebarHeader(), + Expanded(child: HistoryList()), + kVSpacer5, + ], ), ); } @@ -32,7 +38,6 @@ class HistoryList extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final selectedGroupId = ref.watch(selectedRequestGroupIdStateProvider); final historySequence = ref.watch(historySequenceProvider); final alwaysShowHistoryPaneScrollbar = ref.watch(settingsProvider .select((value) => value.alwaysShowCollectionPaneScrollbar)); @@ -43,51 +48,108 @@ class HistoryList extends HookConsumerWidget { controller: scrollController, thumbVisibility: alwaysShowHistoryPaneScrollbar, radius: const Radius.circular(12), - child: ListView( + child: ListView.separated( padding: EdgeInsets.only(bottom: MediaQuery.paddingOf(context).bottom), controller: scrollController, - children: sortedHistoryKeys != null - ? sortedHistoryKeys.map((date) { - var items = historySequence![date]!; - final requestGroups = getRequestGroups(items); - return Column( - children: [ - ExpansionTile( - title: Text( - humanizeDate(date), - ), - initiallyExpanded: true, - children: requestGroups.values.map((item) { - return Padding( - padding: kPv2 + kPh4, - child: SidebarHistoryCard( - id: item.first.historyId, - models: item, - method: item.first.method, - isSelected: selectedGroupId == - getHistoryRequestKey(item.first), - requestGroupSize: item.length, - onTap: () { - ref - .read(historyMetaStateNotifier.notifier) - .loadHistoryRequest(item.first.historyId); - }, - ), - ); - }).toList(), - ), - ], - ); - }).toList() - : [ - const Center( - child: Text( - 'No history', - style: TextStyle(color: Colors.grey), - ), - ) - ], + itemCount: sortedHistoryKeys?.length ?? 0, + separatorBuilder: (context, index) => Divider( + height: 0, + thickness: 2, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + itemBuilder: (context, index) { + var items = historySequence![sortedHistoryKeys![index]]!; + final requestGroups = getRequestGroups(items); + return Padding( + padding: kPv2, + child: HistoryExpansionTile( + date: sortedHistoryKeys[index], + requestGroups: requestGroups, + ), + ); + }, + ), + ); + } +} + +class HistoryExpansionTile extends StatefulHookConsumerWidget { + const HistoryExpansionTile({ + super.key, + required this.requestGroups, + required this.date, + }); + + final Map> requestGroups; + final DateTime date; + + @override + ConsumerState createState() => + _HistoryExpansionTileState(); +} + +class _HistoryExpansionTileState extends ConsumerState + with SingleTickerProviderStateMixin { + @override + Widget build(BuildContext context) { + final animationController = useAnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + final animation = Tween(begin: 0.25, end: 0.0).animate(animationController); + final colorScheme = Theme.of(context).colorScheme; + final selectedGroupId = ref.watch(selectedRequestGroupIdStateProvider); + return ExpansionTile( + dense: true, + title: Row( + children: [ + RotationTransition( + turns: animation, + child: Icon( + Icons.chevron_right_rounded, + size: 20, + color: colorScheme.onSurface.withOpacity(0.6), + )), + kHSpacer5, + Text( + humanizeDate(widget.date), + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], ), + onExpansionChanged: (value) { + if (value) { + animationController.reverse(); + } else { + animationController.forward(); + } + }, + trailing: const SizedBox.shrink(), + tilePadding: kPh8, + shape: const RoundedRectangleBorder(), + collapsedBackgroundColor: colorScheme.surfaceContainerLow, + initiallyExpanded: true, + childrenPadding: kPv8 + kPe4, + children: widget.requestGroups.values.map((item) { + return Padding( + padding: kPv2 + kPh4, + child: SidebarHistoryCard( + id: item.first.historyId, + models: item, + method: item.first.method, + isSelected: selectedGroupId == getHistoryRequestKey(item.first), + requestGroupSize: item.length, + onTap: () { + ref + .read(historyMetaStateNotifier.notifier) + .loadHistoryRequest(item.first.historyId); + }, + ), + ); + }).toList(), ); } } diff --git a/lib/screens/history/history_requests.dart b/lib/screens/history/history_requests.dart index f0bc10cf..7927cd3c 100644 --- a/lib/screens/history/history_requests.dart +++ b/lib/screens/history/history_requests.dart @@ -1,12 +1,20 @@ -import 'package:apidash/consts.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:apidash/providers/history_providers.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; import 'package:apidash/utils/history_utils.dart'; import 'package:apidash/widgets/widgets.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/consts.dart'; class HistoryRequests extends ConsumerWidget { - const HistoryRequests({super.key}); + const HistoryRequests({ + super.key, + this.scrollController, + this.onSelect, + }); + + final ScrollController? scrollController; + final Function()? onSelect; @override Widget build(BuildContext context, WidgetRef ref) { @@ -15,9 +23,12 @@ class HistoryRequests extends ConsumerWidget { final historyMetas = ref.watch(historyMetaStateNotifier); final requestGroup = getRequestGroup( historyMetas?.values.toList(), selectedRequest?.metaData); - return Column( + return ListView( + shrinkWrap: true, + controller: scrollController, + padding: kPh4, children: [ - kVSpacer20, + kVSpacer10, ...requestGroup.map((request) => Padding( padding: kPv2 + kPh4, child: HistoryRequestCard( @@ -30,10 +41,121 @@ class HistoryRequests extends ConsumerWidget { ref .read(historyMetaStateNotifier.notifier) .loadHistoryRequest(request.historyId); + onSelect?.call(); }, ), - )) + )), + kVSpacer10, ], ); } } + +class HistorRequestsScrollableSheet extends StatefulWidget { + const HistorRequestsScrollableSheet({ + super.key, + }); + + @override + State createState() => + _HistorRequestsScrollableSheetState(); +} + +class _HistorRequestsScrollableSheetState + extends State { + double sheetPosition = 0.5; + final double dragSensitivity = 600; + @override + Widget build(BuildContext context) { + return DraggableScrollableSheet( + initialChildSize: sheetPosition, + expand: false, + builder: (context, scrollController) { + return Column( + children: [ + Grabber( + onVerticalDragUpdate: (DragUpdateDetails details) { + setState(() { + sheetPosition -= details.delta.dy / dragSensitivity; + if (sheetPosition < 0.25) { + sheetPosition = 0.25; + } + if (sheetPosition > 0.9) { + sheetPosition = 0.9; + } + }); + }, + isOnDesktopAndWeb: isOnDesktopAndWeb, + ), + Expanded( + child: HistoryRequests( + scrollController: scrollController, + onSelect: () { + Navigator.of(context).pop(); + }, + ), + ), + ], + ); + }); + } + + bool get isOnDesktopAndWeb { + if (kIsWeb) { + return true; + } + switch (defaultTargetPlatform) { + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.windows: + return true; + case TargetPlatform.android: + case TargetPlatform.iOS: + case TargetPlatform.fuchsia: + return false; + } + } +} + +class Grabber extends StatelessWidget { + const Grabber({ + super.key, + required this.onVerticalDragUpdate, + required this.isOnDesktopAndWeb, + }); + + final ValueChanged onVerticalDragUpdate; + final bool isOnDesktopAndWeb; + + @override + Widget build(BuildContext context) { + if (!isOnDesktopAndWeb) { + return const SizedBox.shrink(); + } + final ColorScheme colorScheme = Theme.of(context).colorScheme; + + return GestureDetector( + onVerticalDragUpdate: onVerticalDragUpdate, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: colorScheme.surfaceContainerLow, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), topRight: Radius.circular(16)), + ), + child: Align( + alignment: Alignment.topCenter, + child: Container( + margin: kPv10, + width: 80.0, + height: 6.0, + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8.0), + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/history/history_widgets/his_bottombar.dart b/lib/screens/history/history_widgets/his_bottombar.dart new file mode 100644 index 00000000..5f146cc5 --- /dev/null +++ b/lib/screens/history/history_widgets/his_bottombar.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/utils/utils.dart'; +import '../history_requests.dart'; + +class HistoryPageBottombar extends ConsumerWidget { + const HistoryPageBottombar({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedRequestModel = ref.watch(selectedHistoryRequestModelProvider); + final historyMetas = ref.watch(historyMetaStateNotifier); + final requestGroup = getRequestGroup( + historyMetas?.values.toList(), selectedRequestModel?.metaData); + final requestCount = requestGroup.length; + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + height: 60 + MediaQuery.paddingOf(context).bottom, + width: MediaQuery.sizeOf(context).width, + padding: EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom, + left: 16, + right: 16, + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border( + top: BorderSide( + color: Theme.of(context).colorScheme.onInverseSurface, + width: 1, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + requestCount > 1 + ? Badge( + label: Text( + requestCount > 9 ? '9 +' : requestCount.toString(), + style: const TextStyle(fontWeight: FontWeight.bold), + ), + child: IconButton( + style: IconButton.styleFrom( + backgroundColor: + Theme.of(context).colorScheme.secondaryContainer, + ), + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) { + return ConstrainedBox( + constraints: + const BoxConstraints(maxWidth: 500), + child: const HistorRequestsScrollableSheet()); + }, + ); + }, + icon: const Icon( + Icons.keyboard_arrow_up_rounded, + ), + ), + ) + : const SizedBox.shrink(), + ], + ), + ), + ); + } +} diff --git a/lib/screens/history/details_pane/his_request_pane.dart b/lib/screens/history/history_widgets/his_request_pane.dart similarity index 100% rename from lib/screens/history/details_pane/his_request_pane.dart rename to lib/screens/history/history_widgets/his_request_pane.dart diff --git a/lib/screens/history/details_pane/his_response_pane.dart b/lib/screens/history/history_widgets/his_response_pane.dart similarity index 100% rename from lib/screens/history/details_pane/his_response_pane.dart rename to lib/screens/history/history_widgets/his_response_pane.dart diff --git a/lib/screens/history/history_widgets/his_sidebar_header.dart b/lib/screens/history/history_widgets/his_sidebar_header.dart new file mode 100644 index 00000000..e431c4b7 --- /dev/null +++ b/lib/screens/history/history_widgets/his_sidebar_header.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:apidash/extensions/extensions.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/consts.dart'; + +class HistorySidebarHeader extends ConsumerWidget { + const HistorySidebarHeader({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final mobileScaffoldKey = ref.read(mobileScaffoldKeyStateProvider); + return Padding( + padding: kPe4, + child: Row( + children: [ + kHSpacer10, + Text( + "History", + style: Theme.of(context).textTheme.titleMedium, + ), + const Spacer(), + IconButton( + tooltip: "Auto Delete Settings", + style: IconButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.primary, + ), + onPressed: () {}, + icon: const Icon( + Icons.auto_delete_outlined, + size: 20, + ), + ), + context.width <= kMinWindowSize.width + ? IconButton( + style: IconButton.styleFrom( + padding: const EdgeInsets.all(4), + minimumSize: const Size(36, 36), + ), + onPressed: () { + mobileScaffoldKey.currentState?.closeDrawer(); + }, + icon: const Icon(Icons.chevron_left), + ) + : const SizedBox.shrink(), + ], + ), + ); + } +} diff --git a/lib/screens/history/details_pane/url_card.dart b/lib/screens/history/history_widgets/his_url_card.dart similarity index 100% rename from lib/screens/history/details_pane/url_card.dart rename to lib/screens/history/history_widgets/his_url_card.dart diff --git a/lib/screens/history/history_widgets/history_widgets.dart b/lib/screens/history/history_widgets/history_widgets.dart new file mode 100644 index 00000000..dc8712ff --- /dev/null +++ b/lib/screens/history/history_widgets/history_widgets.dart @@ -0,0 +1,5 @@ +export 'his_bottombar.dart'; +export 'his_request_pane.dart'; +export 'his_response_pane.dart'; +export 'his_sidebar_header.dart'; +export 'his_url_card.dart'; From 2fc02eadd150901f886794c1ca3722f7d8d8656c Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sun, 21 Jul 2024 19:55:32 +0530 Subject: [PATCH 05/71] feat: history of requests --- lib/consts.dart | 21 ++- lib/models/history_meta_model.dart | 1 + lib/models/history_meta_model.freezed.dart | 27 +++- lib/models/history_meta_model.g.dart | 2 + lib/providers/collection_providers.dart | 27 ++++ lib/screens/common_widgets/button_navbar.dart | 43 +++--- lib/screens/history/history_details.dart | 43 ++++-- lib/screens/history/history_page.dart | 17 ++- lib/screens/history/history_pane.dart | 12 +- .../history_widgets/his_action_buttons.dart | 47 +++++++ .../history_widgets/his_bottombar.dart | 131 +++++++++++------- .../requests_page/request_response_tabs.dart | 11 +- lib/widgets/button_group_filled.dart | 60 ++++++++ ...st_response.dart => tabbar_segmented.dart} | 24 ++-- lib/widgets/table_request.dart | 10 ++ lib/widgets/widgets.dart | 3 +- 16 files changed, 355 insertions(+), 124 deletions(-) create mode 100644 lib/screens/history/history_widgets/his_action_buttons.dart create mode 100644 lib/widgets/button_group_filled.dart rename lib/widgets/{tabbar_request_response.dart => tabbar_segmented.dart} (77%) diff --git a/lib/consts.dart b/lib/consts.dart index 63be0705..ed2bc4e1 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -168,6 +168,7 @@ const kPb15 = EdgeInsets.only( const kPb70 = EdgeInsets.only( bottom: 70, ); +const kHSpacer2 = SizedBox(width: 2); const kHSpacer4 = SizedBox(width: 4); const kHSpacer5 = SizedBox(width: 5); const kHSpacer10 = SizedBox(width: 10); @@ -191,8 +192,8 @@ const kRandMax = 100000; const kSuggestionsMenuWidth = 300.0; const kSuggestionsMenuMaxHeight = 200.0; -const kReqResTabWidth = 280.0; -const kReqResTabHeight = 32.0; +const kSegmentedTabWidth = 140.0; +const kSegmentedTabHeight = 32.0; const kDataTableScrollbarTheme = ScrollbarThemeData( crossAxisMargin: -4, @@ -316,6 +317,20 @@ final kColorHttpMethodPut = Colors.amber.shade900; final kColorHttpMethodPatch = kColorHttpMethodPut; final kColorHttpMethodDelete = Colors.red.shade800; +class ButtonData { + ButtonData({ + required this.label, + required this.icon, + this.onPressed, + this.tooltip = "", + }); + + final String label; + final IconData icon; + final VoidCallback? onPressed; + final String tooltip; +} + enum ItemMenuOption { edit("Rename"), delete("Delete"), @@ -724,6 +739,8 @@ const kLabelSave = "Save"; const kLabelDownload = "Download"; const kLabelSaving = "Saving"; const kLabelSaved = "Saved"; +const kLabelCode = "Code"; +const kLabelDuplicate = "Duplicate"; // Request Pane const kLabelRequest = "Request"; const kLabelHideCode = "Hide Code"; diff --git a/lib/models/history_meta_model.dart b/lib/models/history_meta_model.dart index 58e68aee..9188b6ad 100644 --- a/lib/models/history_meta_model.dart +++ b/lib/models/history_meta_model.dart @@ -9,6 +9,7 @@ part 'history_meta_model.g.dart'; class HistoryMetaModel with _$HistoryMetaModel { const factory HistoryMetaModel({ required String historyId, + required String requestId, @Default("") String name, required String url, required HTTPVerb method, diff --git a/lib/models/history_meta_model.freezed.dart b/lib/models/history_meta_model.freezed.dart index fb7ba2eb..41901461 100644 --- a/lib/models/history_meta_model.freezed.dart +++ b/lib/models/history_meta_model.freezed.dart @@ -21,6 +21,7 @@ HistoryMetaModel _$HistoryMetaModelFromJson(Map json) { /// @nodoc mixin _$HistoryMetaModel { String get historyId => throw _privateConstructorUsedError; + String get requestId => throw _privateConstructorUsedError; String get name => throw _privateConstructorUsedError; String get url => throw _privateConstructorUsedError; HTTPVerb get method => throw _privateConstructorUsedError; @@ -41,6 +42,7 @@ abstract class $HistoryMetaModelCopyWith<$Res> { @useResult $Res call( {String historyId, + String requestId, String name, String url, HTTPVerb method, @@ -62,6 +64,7 @@ class _$HistoryMetaModelCopyWithImpl<$Res, $Val extends HistoryMetaModel> @override $Res call({ Object? historyId = null, + Object? requestId = null, Object? name = null, Object? url = null, Object? method = null, @@ -73,6 +76,10 @@ class _$HistoryMetaModelCopyWithImpl<$Res, $Val extends HistoryMetaModel> ? _value.historyId : historyId // ignore: cast_nullable_to_non_nullable as String, + requestId: null == requestId + ? _value.requestId + : requestId // ignore: cast_nullable_to_non_nullable + as String, name: null == name ? _value.name : name // ignore: cast_nullable_to_non_nullable @@ -107,6 +114,7 @@ abstract class _$$HistoryMetaModelImplCopyWith<$Res> @useResult $Res call( {String historyId, + String requestId, String name, String url, HTTPVerb method, @@ -126,6 +134,7 @@ class __$$HistoryMetaModelImplCopyWithImpl<$Res> @override $Res call({ Object? historyId = null, + Object? requestId = null, Object? name = null, Object? url = null, Object? method = null, @@ -137,6 +146,10 @@ class __$$HistoryMetaModelImplCopyWithImpl<$Res> ? _value.historyId : historyId // ignore: cast_nullable_to_non_nullable as String, + requestId: null == requestId + ? _value.requestId + : requestId // ignore: cast_nullable_to_non_nullable + as String, name: null == name ? _value.name : name // ignore: cast_nullable_to_non_nullable @@ -166,6 +179,7 @@ class __$$HistoryMetaModelImplCopyWithImpl<$Res> class _$HistoryMetaModelImpl implements _HistoryMetaModel { const _$HistoryMetaModelImpl( {required this.historyId, + required this.requestId, this.name = "", required this.url, required this.method, @@ -178,6 +192,8 @@ class _$HistoryMetaModelImpl implements _HistoryMetaModel { @override final String historyId; @override + final String requestId; + @override @JsonKey() final String name; @override @@ -191,7 +207,7 @@ class _$HistoryMetaModelImpl implements _HistoryMetaModel { @override String toString() { - return 'HistoryMetaModel(historyId: $historyId, name: $name, url: $url, method: $method, responseStatus: $responseStatus, timeStamp: $timeStamp)'; + return 'HistoryMetaModel(historyId: $historyId, requestId: $requestId, name: $name, url: $url, method: $method, responseStatus: $responseStatus, timeStamp: $timeStamp)'; } @override @@ -201,6 +217,8 @@ class _$HistoryMetaModelImpl implements _HistoryMetaModel { other is _$HistoryMetaModelImpl && (identical(other.historyId, historyId) || other.historyId == historyId) && + (identical(other.requestId, requestId) || + other.requestId == requestId) && (identical(other.name, name) || other.name == name) && (identical(other.url, url) || other.url == url) && (identical(other.method, method) || other.method == method) && @@ -212,8 +230,8 @@ class _$HistoryMetaModelImpl implements _HistoryMetaModel { @JsonKey(ignore: true) @override - int get hashCode => Object.hash( - runtimeType, historyId, name, url, method, responseStatus, timeStamp); + int get hashCode => Object.hash(runtimeType, historyId, requestId, name, url, + method, responseStatus, timeStamp); @JsonKey(ignore: true) @override @@ -233,6 +251,7 @@ class _$HistoryMetaModelImpl implements _HistoryMetaModel { abstract class _HistoryMetaModel implements HistoryMetaModel { const factory _HistoryMetaModel( {required final String historyId, + required final String requestId, final String name, required final String url, required final HTTPVerb method, @@ -245,6 +264,8 @@ abstract class _HistoryMetaModel implements HistoryMetaModel { @override String get historyId; @override + String get requestId; + @override String get name; @override String get url; diff --git a/lib/models/history_meta_model.g.dart b/lib/models/history_meta_model.g.dart index 67975edc..3d8af833 100644 --- a/lib/models/history_meta_model.g.dart +++ b/lib/models/history_meta_model.g.dart @@ -10,6 +10,7 @@ _$HistoryMetaModelImpl _$$HistoryMetaModelImplFromJson( Map json) => _$HistoryMetaModelImpl( historyId: json['historyId'] as String, + requestId: json['requestId'] as String, name: json['name'] as String? ?? "", url: json['url'] as String, method: $enumDecode(_$HTTPVerbEnumMap, json['method']), @@ -21,6 +22,7 @@ Map _$$HistoryMetaModelImplToJson( _$HistoryMetaModelImpl instance) => { 'historyId': instance.historyId, + 'requestId': instance.requestId, 'name': instance.name, 'url': instance.url, 'method': _$HTTPVerbEnumMap[instance.method]!, diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 3bb2258d..02eead40 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -159,6 +159,32 @@ class CollectionStateNotifier ref.read(hasUnsavedChangesProvider.notifier).state = true; } + void duplicateFromHistory(HistoryRequestModel historyRequestModel) { + final newId = getNewUuid(); + + var itemIds = ref.read(requestSequenceProvider); + var currentModel = historyRequestModel; + final newModel = RequestModel( + id: newId, + name: "${currentModel.metaData.name} (history)", + httpRequestModel: currentModel.httpRequestModel, + responseStatus: currentModel.metaData.responseStatus, + message: kResponseCodeReasons[currentModel.metaData.responseStatus], + httpResponseModel: currentModel.httpResponseModel, + isWorking: false, + sendingTime: null, + ); + + itemIds.insert(0, newId); + var map = {...state!}; + map[newId] = newModel; + state = map; + + ref.read(requestSequenceProvider.notifier).state = [...itemIds]; + ref.read(selectedIdStateProvider.notifier).state = newId; + ref.read(hasUnsavedChangesProvider.notifier).state = true; + } + void update( String id, { HTTPVerb? method, @@ -261,6 +287,7 @@ class CollectionStateNotifier historyId: newHistoryId, metaData: HistoryMetaModel( historyId: newHistoryId, + requestId: id, name: requestModel.name, url: substitutedHttpRequestModel.url, method: substitutedHttpRequestModel.method, diff --git a/lib/screens/common_widgets/button_navbar.dart b/lib/screens/common_widgets/button_navbar.dart index 2cefc201..f18c695c 100644 --- a/lib/screens/common_widgets/button_navbar.dart +++ b/lib/screens/common_widgets/button_navbar.dart @@ -27,22 +27,27 @@ class NavbarButton extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final bool isSelected = railIdx == buttonIdx; final Size size = isCompact ? const Size(56, 32) : const Size(65, 32); + var onPress = isSelected + ? null + : () { + if (buttonIdx != null) { + ref.read(navRailIndexStateProvider.notifier).state = buttonIdx!; + if ((railIdx > 2 && buttonIdx! <= 2) || + !(ref + .read(mobileScaffoldKeyStateProvider) + .currentState + ?.isDrawerOpen ?? + true)) { + ref.read(leftDrawerStateProvider.notifier).state = false; + } + } + onTap?.call(); + }; return MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( behavior: HitTestBehavior.translucent, - onTap: isSelected - ? null - : () { - if (buttonIdx != null) { - ref.read(navRailIndexStateProvider.notifier).state = - buttonIdx!; - if (railIdx > 2 && buttonIdx! <= 2) { - ref.read(leftDrawerStateProvider.notifier).state = false; - } - } - onTap?.call(); - }, + onTap: onPress, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -56,19 +61,7 @@ class NavbarButton extends ConsumerWidget { : TextButton.styleFrom( fixedSize: size, ), - onPressed: isSelected - ? null - : () { - if (buttonIdx != null) { - ref.read(navRailIndexStateProvider.notifier).state = - buttonIdx!; - if (railIdx > 2 && buttonIdx! <= 2) { - ref.read(leftDrawerStateProvider.notifier).state = - false; - } - } - onTap?.call(); - }, + onPressed: onPress, child: Icon( isSelected ? selectedIcon : icon, color: Theme.of(context).colorScheme.onSurfaceVariant, diff --git a/lib/screens/history/history_details.dart b/lib/screens/history/history_details.dart index 10f02251..ebdf81c1 100644 --- a/lib/screens/history/history_details.dart +++ b/lib/screens/history/history_details.dart @@ -25,7 +25,7 @@ class _HistoryDetailsState extends ConsumerState final codePaneVisible = ref.watch(historyCodePaneVisibleStateProvider); final TabController controller = - useTabController(initialLength: 2, vsync: this); + useTabController(initialLength: 3, vsync: this); return selectedHistoryRequest != null ? LayoutBuilder( @@ -42,28 +42,45 @@ class _HistoryDetailsState extends ConsumerState )), kVSpacer10, if (isCompact) ...[ - RequestResponseTabbar( + SegmentedTabbar( controller: controller, + tabs: const [ + Tab(text: kLabelRequest), + Tab(text: kLabelResponse), + Tab(text: kLabelCode), + ], ), kVSpacer10, Expanded( - child: TabBarView( - controller: controller, - children: [ - HistoryRequestPane( - isCompact: isCompact, - ), - const HistoryResponsePane(), - ], - )) + child: TabBarView( + controller: controller, + children: [ + HistoryRequestPane( + isCompact: isCompact, + ), + const HistoryResponsePane(), + const CodePane( + isHistoryRequest: true, + ), + ], + ), + ), + const HistoryPageBottombar() ] else ...[ Expanded( child: Padding( padding: kPh4, child: RequestDetailsCard( child: EqualSplitView( - leftWidget: HistoryRequestPane( - isCompact: isCompact, + leftWidget: Column( + children: [ + Expanded( + child: HistoryRequestPane( + isCompact: isCompact, + ), + ), + const HistoryPageBottombar(), + ], ), rightWidget: codePaneVisible ? const CodePane(isHistoryRequest: true) diff --git a/lib/screens/history/history_page.dart b/lib/screens/history/history_page.dart index 3787ce4f..0f288fb7 100644 --- a/lib/screens/history/history_page.dart +++ b/lib/screens/history/history_page.dart @@ -6,7 +6,6 @@ import 'package:apidash/providers/providers.dart'; import 'package:apidash/utils/utils.dart'; import 'history_pane.dart'; import 'history_viewer.dart'; -import 'history_widgets/history_widgets.dart'; class HistoryPage extends ConsumerWidget { const HistoryPage({ @@ -23,14 +22,14 @@ class HistoryPage extends ConsumerWidget { : 'History'; if (context.isMediumWindow) { return DrawerSplitView( - scaffoldKey: scaffoldKey, - mainContent: const HistoryViewer(), - title: Text(title), - leftDrawerContent: const HistoryPane(), - actions: const [SizedBox(width: 16)], - onDrawerChanged: (value) => - ref.read(leftDrawerStateProvider.notifier).state = value, - bottomNavigationBar: const HistoryPageBottombar()); + scaffoldKey: scaffoldKey, + mainContent: const HistoryViewer(), + title: Text(title), + leftDrawerContent: const HistoryPane(), + actions: const [SizedBox(width: 16)], + onDrawerChanged: (value) => + ref.read(leftDrawerStateProvider.notifier).state = value, + ); } return const Column( children: [ diff --git a/lib/screens/history/history_pane.dart b/lib/screens/history/history_pane.dart index d206dabb..8b090b95 100644 --- a/lib/screens/history/history_pane.dart +++ b/lib/screens/history/history_pane.dart @@ -65,6 +65,7 @@ class HistoryList extends HookConsumerWidget { child: HistoryExpansionTile( date: sortedHistoryKeys[index], requestGroups: requestGroups, + initiallyExpanded: index == 0, ), ); }, @@ -78,10 +79,12 @@ class HistoryExpansionTile extends StatefulHookConsumerWidget { super.key, required this.requestGroups, required this.date, + this.initiallyExpanded = false, }); final Map> requestGroups; final DateTime date; + final bool initiallyExpanded; @override ConsumerState createState() => @@ -95,8 +98,9 @@ class _HistoryExpansionTileState extends ConsumerState final animationController = useAnimationController( duration: const Duration(milliseconds: 200), vsync: this, + initialValue: widget.initiallyExpanded ? 1.0 : 0.0, ); - final animation = Tween(begin: 0.25, end: 0.0).animate(animationController); + final animation = Tween(begin: 0.0, end: 0.25).animate(animationController); final colorScheme = Theme.of(context).colorScheme; final selectedGroupId = ref.watch(selectedRequestGroupIdStateProvider); return ExpansionTile( @@ -122,16 +126,16 @@ class _HistoryExpansionTileState extends ConsumerState ), onExpansionChanged: (value) { if (value) { - animationController.reverse(); - } else { animationController.forward(); + } else { + animationController.reverse(); } }, trailing: const SizedBox.shrink(), tilePadding: kPh8, shape: const RoundedRectangleBorder(), collapsedBackgroundColor: colorScheme.surfaceContainerLow, - initiallyExpanded: true, + initiallyExpanded: widget.initiallyExpanded, childrenPadding: kPv8 + kPe4, children: widget.requestGroups.values.map((item) { return Padding( diff --git a/lib/screens/history/history_widgets/his_action_buttons.dart b/lib/screens/history/history_widgets/his_action_buttons.dart new file mode 100644 index 00000000..436c9557 --- /dev/null +++ b/lib/screens/history/history_widgets/his_action_buttons.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/consts.dart'; + +class HistoryActionButtons extends ConsumerWidget { + const HistoryActionButtons({super.key, this.historyRequestModel}); + + final HistoryRequestModel? historyRequestModel; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final collectionStateNotifier = ref.watch(collectionStateNotifierProvider); + final isAvailable = collectionStateNotifier?.values.any((element) => + element.id == historyRequestModel?.metaData.requestId) ?? + false; + final requestId = historyRequestModel?.metaData.requestId; + return FilledButtonGroup(buttons: [ + ButtonData( + icon: Icons.copy_rounded, + label: kLabelDuplicate, + onPressed: requestId != null + ? () { + ref + .read(collectionStateNotifierProvider.notifier) + .duplicateFromHistory(historyRequestModel!); + ref.read(navRailIndexStateProvider.notifier).state = 0; + } + : null, + tooltip: "Duplicate Request", + ), + ButtonData( + icon: Icons.north_east_rounded, + label: kLabelRequest, + onPressed: isAvailable && requestId != null + ? () { + ref.read(selectedIdStateProvider.notifier).state = requestId; + ref.read(navRailIndexStateProvider.notifier).state = 0; + } + : null, + tooltip: isAvailable ? "Go to Request" : "Couldn't find Request", + ), + ]); + } +} diff --git a/lib/screens/history/history_widgets/his_bottombar.dart b/lib/screens/history/history_widgets/his_bottombar.dart index 5f146cc5..9945b7bc 100644 --- a/lib/screens/history/history_widgets/his_bottombar.dart +++ b/lib/screens/history/history_widgets/his_bottombar.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; import '../history_requests.dart'; +import 'his_action_buttons.dart'; class HistoryPageBottombar extends ConsumerWidget { const HistoryPageBottombar({ @@ -16,59 +19,87 @@ class HistoryPageBottombar extends ConsumerWidget { final requestGroup = getRequestGroup( historyMetas?.values.toList(), selectedRequestModel?.metaData); final requestCount = requestGroup.length; - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: Container( - height: 60 + MediaQuery.paddingOf(context).bottom, - width: MediaQuery.sizeOf(context).width, - padding: EdgeInsets.only( - bottom: MediaQuery.paddingOf(context).bottom, - left: 16, - right: 16, - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - border: Border( - top: BorderSide( - color: Theme.of(context).colorScheme.onInverseSurface, - width: 1, - ), + + return Container( + height: 60 + MediaQuery.paddingOf(context).bottom, + width: MediaQuery.sizeOf(context).width, + padding: EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom, + left: 16, + right: 16, + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border( + top: BorderSide( + color: Theme.of(context).colorScheme.onInverseSurface, + width: 1, ), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - requestCount > 1 - ? Badge( - label: Text( - requestCount > 9 ? '9 +' : requestCount.toString(), - style: const TextStyle(fontWeight: FontWeight.bold), - ), - child: IconButton( - style: IconButton.styleFrom( - backgroundColor: - Theme.of(context).colorScheme.secondaryContainer, - ), - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (context) { - return ConstrainedBox( - constraints: - const BoxConstraints(maxWidth: 500), - child: const HistorRequestsScrollableSheet()); - }, - ); - }, - icon: const Icon( - Icons.keyboard_arrow_up_rounded, - ), - ), - ) - : const SizedBox.shrink(), - ], + ), + child: context.isMediumWindow + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + HistoryActionButtons(historyRequestModel: selectedRequestModel), + HistorySheetButton(requestCount: requestCount) + ], + ) + : Center( + child: HistoryActionButtons( + historyRequestModel: selectedRequestModel)), + ); + } +} + +class HistorySheetButton extends StatelessWidget { + const HistorySheetButton({ + super.key, + required this.requestCount, + }); + + final int requestCount; + + @override + Widget build(BuildContext context) { + final isCompact = context.isCompactWindow; + const icon = Icon(Icons.keyboard_arrow_up_rounded); + return Badge( + isLabelVisible: requestCount > 1, + label: Text( + requestCount > 9 ? '9+' : requestCount.toString(), + style: const TextStyle(fontWeight: FontWeight.bold), + ), + child: FilledButton.tonal( + style: FilledButton.styleFrom( + minimumSize: const Size(44, 44), + padding: isCompact ? kP4 : const EdgeInsets.fromLTRB(16, 12, 8, 12), ), + onPressed: requestCount > 1 + ? () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500), + child: const HistorRequestsScrollableSheet()); + }, + ); + } + : null, + child: isCompact + ? icon + : const Row( + children: [ + Text( + "Show All", + style: kTextStyleButton, + ), + kHSpacer5, + icon, + ], + ), ), ); } diff --git a/lib/screens/mobile/requests_page/request_response_tabs.dart b/lib/screens/mobile/requests_page/request_response_tabs.dart index 36ad6c11..34a0ed90 100644 --- a/lib/screens/mobile/requests_page/request_response_tabs.dart +++ b/lib/screens/mobile/requests_page/request_response_tabs.dart @@ -19,13 +19,14 @@ class RequestResponseTabs extends StatelessWidget { child: EditorPaneRequestURLCard(), ), kVSpacer10, - RequestResponseTabbar( + SegmentedTabbar( controller: controller, + tabs: const [ + Tab(text: kLabelRequest), + Tab(text: kLabelResponse), + ], ), - Expanded( - child: RequestResponseTabviews( - controller: controller, - )) + Expanded(child: RequestResponseTabviews(controller: controller)) ], ); } diff --git a/lib/widgets/button_group_filled.dart b/lib/widgets/button_group_filled.dart new file mode 100644 index 00000000..24ae622d --- /dev/null +++ b/lib/widgets/button_group_filled.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +class FilledButtonGroup extends StatelessWidget { + const FilledButtonGroup({super.key, required this.buttons}); + + final List buttons; + + Widget buildButton(ButtonData buttonData, {bool showLabel = true}) { + final icon = Icon(buttonData.icon, size: 20); + final label = Text( + buttonData.label, + style: kTextStyleButton, + ); + return Tooltip( + message: buttonData.tooltip, + child: FilledButton.icon( + style: FilledButton.styleFrom( + minimumSize: const Size(44, 44), + padding: kPh12, + shape: const ContinuousRectangleBorder()), + onPressed: buttonData.onPressed, + label: showLabel + ? Row( + children: [ + icon, + kHSpacer4, + label, + ], + ) + : icon, + ), + ); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + final showLabel = constraints.maxWidth > buttons.length * 110; + List buttonWidgets = buttons + .map((button) => buildButton(button, showLabel: showLabel)) + .toList(); + + List buttonsWithSpacers = []; + for (int i = 0; i < buttonWidgets.length; i++) { + buttonsWithSpacers.add(buttonWidgets[i]); + if (i < buttonWidgets.length - 1) { + buttonsWithSpacers.add(kHSpacer2); + } + } + return ClipRRect( + borderRadius: kBorderRadius20, + child: Row( + mainAxisSize: MainAxisSize.min, + children: buttonsWithSpacers, + ), + ); + }); + } +} diff --git a/lib/widgets/tabbar_request_response.dart b/lib/widgets/tabbar_segmented.dart similarity index 77% rename from lib/widgets/tabbar_request_response.dart rename to lib/widgets/tabbar_segmented.dart index 5a117120..bb58dc13 100644 --- a/lib/widgets/tabbar_request_response.dart +++ b/lib/widgets/tabbar_segmented.dart @@ -1,20 +1,27 @@ import 'package:flutter/material.dart'; import 'package:apidash/consts.dart'; -class RequestResponseTabbar extends StatelessWidget { - const RequestResponseTabbar({ +class SegmentedTabbar extends StatelessWidget { + const SegmentedTabbar({ super.key, required this.controller, + required this.tabs, + this.tabWidth = kSegmentedTabWidth, + this.tabHeight = kSegmentedTabHeight, }); final TabController controller; + final List tabs; + final double tabWidth; + final double tabHeight; @override Widget build(BuildContext context) { return Center( child: Container( - width: kReqResTabWidth, - height: kReqResTabHeight, + margin: kPh4, + width: tabWidth * tabs.length, + height: tabHeight, decoration: BoxDecoration( borderRadius: kBorderRadius20, border: Border.all( @@ -40,14 +47,7 @@ class RequestResponseTabbar extends StatelessWidget { color: Theme.of(context).colorScheme.primary, ), controller: controller, - tabs: const [ - Tab( - text: kLabelRequest, - ), - Tab( - text: kLabelResponse, - ), - ], + tabs: tabs, ), ), ), diff --git a/lib/widgets/table_request.dart b/lib/widgets/table_request.dart index bdde7c47..014f0c86 100644 --- a/lib/widgets/table_request.dart +++ b/lib/widgets/table_request.dart @@ -20,6 +20,10 @@ class RequestDataTable extends StatelessWidget { final clrScheme = Theme.of(context).colorScheme; final List columns = [ + const DataColumn2( + label: Text(''), + fixedWidth: 8, + ), DataColumn2( label: Text(keyName ?? kNameField), ), @@ -30,6 +34,10 @@ class RequestDataTable extends StatelessWidget { DataColumn2( label: Text(valueName ?? kNameValue), ), + const DataColumn2( + label: Text(''), + fixedWidth: 8, + ), ]; final fieldDecoration = InputDecoration( @@ -52,6 +60,7 @@ class RequestDataTable extends StatelessWidget { .map( (MapEntry entry) => DataRow( cells: [ + const DataCell(kHSpacer5), DataCell( ReadOnlyTextField( initialValue: entry.key, @@ -67,6 +76,7 @@ class RequestDataTable extends StatelessWidget { decoration: fieldDecoration, ), ), + const DataCell(kHSpacer5), ], ), ) diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 9f0323aa..db9b860c 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -1,6 +1,7 @@ export 'button_clear_response.dart'; export 'button_copy.dart'; export 'button_discord.dart'; +export 'button_group_filled.dart'; export 'button_repo.dart'; export 'button_save_download.dart'; export 'button_send.dart'; @@ -48,7 +49,7 @@ export 'splitview_drawer.dart'; export 'splitview_dashboard.dart'; export 'splitview_equal.dart'; export 'splitview_history.dart'; -export 'tabbar_request_response.dart'; +export 'tabbar_segmented.dart'; export 'table_map.dart'; export 'table_request.dart'; export 'tabs.dart'; From 5ad679cab34c60b6e40954388de8ce62304ecb83 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sun, 21 Jul 2024 21:42:40 +0530 Subject: [PATCH 06/71] fix: history body view --- .../history_widgets/his_request_pane.dart | 64 +++++++++- .../request_pane/request_form_data.dart | 21 +--- lib/widgets/button_form_data_file.dart | 31 +++++ lib/widgets/editor.dart | 3 + lib/widgets/table_request.dart | 1 - lib/widgets/table_request_form.dart | 119 ++++++++++++++++++ lib/widgets/widgets.dart | 2 + 7 files changed, 220 insertions(+), 21 deletions(-) create mode 100644 lib/widgets/button_form_data_file.dart create mode 100644 lib/widgets/table_request_form.dart diff --git a/lib/screens/history/history_widgets/his_request_pane.dart b/lib/screens/history/history_widgets/his_request_pane.dart index 6378a871..a2ad35b2 100644 --- a/lib/screens/history/history_widgets/his_request_pane.dart +++ b/lib/screens/history/history_widgets/his_request_pane.dart @@ -53,7 +53,69 @@ class HistoryRequestPane extends ConsumerWidget { rows: headersMap, keyName: kNameHeader, ), - const SizedBox(), + const HisRequestBody(), + ], + ); + } +} + +class HisRequestBody extends ConsumerWidget { + const HisRequestBody({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedHistoryModel = ref.watch(selectedHistoryRequestModelProvider); + final requestModel = selectedHistoryModel?.httpRequestModel; + final contentType = requestModel?.bodyContentType; + + return Column( + children: [ + kVSpacer5, + RichText( + text: TextSpan( + style: Theme.of(context).textTheme.labelLarge, + children: [ + const TextSpan( + text: "Content Type: ", + ), + TextSpan( + text: contentType?.name ?? "text", + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + )), + ], + ), + ), + kVSpacer5, + Expanded( + child: switch (contentType) { + ContentType.formdata => Padding( + padding: kPh4, + child: + RequestFormDataTable(rows: requestModel?.formData ?? [])), + // TODO: Fix JsonTextFieldEditor & plug it here + ContentType.json => Padding( + padding: kPt5o10, + child: TextFieldEditor( + key: Key("${selectedHistoryModel?.historyId}-json-body"), + fieldKey: + "${selectedHistoryModel?.historyId}-json-body-viewer", + initialValue: requestModel?.body, + readOnly: true, + ), + ), + _ => Padding( + padding: kPt5o10, + child: TextFieldEditor( + key: Key("${selectedHistoryModel?.historyId}-body"), + fieldKey: "${selectedHistoryModel?.historyId}-body-viewer", + initialValue: requestModel?.body, + readOnly: true, + ), + ), + }, + ) ], ); } diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_form_data.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_form_data.dart index 5228c660..9b0bb123 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_form_data.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_form_data.dart @@ -124,17 +124,7 @@ class _FormDataBodyState extends ConsumerState { ), DataCell( formRows[index].type == FormDataType.file - ? ElevatedButton.icon( - icon: const Icon( - Icons.snippet_folder_rounded, - size: 20, - ), - style: ElevatedButton.styleFrom( - minimumSize: const Size.fromHeight(kDataTableRowHeight), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), - ), + ? FormDataFileButton( onPressed: () async { var pickedResult = await pickFile(); if (pickedResult != null && @@ -146,14 +136,7 @@ class _FormDataBodyState extends ConsumerState { _onFieldChange(selectedId!); } }, - label: Text( - (formRows[index].type == FormDataType.file && - formRows[index].value.isNotEmpty) - ? formRows[index].value.toString() - : kLabelSelectFile, - overflow: TextOverflow.ellipsis, - style: kFormDataButtonLabelTextStyle, - ), + initialValue: formRows[index].value, ) : CellField( keyId: "$selectedId-$index-form-v-$seed", diff --git a/lib/widgets/button_form_data_file.dart b/lib/widgets/button_form_data_file.dart new file mode 100644 index 00000000..262ea76b --- /dev/null +++ b/lib/widgets/button_form_data_file.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +class FormDataFileButton extends StatelessWidget { + const FormDataFileButton({super.key, this.onPressed, this.initialValue}); + + final VoidCallback? onPressed; + final String? initialValue; + + @override + Widget build(BuildContext context) { + return ElevatedButton.icon( + icon: const Icon( + Icons.snippet_folder_rounded, + size: 20, + ), + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(kDataTableRowHeight), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + onPressed: onPressed, + label: Text( + initialValue ?? kLabelSelectFile, + overflow: TextOverflow.ellipsis, + style: kFormDataButtonLabelTextStyle, + ), + ); + } +} diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 736ffd83..5bdd2f82 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -9,11 +9,13 @@ class TextFieldEditor extends StatefulWidget { required this.fieldKey, this.onChanged, this.initialValue, + this.readOnly = false, }); final String fieldKey; final Function(String)? onChanged; final String? initialValue; + final bool readOnly; @override State createState() => _TextFieldEditorState(); } @@ -69,6 +71,7 @@ class _TextFieldEditorState extends State { keyboardType: TextInputType.multiline, expands: true, maxLines: null, + readOnly: widget.readOnly, style: kCodeStyle, textAlignVertical: TextAlignVertical.top, onChanged: widget.onChanged, diff --git a/lib/widgets/table_request.dart b/lib/widgets/table_request.dart index 014f0c86..010aaa27 100644 --- a/lib/widgets/table_request.dart +++ b/lib/widgets/table_request.dart @@ -103,7 +103,6 @@ class RequestDataTable extends StatelessWidget { ), ), ), - kVSpacer40, ], ), ); diff --git a/lib/widgets/table_request_form.dart b/lib/widgets/table_request_form.dart new file mode 100644 index 00000000..a61c0cd7 --- /dev/null +++ b/lib/widgets/table_request_form.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:data_table_2/data_table_2.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/consts.dart'; + +class RequestFormDataTable extends StatelessWidget { + const RequestFormDataTable({ + super.key, + required this.rows, + this.keyName, + this.valueName, + }); + + final List rows; + final String? keyName; + final String? valueName; + + @override + Widget build(BuildContext context) { + final clrScheme = Theme.of(context).colorScheme; + + final List columns = [ + const DataColumn2( + label: Text(''), + fixedWidth: 8, + ), + DataColumn2( + label: Text(keyName ?? kNameField), + ), + const DataColumn2( + label: Text('='), + fixedWidth: 30, + ), + DataColumn2( + label: Text(valueName ?? kNameValue), + ), + const DataColumn2( + label: Text(''), + fixedWidth: 8, + ), + ]; + + final fieldDecoration = InputDecoration( + contentPadding: const EdgeInsets.only(bottom: 12), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: clrScheme.primary.withOpacity( + kHintOpacity, + ), + ), + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: clrScheme.surfaceContainerHighest, + ), + ), + ); + + final List dataRows = rows + .map( + (FormDataModel entry) => DataRow( + cells: [ + const DataCell(kHSpacer5), + DataCell( + ReadOnlyTextField( + initialValue: entry.name, + decoration: fieldDecoration, + ), + ), + const DataCell( + Text('='), + ), + DataCell( + entry.type == FormDataType.file + ? Tooltip( + message: entry.value, + child: FormDataFileButton( + onPressed: () {}, + initialValue: entry.value, + ), + ) + : ReadOnlyTextField( + initialValue: entry.value, + decoration: fieldDecoration, + ), + ), + const DataCell(kHSpacer5), + ], + ), + ) + .toList(); + + return Container( + margin: kP10, + child: Column( + children: [ + Expanded( + child: Theme( + data: Theme.of(context) + .copyWith(scrollbarTheme: kDataTableScrollbarTheme), + child: DataTable2( + columnSpacing: 12, + dividerThickness: 0, + horizontalMargin: 0, + headingRowHeight: 0, + dataRowHeight: kDataTableRowHeight, + bottomMargin: kDataTableBottomPadding, + isVerticalScrollBarVisible: true, + columns: columns, + rows: dataRows, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index db9b860c..5383916d 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -1,6 +1,7 @@ export 'button_clear_response.dart'; export 'button_copy.dart'; export 'button_discord.dart'; +export 'button_form_data_file.dart'; export 'button_group_filled.dart'; export 'button_repo.dart'; export 'button_save_download.dart'; @@ -51,6 +52,7 @@ export 'splitview_equal.dart'; export 'splitview_history.dart'; export 'tabbar_segmented.dart'; export 'table_map.dart'; +export 'table_request_form.dart'; export 'table_request.dart'; export 'tabs.dart'; export 'texts.dart'; From b2cd91274f759e73194d39140c5c1b4fe0e821dc Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Mon, 22 Jul 2024 00:56:44 +0530 Subject: [PATCH 07/71] wip: manage history dialog --- lib/consts.dart | 18 +++- lib/models/settings_model.dart | 23 ++++- lib/providers/settings_providers.dart | 2 + lib/screens/envvar/environments_pane.dart | 2 +- lib/screens/history/history_pane.dart | 4 + .../history_widgets/his_bottombar.dart | 2 +- .../history_widgets/his_sidebar_header.dart | 16 +++- lib/screens/home_page/collection_pane.dart | 2 +- lib/widgets/dialog_history_retention.dart | 85 +++++++++++++++++++ lib/widgets/request_widgets.dart | 4 + lib/widgets/widgets.dart | 1 + 11 files changed, 149 insertions(+), 10 deletions(-) create mode 100644 lib/widgets/dialog_history_retention.dart diff --git a/lib/consts.dart b/lib/consts.dart index ed2bc4e1..5e8790c0 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -109,6 +109,7 @@ const kPv2 = EdgeInsets.symmetric(vertical: 2); const kPv6 = EdgeInsets.symmetric(vertical: 6); const kPv8 = EdgeInsets.symmetric(vertical: 8); const kPv10 = EdgeInsets.symmetric(vertical: 10); +const kPv20 = EdgeInsets.symmetric(vertical: 20); const kPh2 = EdgeInsets.symmetric(horizontal: 2); const kPt28o8 = EdgeInsets.only(top: 28, left: 8.0, right: 8.0, bottom: 8.0); const kPt5o10 = @@ -116,9 +117,8 @@ const kPt5o10 = const kPh4 = EdgeInsets.symmetric(horizontal: 4); const kPh8 = EdgeInsets.symmetric(horizontal: 8); const kPh12 = EdgeInsets.symmetric(horizontal: 12); -const kPh20 = EdgeInsets.symmetric( - horizontal: 20, -); +const kPh20 = EdgeInsets.symmetric(horizontal: 20); +const kPh24 = EdgeInsets.symmetric(horizontal: 24); const kPh20t40 = EdgeInsets.only( left: 20, right: 20, @@ -178,6 +178,7 @@ const kHSpacer40 = SizedBox(width: 40); const kVSpacer5 = SizedBox(height: 5); const kVSpacer8 = SizedBox(height: 8); const kVSpacer10 = SizedBox(height: 10); +const kVSpacer16 = SizedBox(height: 16); const kVSpacer20 = SizedBox(height: 20); const kVSpacer40 = SizedBox(height: 40); @@ -331,6 +332,17 @@ class ButtonData { final String tooltip; } +enum HistoryRetentionPeriod { + oneWeek("1 Week", Icons.calendar_view_week_rounded), + oneMonth("1 Month", Icons.calendar_view_month_rounded), + threeMonths("3 Months", Icons.calendar_month_rounded), + forever("Forever", Icons.all_inclusive_rounded); + + const HistoryRetentionPeriod(this.label, this.icon); + final String label; + final IconData icon; +} + enum ItemMenuOption { edit("Rename"), delete("Delete"), diff --git a/lib/models/settings_model.dart b/lib/models/settings_model.dart index 846bdf52..e82ffdca 100644 --- a/lib/models/settings_model.dart +++ b/lib/models/settings_model.dart @@ -13,6 +13,7 @@ class SettingsModel { this.saveResponses = true, this.promptBeforeClosing = true, this.activeEnvironmentId, + this.historyRetentionPeriod = HistoryRetentionPeriod.oneWeek, }); final bool isDark; @@ -24,6 +25,7 @@ class SettingsModel { final bool saveResponses; final bool promptBeforeClosing; final String? activeEnvironmentId; + final HistoryRetentionPeriod historyRetentionPeriod; SettingsModel copyWith({ bool? isDark, @@ -35,6 +37,7 @@ class SettingsModel { bool? saveResponses, bool? promptBeforeClosing, String? activeEnvironmentId, + HistoryRetentionPeriod? historyRetentionPeriod, }) { return SettingsModel( isDark: isDark ?? this.isDark, @@ -47,6 +50,8 @@ class SettingsModel { saveResponses: saveResponses ?? this.saveResponses, promptBeforeClosing: promptBeforeClosing ?? this.promptBeforeClosing, activeEnvironmentId: activeEnvironmentId ?? this.activeEnvironmentId, + historyRetentionPeriod: + historyRetentionPeriod ?? this.historyRetentionPeriod, ); } @@ -80,6 +85,18 @@ class SettingsModel { final saveResponses = data["saveResponses"] as bool?; final promptBeforeClosing = data["promptBeforeClosing"] as bool?; final activeEnvironmentId = data["activeEnvironmentId"] as String?; + final historyRetentionPeriodStr = data["historyRetentionPeriod"] as String?; + HistoryRetentionPeriod historyRetentionPeriod = + HistoryRetentionPeriod.oneWeek; + if (historyRetentionPeriodStr != null) { + try { + historyRetentionPeriod = + HistoryRetentionPeriod.values.byName(historyRetentionPeriodStr); + } catch (e) { + // pass + } + } + const sm = SettingsModel(); return sm.copyWith( @@ -92,6 +109,7 @@ class SettingsModel { saveResponses: saveResponses, promptBeforeClosing: promptBeforeClosing, activeEnvironmentId: activeEnvironmentId, + historyRetentionPeriod: historyRetentionPeriod, ); } @@ -108,6 +126,7 @@ class SettingsModel { "saveResponses": saveResponses, "promptBeforeClosing": promptBeforeClosing, "activeEnvironmentId": activeEnvironmentId, + "historyRetentionPeriod": historyRetentionPeriod.name, }; } @@ -129,7 +148,8 @@ class SettingsModel { other.defaultCodeGenLang == defaultCodeGenLang && other.saveResponses == saveResponses && other.promptBeforeClosing == promptBeforeClosing && - other.activeEnvironmentId == activeEnvironmentId; + other.activeEnvironmentId == activeEnvironmentId && + other.historyRetentionPeriod == historyRetentionPeriod; } @override @@ -145,6 +165,7 @@ class SettingsModel { saveResponses, promptBeforeClosing, activeEnvironmentId, + historyRetentionPeriod, ); } } diff --git a/lib/providers/settings_providers.dart b/lib/providers/settings_providers.dart index 0fff037b..6293e04b 100644 --- a/lib/providers/settings_providers.dart +++ b/lib/providers/settings_providers.dart @@ -30,6 +30,7 @@ class ThemeStateNotifier extends StateNotifier { bool? saveResponses, bool? promptBeforeClosing, String? activeEnvironmentId, + HistoryRetentionPeriod? historyRetentionPeriod, }) async { state = state.copyWith( isDark: isDark, @@ -41,6 +42,7 @@ class ThemeStateNotifier extends StateNotifier { saveResponses: saveResponses, promptBeforeClosing: promptBeforeClosing, activeEnvironmentId: activeEnvironmentId, + historyRetentionPeriod: historyRetentionPeriod, ); await hiveHandler.saveSettings(state.toJson()); } diff --git a/lib/screens/envvar/environments_pane.dart b/lib/screens/envvar/environments_pane.dart index 340cc0e8..dd6b5af4 100644 --- a/lib/screens/envvar/environments_pane.dart +++ b/lib/screens/envvar/environments_pane.dart @@ -190,8 +190,8 @@ class EnvironmentItem extends ConsumerWidget { ref.read(activeEnvironmentIdStateProvider.notifier).state = id; }, onTap: () { - ref.read(mobileScaffoldKeyStateProvider).currentState?.closeDrawer(); ref.read(selectedEnvironmentIdStateProvider.notifier).state = id; + ref.read(mobileScaffoldKeyStateProvider).currentState?.closeDrawer(); }, focusNode: ref.watch(nameTextFieldFocusNodeProvider), onChangedNameEditor: (value) { diff --git a/lib/screens/history/history_pane.dart b/lib/screens/history/history_pane.dart index 8b090b95..ba89c565 100644 --- a/lib/screens/history/history_pane.dart +++ b/lib/screens/history/history_pane.dart @@ -150,6 +150,10 @@ class _HistoryExpansionTileState extends ConsumerState ref .read(historyMetaStateNotifier.notifier) .loadHistoryRequest(item.first.historyId); + ref + .read(mobileScaffoldKeyStateProvider) + .currentState + ?.closeDrawer(); }, ), ); diff --git a/lib/screens/history/history_widgets/his_bottombar.dart b/lib/screens/history/history_widgets/his_bottombar.dart index 9945b7bc..da6b0ffe 100644 --- a/lib/screens/history/history_widgets/his_bottombar.dart +++ b/lib/screens/history/history_widgets/his_bottombar.dart @@ -82,7 +82,7 @@ class HistorySheetButton extends StatelessWidget { isScrollControlled: true, builder: (context) { return ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 500), + constraints: const BoxConstraints(maxWidth: 400), child: const HistorRequestsScrollableSheet()); }, ); diff --git a/lib/screens/history/history_widgets/his_sidebar_header.dart b/lib/screens/history/history_widgets/his_sidebar_header.dart index e431c4b7..6af1e1e8 100644 --- a/lib/screens/history/history_widgets/his_sidebar_header.dart +++ b/lib/screens/history/history_widgets/his_sidebar_header.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; class HistorySidebarHeader extends ConsumerWidget { @@ -21,13 +22,22 @@ class HistorySidebarHeader extends ConsumerWidget { ), const Spacer(), IconButton( - tooltip: "Auto Delete Settings", + tooltip: "Manage History", style: IconButton.styleFrom( foregroundColor: Theme.of(context).colorScheme.primary, ), - onPressed: () {}, + onPressed: () { + showHistoryRetentionDialog( + context, + ref.read(settingsProvider.select( + (value) => value.historyRetentionPeriod)), (value) { + ref.read(settingsProvider.notifier).update( + historyRetentionPeriod: value, + ); + }); + }, icon: const Icon( - Icons.auto_delete_outlined, + Icons.manage_history_rounded, size: 20, ), ), diff --git a/lib/screens/home_page/collection_pane.dart b/lib/screens/home_page/collection_pane.dart index 521fdad8..7c40ef3f 100644 --- a/lib/screens/home_page/collection_pane.dart +++ b/lib/screens/home_page/collection_pane.dart @@ -220,8 +220,8 @@ class RequestItem extends ConsumerWidget { selectedId: selectedId, editRequestId: editRequestId, onTap: () { - ref.read(mobileScaffoldKeyStateProvider).currentState?.closeDrawer(); ref.read(selectedIdStateProvider.notifier).state = id; + ref.read(mobileScaffoldKeyStateProvider).currentState?.closeDrawer(); }, // onDoubleTap: () { // ref.read(selectedIdStateProvider.notifier).state = id; diff --git a/lib/widgets/dialog_history_retention.dart b/lib/widgets/dialog_history_retention.dart new file mode 100644 index 00000000..67f513ac --- /dev/null +++ b/lib/widgets/dialog_history_retention.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +showHistoryRetentionDialog( + BuildContext context, + HistoryRetentionPeriod historyRetentionPeriod, + Function(HistoryRetentionPeriod) onRetentionPeriodChange, +) { + HistoryRetentionPeriod selectedRetentionPeriod = historyRetentionPeriod; + + showDialog( + context: context, + builder: (context) { + return AlertDialog( + icon: const Icon(Icons.manage_history_rounded), + iconColor: Theme.of(context).colorScheme.primary, + title: const Text("Manage History"), + titleTextStyle: Theme.of(context).textTheme.titleLarge, + contentPadding: kPv20, + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: kPh24, + child: Text( + "Select the duration for which you want to retain your request history", + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.8), + ), + ), + ), + kVSpacer10, + ...HistoryRetentionPeriod.values + .map((e) => RadioListTile( + title: Text( + e.label, + style: TextStyle( + color: selectedRetentionPeriod == e + ? Theme.of(context).colorScheme.primary + : null), + ), + secondary: Icon(e.icon, + color: selectedRetentionPeriod == e + ? Theme.of(context).colorScheme.primary + : Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.6)), + value: e, + groupValue: selectedRetentionPeriod, + onChanged: (value) { + if (value != null) { + selectedRetentionPeriod = value; + (context as Element).markNeedsBuild(); + } + }, + )) + ], + ), + ), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.pop(context); + }, + ), + TextButton( + child: const Text('Confirm'), + onPressed: () { + onRetentionPeriodChange(selectedRetentionPeriod); + Navigator.pop(context); + }, + ), + ], + ); + }, + ); +} diff --git a/lib/widgets/request_widgets.dart b/lib/widgets/request_widgets.dart index 1e7b7ccf..5ad400aa 100644 --- a/lib/widgets/request_widgets.dart +++ b/lib/widgets/request_widgets.dart @@ -52,6 +52,10 @@ class _RequestPaneState extends State mainAxisAlignment: MainAxisAlignment.end, children: [ FilledButton.tonalIcon( + style: FilledButton.styleFrom( + padding: kPh12, + minimumSize: const Size(44, 44), + ), onPressed: widget.onPressedCodeButton, icon: Icon( widget.codePaneVisible diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 5383916d..fcb97a6b 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -15,6 +15,7 @@ export 'checkbox.dart'; export 'code_previewer.dart'; export 'codegen_previewer.dart'; export 'dialog_about.dart'; +export 'dialog_history_retention.dart'; export 'dialog_import.dart'; export 'dialog_rename.dart'; export 'drag_and_drop_area.dart'; From f3e3d30f849987995999230e15619ab48aa8a7f1 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Mon, 22 Jul 2024 11:30:31 +0530 Subject: [PATCH 08/71] wip: history popup menu --- .../common_widgets/environment_dropdown.dart | 4 +- lib/screens/settings_page.dart | 64 +++++++------------ lib/widgets/popup_menu_codegen.dart | 4 +- lib/widgets/popup_menu_env.dart | 20 +++--- lib/widgets/popup_menu_history.dart | 57 +++++++++++++++++ lib/widgets/popup_menu_uri.dart | 4 +- lib/widgets/widgets.dart | 1 + 7 files changed, 96 insertions(+), 58 deletions(-) create mode 100644 lib/widgets/popup_menu_history.dart diff --git a/lib/screens/common_widgets/environment_dropdown.dart b/lib/screens/common_widgets/environment_dropdown.dart index db012b32..cad6a17a 100644 --- a/lib/screens/common_widgets/environment_dropdown.dart +++ b/lib/screens/common_widgets/environment_dropdown.dart @@ -22,8 +22,8 @@ class EnvironmentDropdown extends ConsumerWidget { borderRadius: kBorderRadius8, ), child: EnvironmentPopupMenu( - activeEnvironment: environments?[activeEnvironment], - environments: environmentsList, + value: environments?[activeEnvironment], + items: environmentsList, onChanged: (value) { ref.read(activeEnvironmentIdStateProvider.notifier).state = value?.id; diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index c7426524..da00bfd2 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -81,27 +81,6 @@ class SettingsPage extends ConsumerWidget { }, items: kSupportedUriSchemes, ), - // DropdownButtonHideUnderline( - // child: DropdownButton( - // borderRadius: kBorderRadius8, - // onChanged: (value) { - // ref - // .read(settingsProvider.notifier) - // .update(defaultUriScheme: value); - // }, - // value: settings.defaultUriScheme, - // items: kSupportedUriSchemes - // .map>((String value) { - // return DropdownMenuItem( - // value: value, - // child: Padding( - // padding: kP10, - // child: Text(value), - // ), - // ); - // }).toList(), - // ), - // ), ), ), ListTile( @@ -123,26 +102,6 @@ class SettingsPage extends ConsumerWidget { }, items: CodegenLanguage.values, ), - // DropdownButtonHideUnderline( - // child: DropdownButton( - // borderRadius: kBorderRadius8, - // value: settings.defaultCodeGenLang, - // onChanged: (value) { - // ref - // .read(settingsProvider.notifier) - // .update(defaultCodeGenLang: value); - // }, - // items: CodegenLanguage.values.map((value) { - // return DropdownMenuItem( - // value: value, - // child: Padding( - // padding: kP10, - // child: Text(value.label), - // ), - // ); - // }).toList(), - // ), - // ), ), ), CheckboxListTile( @@ -186,6 +145,29 @@ class SettingsPage extends ConsumerWidget { ), ), ), + ListTile( + hoverColor: kColorTransparent, + title: const Text('History Retention Period'), + subtitle: Text( + 'Your request history will be retained for ${settings.historyRetentionPeriod.label}'), + trailing: Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.onSurface, + ), + borderRadius: kBorderRadius8, + ), + child: HistoryRetentionPopupMenu( + value: settings.historyRetentionPeriod, + onChanged: (value) { + ref + .read(settingsProvider.notifier) + .update(historyRetentionPeriod: value); + }, + items: HistoryRetentionPeriod.values, + ), + ), + ), ListTile( hoverColor: kColorTransparent, title: const Text('Clear Data'), diff --git a/lib/widgets/popup_menu_codegen.dart b/lib/widgets/popup_menu_codegen.dart index 6bb164f6..421d1ce7 100644 --- a/lib/widgets/popup_menu_codegen.dart +++ b/lib/widgets/popup_menu_codegen.dart @@ -1,6 +1,6 @@ -import 'package:apidash/consts.dart'; -import 'package:apidash/extensions/extensions.dart'; import 'package:flutter/material.dart'; +import 'package:apidash/extensions/extensions.dart'; +import 'package:apidash/consts.dart'; class CodegenPopupMenu extends StatelessWidget { const CodegenPopupMenu({ diff --git a/lib/widgets/popup_menu_env.dart b/lib/widgets/popup_menu_env.dart index 9b23ed9a..d61be5b4 100644 --- a/lib/widgets/popup_menu_env.dart +++ b/lib/widgets/popup_menu_env.dart @@ -1,24 +1,24 @@ -import 'package:apidash/consts.dart'; -import 'package:apidash/extensions/extensions.dart'; import 'package:flutter/material.dart'; +import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/models/models.dart'; import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; class EnvironmentPopupMenu extends StatelessWidget { const EnvironmentPopupMenu({ super.key, - this.activeEnvironment, + this.value, this.onChanged, - this.environments, + this.items, }); - final EnvironmentModel? activeEnvironment; + final EnvironmentModel? value; final void Function(EnvironmentModel? value)? onChanged; - final List? environments; + final List? items; final EnvironmentModel? noneEnvironmentModel = null; @override Widget build(BuildContext context) { - final activeEnvironmentName = getEnvironmentTitle(activeEnvironment?.name); + final valueName = getEnvironmentTitle(value?.name); final textClipLength = context.isCompactWindow ? 6 : 10; final double boxLength = context.isCompactWindow ? 100 : 130; return PopupMenuButton( @@ -34,7 +34,7 @@ class EnvironmentPopupMenu extends StatelessWidget { }, child: const Text("None"), ), - ...environments!.map((EnvironmentModel environment) { + ...items!.map((EnvironmentModel environment) { final name = getEnvironmentTitle(environment.name).clip(30); return PopupMenuItem( value: environment, @@ -55,9 +55,7 @@ class EnvironmentPopupMenu extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - activeEnvironment == null - ? "None" - : activeEnvironmentName.clip(textClipLength), + value == null ? "None" : valueName.clip(textClipLength), softWrap: false, ), const Icon( diff --git a/lib/widgets/popup_menu_history.dart b/lib/widgets/popup_menu_history.dart new file mode 100644 index 00000000..3f2af6ee --- /dev/null +++ b/lib/widgets/popup_menu_history.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/extensions/extensions.dart'; +import 'package:apidash/consts.dart'; + +class HistoryRetentionPopupMenu extends StatelessWidget { + const HistoryRetentionPopupMenu({ + super.key, + required this.value, + required this.onChanged, + this.items, + }); + + final HistoryRetentionPeriod value; + final void Function(HistoryRetentionPeriod value) onChanged; + final List? items; + @override + Widget build(BuildContext context) { + final double boxLength = context.isCompactWindow ? 110 : 130; + return PopupMenuButton( + tooltip: "Select retention period", + surfaceTintColor: kColorTransparent, + constraints: BoxConstraints(minWidth: boxLength), + itemBuilder: (BuildContext context) { + return [ + ...items!.map((period) { + return PopupMenuItem( + value: period, + child: Text( + period.label, + softWrap: false, + overflow: TextOverflow.ellipsis, + ), + ); + }) + ]; + }, + onSelected: onChanged, + child: Container( + width: boxLength, + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + value.label, + style: kTextStylePopupMenuItem, + ), + const Icon( + Icons.unfold_more, + size: 16, + ) + ], + ), + ), + ); + } +} diff --git a/lib/widgets/popup_menu_uri.dart b/lib/widgets/popup_menu_uri.dart index a6a3bb6c..177381b0 100644 --- a/lib/widgets/popup_menu_uri.dart +++ b/lib/widgets/popup_menu_uri.dart @@ -1,6 +1,6 @@ -import 'package:apidash/consts.dart'; -import 'package:apidash/extensions/extensions.dart'; import 'package:flutter/material.dart'; +import 'package:apidash/extensions/extensions.dart'; +import 'package:apidash/consts.dart'; class URIPopupMenu extends StatelessWidget { const URIPopupMenu({ diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index fcb97a6b..06bc4bef 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -42,6 +42,7 @@ export 'menu_sidebar_top.dart'; export 'overlay_widget.dart'; export 'popup_menu_codegen.dart'; export 'popup_menu_env.dart'; +export 'popup_menu_history.dart'; export 'popup_menu_uri.dart'; export 'previewer.dart'; export 'request_widgets.dart'; From bed7ad347c4fd77e13b2e11e69b6b47e421647b7 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Mon, 22 Jul 2024 21:19:33 +0530 Subject: [PATCH 09/71] feat: auto clear history --- lib/main.dart | 1 + lib/services/history_service.dart | 51 +++++++++++++++++++++++++++ lib/services/hive_services.dart | 2 +- lib/services/services.dart | 1 + lib/utils/history_utils.dart | 16 +++++++++ test/providers/ui_providers_test.dart | 3 ++ 6 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 lib/services/history_service.dart diff --git a/lib/main.dart b/lib/main.dart index 5ac878aa..d9730a03 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,7 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); GoogleFonts.config.allowRuntimeFetching = false; await openBoxes(); + await autoClearHistory(); if (kIsLinux) { await setupInitialWindow(); } diff --git a/lib/services/history_service.dart b/lib/services/history_service.dart new file mode 100644 index 00000000..d34d17fb --- /dev/null +++ b/lib/services/history_service.dart @@ -0,0 +1,51 @@ +import 'package:apidash/models/models.dart'; +import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; +import 'hive_services.dart'; + +Future autoClearHistory() async { + final settingsMap = hiveHandler.settings; + final retentionPeriod = settingsMap['historyRetentionPeriod']; + + HistoryRetentionPeriod historyRetentionPeriod = + HistoryRetentionPeriod.oneWeek; + if (retentionPeriod != null) { + historyRetentionPeriod = + HistoryRetentionPeriod.values.byName(retentionPeriod); + } + DateTime? retentionDate = getRetentionDate(historyRetentionPeriod); + + if (retentionDate == null) { + return; + } else { + List? historyIds = hiveHandler.getHistoryIds(); + List toRemoveIds = []; + + if (historyIds == null || historyIds.isEmpty) { + return; + } + + for (var historyId in historyIds) { + var jsonModel = hiveHandler.getHistoryMeta(historyId); + if (jsonModel != null) { + var jsonMap = Map.from(jsonModel); + HistoryMetaModel historyMetaModelFromJson = + HistoryMetaModel.fromJson(jsonMap); + if (historyMetaModelFromJson.timeStamp.isBefore(retentionDate)) { + toRemoveIds.add(historyId); + } + } + } + + if (toRemoveIds.isEmpty) { + return; + } + + for (var id in toRemoveIds) { + await hiveHandler.deleteHistoryRequest(id); + hiveHandler.deleteHistoryMeta(id); + } + hiveHandler.setHistoryIds( + historyIds..removeWhere((id) => toRemoveIds.contains(id))); + } +} diff --git a/lib/services/hive_services.dart b/lib/services/hive_services.dart index c6a3cc40..1a0f7ac0 100644 --- a/lib/services/hive_services.dart +++ b/lib/services/hive_services.dart @@ -97,7 +97,7 @@ class HiveHandler { String id, Map? historyRequestJsoon) => historyLazyBox.put(id, historyRequestJsoon); - Future deleteHistoryReqyest(String id) => historyLazyBox.delete(id); + Future deleteHistoryRequest(String id) => historyLazyBox.delete(id); Future clear() async { await dataBox.clear(); diff --git a/lib/services/services.dart b/lib/services/services.dart index a7cf03fd..7551de9b 100644 --- a/lib/services/services.dart +++ b/lib/services/services.dart @@ -1,3 +1,4 @@ export 'http_service.dart'; export 'hive_services.dart'; +export 'history_service.dart'; export 'window_services.dart'; diff --git a/lib/utils/history_utils.dart b/lib/utils/history_utils.dart index acbea853..c91a5e68 100644 --- a/lib/utils/history_utils.dart +++ b/lib/utils/history_utils.dart @@ -113,3 +113,19 @@ List getRequestGroup( requestGroup.sort((a, b) => b.timeStamp.compareTo(a.timeStamp)); return requestGroup; } + +DateTime? getRetentionDate(HistoryRetentionPeriod retentionPeriod) { + DateTime now = DateTime.now(); + DateTime today = DateTime(now.year, now.month, now.day); + + switch (retentionPeriod) { + case HistoryRetentionPeriod.oneWeek: + return today.subtract(const Duration(days: 7)); + case HistoryRetentionPeriod.oneMonth: + return today.subtract(const Duration(days: 30)); + case HistoryRetentionPeriod.threeMonths: + return today.subtract(const Duration(days: 90)); + default: + return null; + } +} diff --git a/test/providers/ui_providers_test.dart b/test/providers/ui_providers_test.dart index 5854f768..1bd76c4d 100644 --- a/test/providers/ui_providers_test.dart +++ b/test/providers/ui_providers_test.dart @@ -1,4 +1,7 @@ import 'dart:io'; + +import 'package:spot/spot.dart'; +import 'package:apidash/consts.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/screens/common_widgets/common_widgets.dart'; import 'package:apidash/screens/dashboard.dart'; From 3265269c255ac8b9da349f1459c1277dfa4ad02d Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Mon, 22 Jul 2024 21:39:06 +0530 Subject: [PATCH 10/71] refactor: imports --- lib/screens/mobile/dashboard.dart | 6 +++--- lib/utils/history_utils.dart | 4 ++-- lib/widgets/card_history_request.dart | 4 ++-- lib/widgets/card_sidebar_history.dart | 4 ++-- lib/widgets/table_request.dart | 2 +- lib/widgets/table_request_form.dart | 3 ++- test/providers/ui_providers_test.dart | 1 - 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/screens/mobile/dashboard.dart b/lib/screens/mobile/dashboard.dart index a74e6094..8da58375 100644 --- a/lib/screens/mobile/dashboard.dart +++ b/lib/screens/mobile/dashboard.dart @@ -1,15 +1,15 @@ -import 'package:apidash/screens/history/history_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/providers/providers.dart'; -import '../settings_page.dart'; -import 'navbar.dart'; import 'requests_page/requests_page.dart'; import '../envvar/environment_page.dart'; +import '../history/history_page.dart'; +import '../settings_page.dart'; import 'widgets/page_base.dart'; +import 'navbar.dart'; class MobileDashboard extends ConsumerStatefulWidget { const MobileDashboard({super.key}); diff --git a/lib/utils/history_utils.dart b/lib/utils/history_utils.dart index c91a5e68..cce87998 100644 --- a/lib/utils/history_utils.dart +++ b/lib/utils/history_utils.dart @@ -1,6 +1,6 @@ -import 'package:apidash/utils/convert_utils.dart'; import 'package:apidash/models/models.dart'; import 'package:apidash/consts.dart'; +import 'convert_utils.dart'; DateTime stripTime(DateTime dateTime) { return DateTime(dateTime.year, dateTime.month, dateTime.day); @@ -116,7 +116,7 @@ List getRequestGroup( DateTime? getRetentionDate(HistoryRetentionPeriod retentionPeriod) { DateTime now = DateTime.now(); - DateTime today = DateTime(now.year, now.month, now.day); + DateTime today = stripTime(now); switch (retentionPeriod) { case HistoryRetentionPeriod.oneWeek: diff --git a/lib/widgets/card_history_request.dart b/lib/widgets/card_history_request.dart index a76e477c..228cbf5b 100644 --- a/lib/widgets/card_history_request.dart +++ b/lib/widgets/card_history_request.dart @@ -1,7 +1,7 @@ -import 'package:apidash/models/history_meta_model.dart'; import 'package:flutter/material.dart'; -import 'package:apidash/consts.dart'; +import 'package:apidash/models/models.dart'; import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; import 'texts.dart'; class HistoryRequestCard extends StatelessWidget { diff --git a/lib/widgets/card_sidebar_history.dart b/lib/widgets/card_sidebar_history.dart index be6387b3..9b1a9f20 100644 --- a/lib/widgets/card_sidebar_history.dart +++ b/lib/widgets/card_sidebar_history.dart @@ -1,7 +1,7 @@ -import 'package:apidash/models/history_meta_model.dart'; import 'package:flutter/material.dart'; -import 'package:apidash/consts.dart'; +import 'package:apidash/models/models.dart'; import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; import 'texts.dart' show MethodBox; class SidebarHistoryCard extends StatelessWidget { diff --git a/lib/widgets/table_request.dart b/lib/widgets/table_request.dart index 010aaa27..8c3d9055 100644 --- a/lib/widgets/table_request.dart +++ b/lib/widgets/table_request.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:data_table_2/data_table_2.dart'; -import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; +import 'field_read_only.dart'; class RequestDataTable extends StatelessWidget { const RequestDataTable({ diff --git a/lib/widgets/table_request_form.dart b/lib/widgets/table_request_form.dart index a61c0cd7..91a1b39d 100644 --- a/lib/widgets/table_request_form.dart +++ b/lib/widgets/table_request_form.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:data_table_2/data_table_2.dart'; -import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/models/models.dart'; import 'package:apidash/consts.dart'; +import 'button_form_data_file.dart'; +import 'field_read_only.dart'; class RequestFormDataTable extends StatelessWidget { const RequestFormDataTable({ diff --git a/test/providers/ui_providers_test.dart b/test/providers/ui_providers_test.dart index 1bd76c4d..81fb7656 100644 --- a/test/providers/ui_providers_test.dart +++ b/test/providers/ui_providers_test.dart @@ -1,5 +1,4 @@ import 'dart:io'; - import 'package:spot/spot.dart'; import 'package:apidash/consts.dart'; import 'package:apidash/providers/providers.dart'; From dace495264f478f5e9e10f3c5498221b41311254 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Tue, 23 Jul 2024 16:02:46 +0530 Subject: [PATCH 11/71] fix: ui tests --- lib/consts.dart | 6 ++- lib/providers/ui_providers.dart | 4 +- lib/screens/common_widgets/button_navbar.dart | 14 ++++--- lib/screens/dashboard.dart | 19 ++++----- lib/screens/envvar/environment_page.dart | 4 +- lib/screens/envvar/environments_pane.dart | 2 +- lib/screens/history/history_page.dart | 5 +-- lib/screens/history/history_pane.dart | 5 +-- lib/screens/home_page/collection_pane.dart | 4 +- lib/screens/home_page/home_page.dart | 24 +++++++----- lib/screens/mobile/dashboard.dart | 13 ++----- lib/screens/mobile/navbar.dart | 2 +- .../mobile/requests_page/requests_page.dart | 5 +-- lib/screens/settings_page.dart | 2 +- lib/utils/ui_utils.dart | 11 ++++++ lib/widgets/popup_menu_history.dart | 6 +-- lib/widgets/popup_menu_uri.dart | 2 +- test/models/settings_model_test.dart | 10 +++-- test/providers/ui_providers_test.dart | 39 ++++++++++++++++++- 19 files changed, 108 insertions(+), 69 deletions(-) diff --git a/lib/consts.dart b/lib/consts.dart index 5e8790c0..73de67f6 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -71,6 +71,10 @@ final kCodeStyle = TextStyle( fontFamilyFallback: kFontFamilyFallback, ); +final kHomeScaffoldKey = GlobalKey(); +final kEnvScaffoldKey = GlobalKey(); +final kHisScaffoldKey = GlobalKey(); + const kHintOpacity = 0.6; const kForegroundOpacity = 0.05; const kOverlayBackgroundOpacity = 0.5; @@ -82,7 +86,7 @@ const kFormDataButtonLabelTextStyle = TextStyle( fontSize: 12, fontWeight: FontWeight.w600, ); -const kTextStylePopupMenuItem = TextStyle(fontSize: 16); +const kTextStylePopupMenuItem = TextStyle(fontSize: 14); final kButtonSidebarStyle = ElevatedButton.styleFrom(padding: kPh12); diff --git a/lib/providers/ui_providers.dart b/lib/providers/ui_providers.dart index 0478b040..d8076935 100644 --- a/lib/providers/ui_providers.dart +++ b/lib/providers/ui_providers.dart @@ -2,8 +2,8 @@ import 'package:apidash/consts.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -final mobileScaffoldKeyStateProvider = StateProvider>( - (ref) => GlobalKey()); +final mobileScaffoldKeyStateProvider = + StateProvider>((ref) => kHomeScaffoldKey); final leftDrawerStateProvider = StateProvider((ref) => false); final navRailIndexStateProvider = StateProvider((ref) => 0); final selectedIdEditStateProvider = StateProvider((ref) => null); diff --git a/lib/screens/common_widgets/button_navbar.dart b/lib/screens/common_widgets/button_navbar.dart index f18c695c..bce7687f 100644 --- a/lib/screens/common_widgets/button_navbar.dart +++ b/lib/screens/common_widgets/button_navbar.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; +import 'package:apidash/utils/utils.dart'; class NavbarButton extends ConsumerWidget { const NavbarButton({ @@ -25,19 +26,20 @@ class NavbarButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final mobileScaffoldKey = ref.watch(mobileScaffoldKeyStateProvider); + final mobileScaffoldKeyNotifier = + ref.watch(mobileScaffoldKeyStateProvider.notifier); final bool isSelected = railIdx == buttonIdx; final Size size = isCompact ? const Size(56, 32) : const Size(65, 32); var onPress = isSelected ? null : () { if (buttonIdx != null) { - ref.read(navRailIndexStateProvider.notifier).state = buttonIdx!; + final scaffoldKey = getScaffoldKey(buttonIdx!); + ref.watch(navRailIndexStateProvider.notifier).state = buttonIdx!; + mobileScaffoldKeyNotifier.state = scaffoldKey; if ((railIdx > 2 && buttonIdx! <= 2) || - !(ref - .read(mobileScaffoldKeyStateProvider) - .currentState - ?.isDrawerOpen ?? - true)) { + !(mobileScaffoldKey.currentState?.isDrawerOpen ?? true)) { ref.read(leftDrawerStateProvider.notifier).state = false; } } diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index c9b57475..c0f1532f 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -1,4 +1,3 @@ -import 'package:apidash/screens/history/history_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; @@ -7,6 +6,7 @@ import 'package:apidash/consts.dart'; import 'common_widgets/common_widgets.dart'; import 'envvar/environment_page.dart'; import 'home_page/home_page.dart'; +import 'history/history_page.dart'; import 'settings_page.dart'; class Dashboard extends ConsumerWidget { @@ -15,7 +15,6 @@ class Dashboard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final railIdx = ref.watch(navRailIndexStateProvider); - final mobileScaffoldKey = ref.watch(mobileScaffoldKeyStateProvider); return Scaffold( body: SafeArea( child: Row( @@ -61,7 +60,7 @@ class Dashboard extends ConsumerWidget { ref.read(navRailIndexStateProvider.notifier).state = 2; }, icon: const Icon(Icons.history_outlined), - selectedIcon: const Icon(Icons.history), + selectedIcon: const Icon(Icons.history_rounded), ), Text( 'History', @@ -113,15 +112,11 @@ class Dashboard extends ConsumerWidget { child: IndexedStack( alignment: AlignmentDirectional.topCenter, index: railIdx, - children: [ - const HomePage(), - EnvironmentPage( - scaffoldKey: mobileScaffoldKey, - ), - HistoryPage( - scaffoldKey: mobileScaffoldKey, - ), - const SettingsPage(), + children: const [ + HomePage(), + EnvironmentPage(), + HistoryPage(), + SettingsPage(), ], ), ) diff --git a/lib/screens/envvar/environment_page.dart b/lib/screens/envvar/environment_page.dart index bbf1dcec..360b4551 100644 --- a/lib/screens/envvar/environment_page.dart +++ b/lib/screens/envvar/environment_page.dart @@ -12,10 +12,8 @@ import 'environment_editor.dart'; class EnvironmentPage extends ConsumerWidget { const EnvironmentPage({ super.key, - required this.scaffoldKey, }); - final GlobalKey scaffoldKey; @override Widget build(BuildContext context, WidgetRef ref) { final id = ref.watch(selectedEnvironmentIdStateProvider); @@ -23,7 +21,7 @@ class EnvironmentPage extends ConsumerWidget { selectedEnvironmentModelProvider.select((value) => value?.name))); if (context.isMediumWindow) { return DrawerSplitView( - scaffoldKey: scaffoldKey, + scaffoldKey: kEnvScaffoldKey, mainContent: const EnvironmentEditor(), title: EditorTitle( title: name, diff --git a/lib/screens/envvar/environments_pane.dart b/lib/screens/envvar/environments_pane.dart index dd6b5af4..34d58e4b 100644 --- a/lib/screens/envvar/environments_pane.dart +++ b/lib/screens/envvar/environments_pane.dart @@ -191,7 +191,7 @@ class EnvironmentItem extends ConsumerWidget { }, onTap: () { ref.read(selectedEnvironmentIdStateProvider.notifier).state = id; - ref.read(mobileScaffoldKeyStateProvider).currentState?.closeDrawer(); + kEnvScaffoldKey.currentState?.closeDrawer(); }, focusNode: ref.watch(nameTextFieldFocusNodeProvider), onChangedNameEditor: (value) { diff --git a/lib/screens/history/history_page.dart b/lib/screens/history/history_page.dart index 0f288fb7..d56c90f7 100644 --- a/lib/screens/history/history_page.dart +++ b/lib/screens/history/history_page.dart @@ -4,16 +4,15 @@ import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; import 'history_pane.dart'; import 'history_viewer.dart'; class HistoryPage extends ConsumerWidget { const HistoryPage({ super.key, - required this.scaffoldKey, }); - final GlobalKey scaffoldKey; @override Widget build(BuildContext context, WidgetRef ref) { final historyModel = ref.watch(selectedHistoryRequestModelProvider); @@ -22,7 +21,7 @@ class HistoryPage extends ConsumerWidget { : 'History'; if (context.isMediumWindow) { return DrawerSplitView( - scaffoldKey: scaffoldKey, + scaffoldKey: kHisScaffoldKey, mainContent: const HistoryViewer(), title: Text(title), leftDrawerContent: const HistoryPane(), diff --git a/lib/screens/history/history_pane.dart b/lib/screens/history/history_pane.dart index ba89c565..d76eac17 100644 --- a/lib/screens/history/history_pane.dart +++ b/lib/screens/history/history_pane.dart @@ -150,10 +150,7 @@ class _HistoryExpansionTileState extends ConsumerState ref .read(historyMetaStateNotifier.notifier) .loadHistoryRequest(item.first.historyId); - ref - .read(mobileScaffoldKeyStateProvider) - .currentState - ?.closeDrawer(); + kHisScaffoldKey.currentState?.closeDrawer(); }, ), ); diff --git a/lib/screens/home_page/collection_pane.dart b/lib/screens/home_page/collection_pane.dart index 7c40ef3f..b30eded2 100644 --- a/lib/screens/home_page/collection_pane.dart +++ b/lib/screens/home_page/collection_pane.dart @@ -1,7 +1,7 @@ -import 'package:apidash/importer/importer.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; +import 'package:apidash/importer/importer.dart'; import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/models/models.dart'; @@ -221,7 +221,7 @@ class RequestItem extends ConsumerWidget { editRequestId: editRequestId, onTap: () { ref.read(selectedIdStateProvider.notifier).state = id; - ref.read(mobileScaffoldKeyStateProvider).currentState?.closeDrawer(); + kHomeScaffoldKey.currentState?.closeDrawer(); }, // onDoubleTap: () { // ref.read(selectedIdStateProvider.notifier).state = id; diff --git a/lib/screens/home_page/home_page.dart b/lib/screens/home_page/home_page.dart index a7338a08..76509a08 100644 --- a/lib/screens/home_page/home_page.dart +++ b/lib/screens/home_page/home_page.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/extensions/extensions.dart'; +import '../mobile/requests_page/requests_page.dart'; import 'editor_pane/editor_pane.dart'; import 'collection_pane.dart'; @@ -8,15 +10,17 @@ class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { - return const Column( - children: [ - Expanded( - child: DashboardSplitView( - sidebarWidget: CollectionPane(), - mainWidget: RequestEditorPane(), - ), - ), - ], - ); + return context.isMediumWindow + ? const RequestResponsePage() + : const Column( + children: [ + Expanded( + child: DashboardSplitView( + sidebarWidget: CollectionPane(), + mainWidget: RequestEditorPane(), + ), + ), + ], + ); } } diff --git a/lib/screens/mobile/dashboard.dart b/lib/screens/mobile/dashboard.dart index 8da58375..0cef40ab 100644 --- a/lib/screens/mobile/dashboard.dart +++ b/lib/screens/mobile/dashboard.dart @@ -67,25 +67,18 @@ class PageBranch extends ConsumerWidget { final int pageIndex; @override Widget build(BuildContext context, WidgetRef ref) { - final scaffoldKey = ref.watch(mobileScaffoldKeyStateProvider); switch (pageIndex) { case 1: - return EnvironmentPage( - scaffoldKey: scaffoldKey, - ); + return const EnvironmentPage(); case 2: - return HistoryPage( - scaffoldKey: scaffoldKey, - ); + return const HistoryPage(); case 3: return const PageBase( title: 'Settings', scaffoldBody: SettingsPage(), ); default: - return RequestResponsePage( - scaffoldKey: scaffoldKey, - ); + return const RequestResponsePage(); } } } diff --git a/lib/screens/mobile/navbar.dart b/lib/screens/mobile/navbar.dart index 5449f45f..b5402b88 100644 --- a/lib/screens/mobile/navbar.dart +++ b/lib/screens/mobile/navbar.dart @@ -52,7 +52,7 @@ class BottomNavBar extends ConsumerWidget { child: NavbarButton( railIdx: railIdx, buttonIdx: 2, - selectedIcon: Icons.history, + selectedIcon: Icons.history_rounded, icon: Icons.history_outlined, label: 'History', ), diff --git a/lib/screens/mobile/requests_page/requests_page.dart b/lib/screens/mobile/requests_page/requests_page.dart index 4164356c..fcd291b1 100644 --- a/lib/screens/mobile/requests_page/requests_page.dart +++ b/lib/screens/mobile/requests_page/requests_page.dart @@ -15,11 +15,8 @@ import 'request_response_tabs.dart'; class RequestResponsePage extends StatefulHookConsumerWidget { const RequestResponsePage({ super.key, - required this.scaffoldKey, }); - final GlobalKey scaffoldKey; - @override ConsumerState createState() => _RequestResponsePageState(); @@ -35,7 +32,7 @@ class _RequestResponsePageState extends ConsumerState final TabController requestResponseTabController = useTabController(initialLength: 2, vsync: this); return DrawerSplitView( - scaffoldKey: widget.scaffoldKey, + scaffoldKey: kHomeScaffoldKey, title: EditorTitle( title: name, onSelected: (ItemMenuOption item) { diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index da00bfd2..2ce155f2 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -149,7 +149,7 @@ class SettingsPage extends ConsumerWidget { hoverColor: kColorTransparent, title: const Text('History Retention Period'), subtitle: Text( - 'Your request history will be retained for ${settings.historyRetentionPeriod.label}'), + 'Your request history will be retained${settings.historyRetentionPeriod == HistoryRetentionPeriod.forever ? "" : " for"} ${settings.historyRetentionPeriod.label}'), trailing: Container( decoration: BoxDecoration( border: Border.all( diff --git a/lib/utils/ui_utils.dart b/lib/utils/ui_utils.dart index 3bf1fb98..764c821b 100644 --- a/lib/utils/ui_utils.dart +++ b/lib/utils/ui_utils.dart @@ -69,3 +69,14 @@ double? getJsonPreviewerMaxRootNodeWidth(double w) { } return w - 150; } + +GlobalKey getScaffoldKey(int railIdx) { + switch (railIdx) { + case 1: + return kEnvScaffoldKey; + case 2: + return kHisScaffoldKey; + default: + return kHomeScaffoldKey; + } +} diff --git a/lib/widgets/popup_menu_history.dart b/lib/widgets/popup_menu_history.dart index 3f2af6ee..72398bb3 100644 --- a/lib/widgets/popup_menu_history.dart +++ b/lib/widgets/popup_menu_history.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/consts.dart'; class HistoryRetentionPopupMenu extends StatelessWidget { @@ -15,11 +14,11 @@ class HistoryRetentionPopupMenu extends StatelessWidget { final List? items; @override Widget build(BuildContext context) { - final double boxLength = context.isCompactWindow ? 110 : 130; + const double boxLength = 120; return PopupMenuButton( tooltip: "Select retention period", surfaceTintColor: kColorTransparent, - constraints: BoxConstraints(minWidth: boxLength), + constraints: const BoxConstraints(minWidth: boxLength), itemBuilder: (BuildContext context) { return [ ...items!.map((period) { @@ -44,6 +43,7 @@ class HistoryRetentionPopupMenu extends StatelessWidget { Text( value.label, style: kTextStylePopupMenuItem, + overflow: TextOverflow.ellipsis, ), const Icon( Icons.unfold_more, diff --git a/lib/widgets/popup_menu_uri.dart b/lib/widgets/popup_menu_uri.dart index 177381b0..5a62c97e 100644 --- a/lib/widgets/popup_menu_uri.dart +++ b/lib/widgets/popup_menu_uri.dart @@ -15,7 +15,7 @@ class URIPopupMenu extends StatelessWidget { final List? items; @override Widget build(BuildContext context) { - final double boxLength = context.isCompactWindow ? 90 : 130; + final double boxLength = context.isCompactWindow ? 90 : 110; return PopupMenuButton( tooltip: "Select URI Scheme", surfaceTintColor: kColorTransparent, diff --git a/test/models/settings_model_test.dart b/test/models/settings_model_test.dart index 41fea3b3..6df01335 100644 --- a/test/models/settings_model_test.dart +++ b/test/models/settings_model_test.dart @@ -14,6 +14,7 @@ void main() { saveResponses: true, promptBeforeClosing: true, activeEnvironmentId: null, + historyRetentionPeriod: HistoryRetentionPeriod.oneWeek, ); test('Testing toJson()', () { @@ -28,7 +29,8 @@ void main() { "defaultCodeGenLang": "curl", "saveResponses": true, "promptBeforeClosing": true, - 'activeEnvironmentId': null + "activeEnvironmentId": null, + "historyRetentionPeriod": "oneWeek", }; expect(sm.toJson(), expectedResult); }); @@ -45,7 +47,8 @@ void main() { "defaultCodeGenLang": "curl", "saveResponses": true, "promptBeforeClosing": true, - 'activeEnvironmentId': null + "activeEnvironmentId": null, + "historyRetentionPeriod": "oneWeek", }; expect(SettingsModel.fromJson(input), sm); }); @@ -61,6 +64,7 @@ void main() { saveResponses: false, promptBeforeClosing: true, activeEnvironmentId: null, + historyRetentionPeriod: HistoryRetentionPeriod.oneWeek, ); expect( sm.copyWith( @@ -72,7 +76,7 @@ void main() { test('Testing toString()', () { const expectedResult = - "{isDark: false, alwaysShowCollectionPaneScrollbar: true, width: 300.0, height: 200.0, dx: 100.0, dy: 150.0, defaultUriScheme: http, defaultCodeGenLang: curl, saveResponses: true, promptBeforeClosing: true, activeEnvironmentId: null}"; + "{isDark: false, alwaysShowCollectionPaneScrollbar: true, width: 300.0, height: 200.0, dx: 100.0, dy: 150.0, defaultUriScheme: http, defaultCodeGenLang: curl, saveResponses: true, promptBeforeClosing: true, activeEnvironmentId: null, historyRetentionPeriod: oneWeek}"; expect(sm.toString(), expectedResult); }); diff --git a/test/providers/ui_providers_test.dart b/test/providers/ui_providers_test.dart index 81fb7656..9b0653d3 100644 --- a/test/providers/ui_providers_test.dart +++ b/test/providers/ui_providers_test.dart @@ -12,6 +12,7 @@ import 'package:apidash/screens/home_page/editor_pane/editor_pane.dart'; import 'package:apidash/screens/home_page/editor_pane/url_card.dart'; import 'package:apidash/screens/home_page/home_page.dart'; import 'package:apidash/screens/settings_page.dart'; +import 'package:apidash/screens/history/history_page.dart'; import 'package:apidash/services/hive_services.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:extended_text_field/extended_text_field.dart'; @@ -59,6 +60,7 @@ void main() { // Verify that the HomePage is displayed initially expect(find.byType(HomePage), findsOneWidget); expect(find.byType(EnvironmentPage), findsNothing); + expect(find.byType(HistoryPage), findsNothing); expect(find.byType(SettingsPage), findsNothing); }); @@ -79,9 +81,32 @@ void main() { // Verify that the EnvironmentPage is displayed expect(find.byType(HomePage), findsNothing); expect(find.byType(EnvironmentPage), findsOneWidget); + expect(find.byType(HistoryPage), findsNothing); expect(find.byType(SettingsPage), findsNothing); }); + testWidgets( + "Dashboard should display HistorPage when navRailIndexStateProvider is 2", + (WidgetTester tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + navRailIndexStateProvider.overrideWith((ref) => 2), + ], + child: const Portal( + child: MaterialApp( + home: Dashboard(), + ), + ), + ), + ); + + // Verify that the SettingsPage is displayed + expect(find.byType(HomePage), findsNothing); + expect(find.byType(EnvironmentPage), findsNothing); + expect(find.byType(HistoryPage), findsOneWidget); + expect(find.byType(SettingsPage), findsNothing); + }); testWidgets( "Dashboard should display SettingsPage when navRailIndexStateProvider is 3", (WidgetTester tester) async { @@ -101,6 +126,7 @@ void main() { // Verify that the SettingsPage is displayed expect(find.byType(HomePage), findsNothing); expect(find.byType(EnvironmentPage), findsNothing); + expect(find.byType(HistoryPage), findsNothing); expect(find.byType(SettingsPage), findsOneWidget); }); @@ -159,7 +185,7 @@ void main() { // Verify that the navRailIndexStateProvider still has the updated value final dashboard = tester.element(find.byType(Dashboard)); final container = ProviderScope.containerOf(dashboard); - expect(container.read(navRailIndexStateProvider), 2); + expect(container.read(navRailIndexStateProvider), 3); // Verify that the SettingsPage is still displayed after the rebuild expect(find.byType(SettingsPage), findsOneWidget); @@ -193,10 +219,19 @@ void main() { // Verify that the selected icon is the filled version (selectedIcon) expect(find.byIcon(Icons.computer_rounded), findsOneWidget); - // Go to SettingsPage + // Go to HistoryPage container.read(navRailIndexStateProvider.notifier).state = 2; await tester.pump(); + // Verify that the HistoryPage is displayed + expect(find.byType(HistoryPage), findsOneWidget); + // Verify that the selected icon is the filled version (selectedIcon) + expect(find.byIcon(Icons.history_rounded), findsOneWidget); + + // Go to SettingsPage + container.read(navRailIndexStateProvider.notifier).state = 3; + await tester.pump(); + // Verify that the SettingsPage is displayed expect(find.byType(SettingsPage), findsOneWidget); // Verify that the selected icon is the filled version (selectedIcon) From 1d063c19114e8613133a659383f5c9ca73f555bb Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Thu, 25 Jul 2024 17:25:54 +0530 Subject: [PATCH 12/71] fix: scaffold drawer issue --- lib/screens/common_widgets/button_navbar.dart | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/screens/common_widgets/button_navbar.dart b/lib/screens/common_widgets/button_navbar.dart index bce7687f..b9ea8e61 100644 --- a/lib/screens/common_widgets/button_navbar.dart +++ b/lib/screens/common_widgets/button_navbar.dart @@ -26,7 +26,6 @@ class NavbarButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final mobileScaffoldKey = ref.watch(mobileScaffoldKeyStateProvider); final mobileScaffoldKeyNotifier = ref.watch(mobileScaffoldKeyStateProvider.notifier); final bool isSelected = railIdx == buttonIdx; @@ -38,10 +37,7 @@ class NavbarButton extends ConsumerWidget { final scaffoldKey = getScaffoldKey(buttonIdx!); ref.watch(navRailIndexStateProvider.notifier).state = buttonIdx!; mobileScaffoldKeyNotifier.state = scaffoldKey; - if ((railIdx > 2 && buttonIdx! <= 2) || - !(mobileScaffoldKey.currentState?.isDrawerOpen ?? true)) { - ref.read(leftDrawerStateProvider.notifier).state = false; - } + ref.read(leftDrawerStateProvider.notifier).state = false; } onTap?.call(); }; From ee989d126e924a7ac31f3ef3945124b38d79f979 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Thu, 25 Jul 2024 17:25:54 +0530 Subject: [PATCH 13/71] fix: scaffold drawer issue --- lib/screens/common_widgets/button_navbar.dart | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/screens/common_widgets/button_navbar.dart b/lib/screens/common_widgets/button_navbar.dart index bce7687f..b9ea8e61 100644 --- a/lib/screens/common_widgets/button_navbar.dart +++ b/lib/screens/common_widgets/button_navbar.dart @@ -26,7 +26,6 @@ class NavbarButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final mobileScaffoldKey = ref.watch(mobileScaffoldKeyStateProvider); final mobileScaffoldKeyNotifier = ref.watch(mobileScaffoldKeyStateProvider.notifier); final bool isSelected = railIdx == buttonIdx; @@ -38,10 +37,7 @@ class NavbarButton extends ConsumerWidget { final scaffoldKey = getScaffoldKey(buttonIdx!); ref.watch(navRailIndexStateProvider.notifier).state = buttonIdx!; mobileScaffoldKeyNotifier.state = scaffoldKey; - if ((railIdx > 2 && buttonIdx! <= 2) || - !(mobileScaffoldKey.currentState?.isDrawerOpen ?? true)) { - ref.read(leftDrawerStateProvider.notifier).state = false; - } + ref.read(leftDrawerStateProvider.notifier).state = false; } onTap?.call(); }; From 289c7c73eacbf4d59f6f57026c4e223573c3c329 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Fri, 26 Jul 2024 22:58:49 +0530 Subject: [PATCH 14/71] fix: ui changes --- lib/screens/history/history_details.dart | 18 ++----- .../history_widgets/his_action_buttons.dart | 1 + .../history/history_widgets/his_url_card.dart | 21 +++++--- ...t_response_tabs.dart => request_tabs.dart} | 13 +++-- .../mobile/requests_page/requests_page.dart | 50 +++++-------------- lib/screens/settings_page.dart | 14 +++++- lib/widgets/dialog_rename.dart | 28 ++++++++--- 7 files changed, 72 insertions(+), 73 deletions(-) rename lib/screens/mobile/requests_page/{request_response_tabs.dart => request_tabs.dart} (72%) diff --git a/lib/screens/history/history_details.dart b/lib/screens/history/history_details.dart index ebdf81c1..6e12b422 100644 --- a/lib/screens/history/history_details.dart +++ b/lib/screens/history/history_details.dart @@ -20,10 +20,7 @@ class _HistoryDetailsState extends ConsumerState Widget build(BuildContext context) { final selectedHistoryRequest = ref.watch(selectedHistoryRequestModelProvider); - final metaData = selectedHistoryRequest?.metaData; - final codePaneVisible = ref.watch(historyCodePaneVisibleStateProvider); - final TabController controller = useTabController(initialLength: 3, vsync: this); @@ -37,8 +34,7 @@ class _HistoryDetailsState extends ConsumerState Padding( padding: kPh4, child: HistoryURLCard( - method: metaData!.method, - url: metaData.url, + historyRequestModel: selectedHistoryRequest, )), kVSpacer10, if (isCompact) ...[ @@ -72,16 +68,8 @@ class _HistoryDetailsState extends ConsumerState padding: kPh4, child: RequestDetailsCard( child: EqualSplitView( - leftWidget: Column( - children: [ - Expanded( - child: HistoryRequestPane( - isCompact: isCompact, - ), - ), - const HistoryPageBottombar(), - ], - ), + leftWidget: + HistoryRequestPane(isCompact: isCompact), rightWidget: codePaneVisible ? const CodePane(isHistoryRequest: true) : const HistoryResponsePane(), diff --git a/lib/screens/history/history_widgets/his_action_buttons.dart b/lib/screens/history/history_widgets/his_action_buttons.dart index 436c9557..d4e79959 100644 --- a/lib/screens/history/history_widgets/his_action_buttons.dart +++ b/lib/screens/history/history_widgets/his_action_buttons.dart @@ -17,6 +17,7 @@ class HistoryActionButtons extends ConsumerWidget { element.id == historyRequestModel?.metaData.requestId) ?? false; final requestId = historyRequestModel?.metaData.requestId; + return FilledButtonGroup(buttons: [ ButtonData( icon: Icons.copy_rounded, diff --git a/lib/screens/history/history_widgets/his_url_card.dart b/lib/screens/history/history_widgets/his_url_card.dart index cb8ec0da..f7fd89b3 100644 --- a/lib/screens/history/history_widgets/his_url_card.dart +++ b/lib/screens/history/history_widgets/his_url_card.dart @@ -1,23 +1,27 @@ import 'package:flutter/material.dart'; import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/models/models.dart'; import 'package:apidash/utils/utils.dart'; import 'package:apidash/consts.dart'; +import 'his_action_buttons.dart'; class HistoryURLCard extends StatelessWidget { const HistoryURLCard({ super.key, - required this.method, - required this.url, + required this.historyRequestModel, }); - final HTTPVerb method; - final String url; + final HistoryRequestModel? historyRequestModel; @override Widget build(BuildContext context) { + final method = historyRequestModel?.metaData.method; + final url = historyRequestModel?.metaData.url; final fontSize = Theme.of(context).textTheme.titleMedium?.fontSize; + return LayoutBuilder(builder: (context, constraints) { final isCompact = constraints.maxWidth <= kMinWindowSize.width; + final isExpanded = constraints.maxWidth >= kMediumWindowWidth; return Card( color: kColorTransparent, surfaceTintColor: kColorTransparent, @@ -37,7 +41,7 @@ class HistoryURLCard extends StatelessWidget { children: [ isCompact ? const SizedBox.shrink() : kHSpacer10, Text( - method.name.toUpperCase(), + method!.name.toUpperCase(), style: kCodeStyle.copyWith( fontSize: fontSize, fontWeight: FontWeight.bold, @@ -55,7 +59,12 @@ class HistoryURLCard extends StatelessWidget { fontSize: fontSize, ), ), - ) + ), + isExpanded + ? HistoryActionButtons( + historyRequestModel: historyRequestModel, + ) + : const SizedBox.shrink() ], ), ), diff --git a/lib/screens/mobile/requests_page/request_response_tabs.dart b/lib/screens/mobile/requests_page/request_tabs.dart similarity index 72% rename from lib/screens/mobile/requests_page/request_response_tabs.dart rename to lib/screens/mobile/requests_page/request_tabs.dart index 34a0ed90..7d542095 100644 --- a/lib/screens/mobile/requests_page/request_response_tabs.dart +++ b/lib/screens/mobile/requests_page/request_tabs.dart @@ -1,3 +1,4 @@ +import 'package:apidash/screens/common_widgets/common_widgets.dart'; import 'package:flutter/material.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; @@ -5,8 +6,8 @@ import '../../home_page/editor_pane/details_card/response_pane.dart'; import '../../home_page/editor_pane/editor_request.dart'; import '../../home_page/editor_pane/url_card.dart'; -class RequestResponseTabs extends StatelessWidget { - const RequestResponseTabs({super.key, required this.controller}); +class RequestTabs extends StatelessWidget { + const RequestTabs({super.key, required this.controller}); final TabController controller; @override @@ -24,16 +25,17 @@ class RequestResponseTabs extends StatelessWidget { tabs: const [ Tab(text: kLabelRequest), Tab(text: kLabelResponse), + Tab(text: kLabelCode), ], ), - Expanded(child: RequestResponseTabviews(controller: controller)) + Expanded(child: RequestTabviews(controller: controller)) ], ); } } -class RequestResponseTabviews extends StatelessWidget { - const RequestResponseTabviews({super.key, required this.controller}); +class RequestTabviews extends StatelessWidget { + const RequestTabviews({super.key, required this.controller}); final TabController controller; @override @@ -46,6 +48,7 @@ class RequestResponseTabviews extends StatelessWidget { padding: kPt8, child: ResponsePane(), ), + CodePane(), ], ); } diff --git a/lib/screens/mobile/requests_page/requests_page.dart b/lib/screens/mobile/requests_page/requests_page.dart index fcd291b1..f9c986d9 100644 --- a/lib/screens/mobile/requests_page/requests_page.dart +++ b/lib/screens/mobile/requests_page/requests_page.dart @@ -9,8 +9,7 @@ import '../../home_page/collection_pane.dart'; import '../../home_page/editor_pane/url_card.dart'; import '../../home_page/editor_pane/editor_default.dart'; import '../../common_widgets/common_widgets.dart'; -import '../widgets/page_base.dart'; -import 'request_response_tabs.dart'; +import 'request_tabs.dart'; class RequestResponsePage extends StatefulHookConsumerWidget { const RequestResponsePage({ @@ -29,8 +28,8 @@ class _RequestResponsePageState extends ConsumerState final id = ref.watch(selectedIdStateProvider); final name = getRequestTitleFromUrl( ref.watch(selectedRequestModelProvider.select((value) => value?.name))); - final TabController requestResponseTabController = - useTabController(initialLength: 2, vsync: this); + final TabController requestTabController = + useTabController(initialLength: 3, vsync: this); return DrawerSplitView( scaffoldKey: kHomeScaffoldKey, title: EditorTitle( @@ -55,11 +54,11 @@ class _RequestResponsePageState extends ConsumerState actions: const [Padding(padding: kPh8, child: EnvironmentDropdown())], mainContent: id == null ? const RequestEditorDefault() - : RequestResponseTabs( - controller: requestResponseTabController, + : RequestTabs( + controller: requestTabController, ), bottomNavigationBar: RequestResponsePageBottombar( - requestResponseTabController: requestResponseTabController, + requestTabController: requestTabController, ), onDrawerChanged: (value) => ref.read(leftDrawerStateProvider.notifier).state = value, @@ -67,16 +66,15 @@ class _RequestResponsePageState extends ConsumerState } } -class RequestResponsePageBottombar extends ConsumerWidget { +class RequestResponsePageBottombar extends StatelessWidget { const RequestResponsePageBottombar({ super.key, - required this.requestResponseTabController, + required this.requestTabController, }); - final TabController requestResponseTabController; + final TabController requestTabController; @override - Widget build(BuildContext context, WidgetRef ref) { - final selecetdId = ref.watch(selectedIdStateProvider); + Widget build(BuildContext context) { return Padding( padding: MediaQuery.of(context).viewInsets, child: Container( @@ -97,36 +95,14 @@ class RequestResponsePageBottombar extends ConsumerWidget { ), ), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - IconButton.filledTonal( - style: IconButton.styleFrom( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - ), - ), - onPressed: selecetdId == null - ? null - : () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const PageBase( - title: 'View Code', - scaffoldBody: CodePane(), - addBottomPadding: false, - ), - fullscreenDialog: true, - ), - ); - }, - icon: const Icon(Icons.code_rounded), - ), + const Spacer(), SizedBox( height: 36, child: SendRequestButton( onTap: () { - if (requestResponseTabController.index != 1) { - requestResponseTabController.animateTo(1); + if (requestTabController.index != 1) { + requestTabController.animateTo(1); } }, ), diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index 2ce155f2..8e9c8486 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -184,9 +184,19 @@ class SettingsPage extends ConsumerWidget { : () => showDialog( context: context, builder: (BuildContext context) => AlertDialog( + icon: const Icon(Icons.manage_history_rounded), + iconColor: settings.isDark + ? kColorDarkDanger + : kColorLightDanger, title: const Text('Clear Data'), - content: const Text( - 'This action will clear all the requests data from the disk and is irreversible. Do you want to proceed?'), + titleTextStyle: + Theme.of(context).textTheme.titleLarge, + content: ConstrainedBox( + constraints: + const BoxConstraints(maxWidth: 300), + child: const Text( + 'This action will clear all the requests data from the disk and is irreversible. Do you want to proceed?'), + ), actions: [ TextButton( onPressed: () => diff --git a/lib/widgets/dialog_rename.dart b/lib/widgets/dialog_rename.dart index 900ba758..4b78e987 100644 --- a/lib/widgets/dialog_rename.dart +++ b/lib/widgets/dialog_rename.dart @@ -1,3 +1,4 @@ +import 'package:apidash/consts.dart'; import 'package:flutter/material.dart'; showRenameDialog( @@ -13,19 +14,30 @@ showRenameDialog( controller.selection = TextSelection(baseOffset: 0, extentOffset: controller.text.length); return AlertDialog( + icon: const Icon(Icons.edit_rounded), + iconColor: Theme.of(context).colorScheme.primary, title: Text(dialogTitle), - content: TextField( - autofocus: true, - controller: controller, - decoration: const InputDecoration(hintText: "Enter new name"), + titleTextStyle: Theme.of(context).textTheme.titleLarge, + content: Container( + padding: kPt20, + width: 300, + child: TextField( + autofocus: true, + controller: controller, + decoration: const InputDecoration( + hintText: "Enter new name", + border: OutlineInputBorder( + borderRadius: kBorderRadius12, + )), + ), ), actions: [ - OutlinedButton( + TextButton( onPressed: () { Navigator.pop(context); }, - child: const Text('CANCEL')), - FilledButton( + child: const Text('Cancel')), + TextButton( onPressed: () { final val = controller.text.trim(); onRename(val); @@ -34,7 +46,7 @@ showRenameDialog( controller.dispose(); }); }, - child: const Text('OK')), + child: const Text('Ok')), ], ); }); From cfcf44866469978e21d9f35fb6c208fd7529b2a7 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Mon, 29 Jul 2024 00:23:58 +0530 Subject: [PATCH 15/71] wip: convert_utils_tests --- lib/extensions/string_extensions.dart | 6 ++ lib/utils/convert_utils.dart | 15 +--- pubspec.lock | 8 ++ pubspec.yaml | 1 + test/extensions/widget_tester_extensions.dart | 11 +-- test/test_consts.dart | 10 +++ test/utils/convert_utils_test.dart | 78 ++++++++++++------- 7 files changed, 80 insertions(+), 49 deletions(-) diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 824c47d5..37632a2f 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -1,5 +1,11 @@ extension StringExtension on String { String capitalize() { + if (isEmpty) { + return this; + } + if (length == 1) { + return toUpperCase(); + } return "${this[0].toUpperCase()}${substring(1).toLowerCase()}"; } diff --git a/lib/utils/convert_utils.dart b/lib/utils/convert_utils.dart index c772134e..d0b2ac26 100644 --- a/lib/utils/convert_utils.dart +++ b/lib/utils/convert_utils.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; import 'dart:convert'; +import 'package:apidash/extensions/extensions.dart'; import 'package:collection/collection.dart'; import 'package:intl/intl.dart'; import '../models/models.dart'; @@ -49,21 +50,9 @@ String audioPosition(Duration? duration) { return "$min:$secondsPadding$secs"; } -String capitalizeFirstLetter(String? text) { - if (text == null || text == "") { - return ""; - } else if (text.length == 1) { - return text.toUpperCase(); - } else { - var first = text[0]; - var rest = text.substring(1); - return first.toUpperCase() + rest; - } -} - String formatHeaderCase(String text) { var sp = text.split("-"); - sp = sp.map((e) => capitalizeFirstLetter(e)).toList(); + sp = sp.map((e) => e.capitalize()).toList(); return sp.join("-"); } diff --git a/pubspec.lock b/pubspec.lock index b04f6a8e..12247062 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1383,6 +1383,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.0" + test_cov_console: + dependency: "direct dev" + description: + name: test_cov_console + sha256: "73519e8be3689d73f5cffb652c12c310acacf48379396d834da937094836e65e" + url: "https://pub.dev" + source: hosted + version: "0.2.2" textwrap: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 515fc0c9..93053652 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -89,6 +89,7 @@ dev_dependencies: freezed: ^2.5.2 json_serializable: ^6.7.1 spot: ^0.13.0 + test_cov_console: ^0.2.2 flutter: uses-material-design: true diff --git a/test/extensions/widget_tester_extensions.dart b/test/extensions/widget_tester_extensions.dart index e9c5adbd..d17c26e5 100644 --- a/test/extensions/widget_tester_extensions.dart +++ b/test/extensions/widget_tester_extensions.dart @@ -1,15 +1,6 @@ import 'dart:ui'; import 'package:flutter_test/flutter_test.dart'; - -class ScreenSize { - const ScreenSize(this.name, this.width, this.height, this.pixelDensity); - final String name; - final double width, height, pixelDensity; -} - -const compactWidthDevice = ScreenSize('compact__width_device', 500, 600, 1); -const mediumWidthDevice = ScreenSize('medium__width_device', 800, 800, 1); -const largeWidthDevice = ScreenSize('large_width_device', 1300, 800, 1); +import '../test_consts.dart'; extension ScreenSizeManager on WidgetTester { Future setScreenSize(ScreenSize screenSize) async { diff --git a/test/test_consts.dart b/test/test_consts.dart index 6ef47887..cbd951d0 100644 --- a/test/test_consts.dart +++ b/test/test_consts.dart @@ -1,6 +1,16 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +class ScreenSize { + const ScreenSize(this.name, this.width, this.height, this.pixelDensity); + final String name; + final double width, height, pixelDensity; +} + +const compactWidthDevice = ScreenSize('compact__width_device', 500, 600, 1); +const mediumWidthDevice = ScreenSize('medium__width_device', 800, 800, 1); +const largeWidthDevice = ScreenSize('large_width_device', 1300, 800, 1); + final kThemeDataDark = ThemeData( useMaterial3: true, colorSchemeSeed: Colors.blue, diff --git a/test/utils/convert_utils_test.dart b/test/utils/convert_utils_test.dart index e996c319..ab853bfe 100644 --- a/test/utils/convert_utils_test.dart +++ b/test/utils/convert_utils_test.dart @@ -5,6 +5,58 @@ import 'package:apidash/models/name_value_model.dart'; import 'package:apidash/models/form_data_model.dart'; void main() { + group("Testing humanizeDate function", () { + test('Testing using date1', () { + DateTime date1 = DateTime(2024, 12, 31); + String date1Expected = "December 31, 2024"; + expect(humanizeDate(date1), date1Expected); + }); + + test('Testing using date2', () { + DateTime date2 = DateTime(2024, 1, 1); + String date2Expected = "January 1, 2024"; + expect(humanizeDate(date2), date2Expected); + }); + + test('Testing using date3', () { + DateTime date3 = DateTime(2024, 6, 15); + String date3Expected = "June 15, 2024"; + expect(humanizeDate(date3), date3Expected); + }); + + test('Testing using date4', () { + DateTime date4 = DateTime(2024, 9, 30); + String date4Expected = "September 30, 2024"; + expect(humanizeDate(date4), date4Expected); + }); + }); + + group("Testing humanizeTime function", () { + test('Testing using time1', () { + DateTime time1 = DateTime(2024, 12, 31, 23, 59, 59); + String time1Expected = "11:59:59 PM"; + expect(humanizeTime(time1), time1Expected); + }); + + test('Testing using time2', () { + DateTime time2 = DateTime(2024, 1, 1, 0, 0, 0); + String time2Expected = "12:00:00 AM"; + expect(humanizeTime(time2), time2Expected); + }); + + test('Testing using time3', () { + DateTime time3 = DateTime(2024, 6, 15, 12, 0, 0); + String time3Expected = "12:00:00 PM"; + expect(humanizeTime(time3), time3Expected); + }); + + test('Testing using time4', () { + DateTime time4 = DateTime(2024, 9, 30, 15, 30, 45); + String time4Expected = "03:30:45 PM"; + expect(humanizeTime(time4), time4Expected); + }); + }); + group("Testing humanizeDuration function", () { test('Testing using dur1', () { Duration dur1 = const Duration(minutes: 1, seconds: 3); @@ -31,32 +83,6 @@ void main() { }); }); - group("Testing capitalizeFirstLetter function", () { - test('Testing using text1', () { - String text1 = ""; - String text1Expected = ""; - expect(capitalizeFirstLetter(text1), text1Expected); - }); - - test('Testing using text2', () { - String text2 = "a"; - String text2Expected = "A"; - expect(capitalizeFirstLetter(text2), text2Expected); - }); - - test('Testing using text3', () { - String text3 = "world"; - String text3Expected = "World"; - expect(capitalizeFirstLetter(text3), text3Expected); - }); - - test('Testing using text4', () { - String text4 = "worldly affairs"; - String text4Expected = "Worldly affairs"; - expect(capitalizeFirstLetter(text4), text4Expected); - }); - }); - group("Testing formatHeaderCase function", () { test('Testing using headerText1', () { String headerText1 = "content-type"; From ce70a8d1c4703f3ae73cd80a18bf590a71122ee9 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Mon, 29 Jul 2024 16:18:10 +0530 Subject: [PATCH 16/71] wip: envvar_utils_test --- lib/consts.dart | 2 +- lib/models/environment_model.dart | 14 ++ lib/utils/envvar_utils.dart | 6 +- test/utils/envvar_utils_test.dart | 327 ++++++++++++++++++++++++++++++ 4 files changed, 345 insertions(+), 4 deletions(-) create mode 100644 test/utils/envvar_utils_test.dart diff --git a/lib/consts.dart b/lib/consts.dart index 73de67f6..9ef5eed7 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -369,7 +369,7 @@ enum FormDataType { text, file } enum EnvironmentVariableType { variable, secret } -final kEnvVarRegEx = RegExp(r'{{(.*?)}}'); +final kEnvVarRegEx = RegExp(r'{{([^{}]*)}}'); const kSupportedUriSchemes = ["https", "http"]; const kDefaultUriScheme = "https"; diff --git a/lib/models/environment_model.dart b/lib/models/environment_model.dart index 0454e98f..2d0014fa 100644 --- a/lib/models/environment_model.dart +++ b/lib/models/environment_model.dart @@ -65,4 +65,18 @@ class EnvironmentVariableSuggestion { isUnknown: isUnknown ?? this.isUnknown, ); } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is EnvironmentVariableSuggestion && + other.environmentId == environmentId && + other.variable == variable && + other.isUnknown == isUnknown; + } + + @override + int get hashCode => + environmentId.hashCode ^ variable.hashCode ^ isUnknown.hashCode; } diff --git a/lib/utils/envvar_utils.dart b/lib/utils/envvar_utils.dart index aec71567..01e6440f 100644 --- a/lib/utils/envvar_utils.dart +++ b/lib/utils/envvar_utils.dart @@ -91,7 +91,7 @@ HttpRequestModel substituteHttpRequestModel( } List? getEnvironmentTriggerSuggestions( - String? query, + String query, Map> envMap, String? activeEnvironmentId) { final suggestions = []; @@ -99,7 +99,7 @@ List? getEnvironmentTriggerSuggestions( if (activeEnvironmentId != null && envMap[activeEnvironmentId] != null) { for (final variable in envMap[activeEnvironmentId]!) { - if ((query!.isEmpty || variable.key.contains(query)) && + if ((query.isEmpty || variable.key.contains(query)) && !addedVariableKeys.contains(variable.key)) { suggestions.add(EnvironmentVariableSuggestion( environmentId: activeEnvironmentId, variable: variable)); @@ -109,7 +109,7 @@ List? getEnvironmentTriggerSuggestions( } envMap[kGlobalEnvironmentId]?.forEach((variable) { - if ((query!.isEmpty || variable.key.contains(query)) && + if ((query.isEmpty || variable.key.contains(query)) && !addedVariableKeys.contains(variable.key)) { suggestions.add(EnvironmentVariableSuggestion( environmentId: kGlobalEnvironmentId, variable: variable)); diff --git a/test/utils/envvar_utils_test.dart b/test/utils/envvar_utils_test.dart new file mode 100644 index 00000000..59bb4740 --- /dev/null +++ b/test/utils/envvar_utils_test.dart @@ -0,0 +1,327 @@ +import 'package:apidash/models/models.dart'; +import 'package:apidash/utils/envvar_utils.dart'; +import 'package:apidash/consts.dart'; +import 'package:test/test.dart'; + +const envVars = [ + EnvironmentVariableModel( + key: "var1", + value: "var1-value", + type: EnvironmentVariableType.variable, + ), + EnvironmentVariableModel( + key: "var2", + value: "var2-value", + type: EnvironmentVariableType.variable, + ), +]; +const envSecrets = [ + EnvironmentVariableModel( + key: "secret1", + value: "secret1-value", + type: EnvironmentVariableType.secret, + ), + EnvironmentVariableModel( + key: "secret2", + value: "secret2-value", + type: EnvironmentVariableType.secret, + ), +]; +const emptyEnvVar = EnvironmentVariableModel( + key: "", + value: "", + type: EnvironmentVariableType.variable, +); +const emptyEnvSecret = EnvironmentVariableModel( + key: "", + value: "", + type: EnvironmentVariableType.secret, +); +const environmentModel = EnvironmentModel( + id: "id", + name: "Testing", + values: [...envVars, emptyEnvVar, ...envSecrets, emptyEnvSecret]); +const emptyEnvironmentModel = + EnvironmentModel(id: "id", name: "Testing", values: []); +const globalVars = [ + EnvironmentVariableModel(key: "url", value: "api.foss42.com"), + EnvironmentVariableModel(key: "num", value: "5670000"), + EnvironmentVariableModel(key: "token", value: "token"), +]; +const activeEnvVars = [ + EnvironmentVariableModel(key: "url", value: "api.apidash.dev"), + EnvironmentVariableModel(key: "num", value: "8940000"), +]; + +void main() { + group("Testing getEnvironmentTitle function", () { + String titleUntitled = "untitled"; + test("Testing getEnvironmentTitle with null", () { + String? envName1; + expect(getEnvironmentTitle(envName1), titleUntitled); + }); + + test("Testing getEnvironmentTitle with empty string", () { + String envName2 = ""; + expect(getEnvironmentTitle(envName2), titleUntitled); + }); + + test("Testing getEnvironmentTitle with trimmable string", () { + String envName3 = " "; + expect(getEnvironmentTitle(envName3), titleUntitled); + }); + + test("Testing getEnvironmentTitle with non-empty string", () { + String envName4 = "test"; + expect(getEnvironmentTitle(envName4), "test"); + }); + }); + + group("Testing getEnvironmentVariables function", () { + test("Testing getEnvironmentVariables with null", () { + EnvironmentModel? environment; + expect(getEnvironmentVariables(environment), []); + }); + + test("Testing getEnvironmentVariables with empty", () { + expect(getEnvironmentVariables(emptyEnvironmentModel), []); + }); + + test("Testing getEnvironmentVariables with non-empty environmentModel", () { + expect( + getEnvironmentVariables(environmentModel), [...envVars, emptyEnvVar]); + }); + + test( + "Testing getEnvironmentVariables with non-empty environmentModel and removeEmptyModels", + () { + expect(getEnvironmentVariables(environmentModel, removeEmptyModels: true), + envVars); + }); + }); + + group("Testing getEnvironmentSecrets function", () { + test("Testing getEnvironmentSecrets with null", () { + EnvironmentModel? environment; + expect(getEnvironmentSecrets(environment), []); + }); + + test("Testing getEnvironmentSecrets with empty", () { + expect(getEnvironmentSecrets(emptyEnvironmentModel), []); + }); + + test("Testing getEnvironmentSecrets with non-empty environmentModel", () { + expect(getEnvironmentSecrets(environmentModel), + [...envSecrets, emptyEnvSecret]); + }); + + test( + "Testing getEnvironmentSecrets with non-empty environmentModel and removeEmptyModels", + () { + expect(getEnvironmentSecrets(environmentModel, removeEmptyModels: true), + envSecrets); + }); + }); + + group("Testing substituteVariables function", () { + test("Testing substituteVariables with null", () { + String? input; + Map> envMap = {}; + String? activeEnvironmentId; + expect(substituteVariables(input, envMap, activeEnvironmentId), null); + }); + + test("Testing substituteVariables with empty input", () { + String input = ""; + Map> envMap = {}; + String? activeEnvironmentId; + expect(substituteVariables(input, envMap, activeEnvironmentId), ""); + }); + + test("Testing substituteVariables with empty envMap", () { + String input = "{{url}}/humanize/social?num={{num}}"; + Map> envMap = {}; + String? activeEnvironmentId; + String expected = "/humanize/social?num="; + expect(substituteVariables(input, envMap, activeEnvironmentId), expected); + }); + + test("Testing substituteVariables with empty activeEnvironmentId", () { + String input = "{{url}}/humanize/social?num={{num}}"; + Map> envMap = { + kGlobalEnvironmentId: globalVars, + }; + String expected = "api.foss42.com/humanize/social?num=5670000"; + expect(substituteVariables(input, envMap, null), expected); + }); + + test("Testing substituteVariables with non-empty activeEnvironmentId", () { + String input = "{{url}}/humanize/social?num={{num}}"; + Map> envMap = { + kGlobalEnvironmentId: globalVars, + "activeEnvId": activeEnvVars, + }; + String? activeEnvId = "activeEnvId"; + String expected = "api.apidash.dev/humanize/social?num=8940000"; + expect(substituteVariables(input, envMap, activeEnvId), expected); + }); + + test("Testing substituteVariables with incorrect paranthesis", () { + String input = "{{url}}}/humanize/social?num={{num}}"; + Map> envMap = { + kGlobalEnvironmentId: globalVars, + "activeEnvId": activeEnvVars, + }; + String? activeEnvId = "activeEnvId"; + String expected = "api.apidash.dev}/humanize/social?num=8940000"; + expect(substituteVariables(input, envMap, activeEnvId), expected); + }); + + test("Testing substituteVariables function with unavailable variables", () { + String input = "{{url1}}/humanize/social?num={{num}}"; + Map> envMap = { + kGlobalEnvironmentId: globalVars, + "activeEnvId": activeEnvVars, + }; + String? activeEnvironmentId = "activeEnvId"; + String expected = "/humanize/social?num=8940000"; + expect(substituteVariables(input, envMap, activeEnvironmentId), expected); + }); + }); + + group("Testing substituteHttpRequestModel function", () { + test("Testing substituteHttpRequestModel with empty", () { + const httpRequestModel = HttpRequestModel(); + Map> envMap = {}; + String? activeEnvironmentId; + const expected = HttpRequestModel(); + expect( + substituteHttpRequestModel( + httpRequestModel, envMap, activeEnvironmentId), + expected); + }); + + test("Testing substituteHttpRequestModel with non-empty", () { + const httpRequestModel = HttpRequestModel( + url: "{{url}}/humanize/social", + headers: [ + NameValueModel(name: "Authorization", value: "Bearer {{token}}"), + ], + params: [ + NameValueModel(name: "num", value: "{{num}}"), + ], + ); + Map> envMap = { + kGlobalEnvironmentId: globalVars, + "activeEnvId": activeEnvVars, + }; + String? activeEnvironmentId = "activeEnvId"; + const expected = HttpRequestModel( + url: "api.apidash.dev/humanize/social", + headers: [ + NameValueModel(name: "Authorization", value: "Bearer token"), + ], + params: [ + NameValueModel(name: "num", value: "8940000"), + ], + ); + expect( + substituteHttpRequestModel( + httpRequestModel, envMap, activeEnvironmentId), + expected); + }); + + test("Testing substituteHttpRequestModel with unavailable variables", () { + const httpRequestModel = HttpRequestModel( + url: "{{url1}}/humanize/social", + headers: [ + NameValueModel(name: "Authorization", value: "Bearer {{token1}}"), + ], + params: [ + NameValueModel(name: "num", value: "{{num}}"), + ], + ); + Map> envMap = { + kGlobalEnvironmentId: globalVars, + "activeEnvId": activeEnvVars, + }; + String? activeEnvironmentId = "activeEnvId"; + const expected = HttpRequestModel( + url: "/humanize/social", + headers: [ + NameValueModel(name: "Authorization", value: "Bearer "), + ], + params: [ + NameValueModel(name: "num", value: "8940000"), + ], + ); + expect( + substituteHttpRequestModel( + httpRequestModel, envMap, activeEnvironmentId), + expected); + }); + }); + + group("Testing getEnvironmentTriggerSuggestions function", () { + test("Testing getEnvironmentTriggerSuggestion with empty envMap", () { + const query = "u"; + Map> envMap = {}; + const activeEnvironmentId = ""; + expect( + getEnvironmentTriggerSuggestions(query, envMap, activeEnvironmentId), + []); + }); + + test("Testing getEnvironmentTriggerSuggestion with empty query", () { + const query = ""; + Map> envMap = { + kGlobalEnvironmentId: globalVars, + "activeEnvId": activeEnvVars, + }; + const activeEnvironmentId = "activeEnvId"; + expect( + getEnvironmentTriggerSuggestions(query, envMap, activeEnvironmentId), + const [ + EnvironmentVariableSuggestion( + environmentId: activeEnvironmentId, + variable: EnvironmentVariableModel( + key: "url", value: "api.apidash.dev")), + EnvironmentVariableSuggestion( + environmentId: activeEnvironmentId, + variable: + EnvironmentVariableModel(key: "num", value: "8940000")), + EnvironmentVariableSuggestion( + environmentId: kGlobalEnvironmentId, + variable: + EnvironmentVariableModel(key: "token", value: "token")), + ]); + }); + }); + + group("Testing getVariableStatus function", () { + test("Testing getVariableStatus with available variable", () { + const query = "num"; + Map> envMap = { + kGlobalEnvironmentId: globalVars, + }; + const expected = EnvironmentVariableSuggestion( + environmentId: kGlobalEnvironmentId, + variable: EnvironmentVariableModel(key: "num", value: "5670000")); + expect(getVariableStatus(query, envMap, null), expected); + }); + + test("Testing getVariableStatus with unavailable variable", () { + const query = "path"; + Map> envMap = { + kGlobalEnvironmentId: globalVars, + "activeEnvId": activeEnvVars, + }; + const activeEnvironmentId = "activeEnvId"; + const expected = EnvironmentVariableSuggestion( + isUnknown: true, + environmentId: "unknown", + variable: EnvironmentVariableModel(key: query, value: "unknown")); + expect(getVariableStatus(query, envMap, activeEnvironmentId), expected); + }); + }); +} From 593d83c4ec1315255e1343a543b2f203c68a8e2f Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Mon, 29 Jul 2024 22:56:15 +0530 Subject: [PATCH 17/71] test: environment models --- test/models/environment_models.dart | 145 +++++++++++++++++++ test/models/environment_models_test.dart | 173 +++++++++++++++++++++++ 2 files changed, 318 insertions(+) create mode 100644 test/models/environment_models.dart create mode 100644 test/models/environment_models_test.dart diff --git a/test/models/environment_models.dart b/test/models/environment_models.dart new file mode 100644 index 00000000..66d61eb3 --- /dev/null +++ b/test/models/environment_models.dart @@ -0,0 +1,145 @@ +import 'package:apidash/consts.dart'; +import 'package:apidash/models/models.dart' + show + EnvironmentModel, + EnvironmentVariableModel, + EnvironmentVariableSuggestion; + +/// Global environment model +const globalEnvironment = EnvironmentModel( + id: kGlobalEnvironmentId, + name: 'Global', + values: [ + EnvironmentVariableModel( + key: 'key1', + value: 'value1', + enabled: true, + ), + EnvironmentVariableModel( + key: 'key2', + value: 'value2', + enabled: false, + ), + ], +); + +/// Basic Environment model with 2 variables +const environmentModel1 = EnvironmentModel( + id: 'environmentId', + name: 'Development', + values: [ + EnvironmentVariableModel( + key: 'key1', + value: 'value1', + type: EnvironmentVariableType.variable, + enabled: true, + ), + EnvironmentVariableModel( + key: 'key2', + value: 'value2', + type: EnvironmentVariableType.variable, + enabled: false, + ), + ], +); + +/// Basic Environment model with 2 secrets +const environmentModel2 = EnvironmentModel( + id: 'environmentId', + name: 'Development', + values: [ + EnvironmentVariableModel( + key: 'key1', + value: 'value1', + type: EnvironmentVariableType.secret, + enabled: true, + ), + EnvironmentVariableModel( + key: 'key2', + value: 'value2', + type: EnvironmentVariableType.secret, + enabled: false, + ), + ], +); + +/// Basic Environment Variable +const environmentVariableModel1 = EnvironmentVariableModel( + key: 'key1', + value: 'value1', + type: EnvironmentVariableType.variable, + enabled: true, +); + +/// Secret Environment Variable +const environmentVariableModel2 = EnvironmentVariableModel( + key: 'key1', + value: 'value1', + type: EnvironmentVariableType.secret, + enabled: true, +); + +/// Basic Environment Variable Suggestion +const environmentVariableSuggestion1 = EnvironmentVariableSuggestion( + environmentId: 'environmentId', + variable: environmentVariableModel1, +); + +/// Secret Environment Variable Suggestion +const environmentVariableSuggestion2 = EnvironmentVariableSuggestion( + environmentId: 'environmentId', + variable: environmentVariableModel2, +); + +/// JSONs +const environmentModel1Json = { + 'id': 'environmentId', + 'name': 'Development', + 'values': [ + { + 'key': 'key1', + 'value': 'value1', + 'type': 'variable', + 'enabled': true, + }, + { + 'key': 'key2', + 'value': 'value2', + 'type': 'variable', + 'enabled': false, + }, + ], +}; + +const environmentModel2Json = { + 'id': 'environmentId', + 'name': 'Development', + 'values': [ + { + 'key': 'key', + 'value': 'value1', + 'type': 'secret', + 'enabled': true, + }, + { + 'key': 'key2', + 'value': 'value2', + 'type': 'secret', + 'enabled': false, + }, + ], +}; + +const environmentVariableModel1Json = { + 'key': 'key1', + 'value': 'value1', + 'type': 'variable', + 'enabled': true, +}; + +const environmentVariableModel2Json = { + 'key': 'key1', + 'value': 'value1', + 'type': 'secret', + 'enabled': true, +}; diff --git a/test/models/environment_models_test.dart b/test/models/environment_models_test.dart new file mode 100644 index 00000000..eb305147 --- /dev/null +++ b/test/models/environment_models_test.dart @@ -0,0 +1,173 @@ +import 'package:test/test.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/consts.dart'; + +import 'environment_models.dart'; + +void main() { + group("Testing EnvironmentModel", () { + test("Testing EnvironmentModel copyWith", () { + var environmentModel = environmentModel1; + final environmentModelcopyWith = + environmentModel.copyWith(name: 'Production'); + expect(environmentModelcopyWith.name, 'Production'); + // original model unchanged + expect(environmentModel.name, 'Development'); + }); + + test("Testing EnvironmentModel toJson", () { + var environmentModel = environmentModel1; + expect(environmentModel.toJson(), environmentModel1Json); + }); + + test("Testing EnvironmentModel fromJson", () { + var environmentModel = environmentModel1; + final modelFromJson = EnvironmentModel.fromJson(environmentModel1Json); + expect(modelFromJson, environmentModel); + expect(modelFromJson.values, const [ + EnvironmentVariableModel( + key: 'key1', + value: 'value1', + type: EnvironmentVariableType.variable, + enabled: true, + ), + EnvironmentVariableModel( + key: 'key2', + value: 'value2', + type: EnvironmentVariableType.variable, + enabled: false, + ), + ]); + }); + + test("Testing EnvironmentModel getters", () { + var environmentModel = environmentModel1; + expect(environmentModel.values, const [ + EnvironmentVariableModel( + key: 'key1', + value: 'value1', + type: EnvironmentVariableType.variable, + enabled: true, + ), + EnvironmentVariableModel( + key: 'key2', + value: 'value2', + type: EnvironmentVariableType.variable, + enabled: false, + ), + ]); + expect(environmentModel.name, 'Development'); + expect(environmentModel.id, 'environmentId'); + }); + + test("Testing EnvironmentModel immutability", () { + var testEnvironmentModel = environmentModel1; + final testEnvironmentModel2 = + testEnvironmentModel.copyWith(values: testEnvironmentModel.values); + expect(testEnvironmentModel2.values, testEnvironmentModel.values); + + expect( + identical(testEnvironmentModel.values, testEnvironmentModel2.values), + false); + var testEnvironmentModel3 = testEnvironmentModel.copyWith(values: []); + expect(testEnvironmentModel3.values, []); + }); + }); + + group("Testing EnvironmentVariableModel", () { + test("Testing EnvironmentVariableModel copyWith", () { + var environmentVariableModel = environmentVariableModel1; + final environmentVariableModelcopyWith = environmentVariableModel + .copyWith(key: 'key3', value: 'value3', enabled: false); + expect(environmentVariableModelcopyWith.key, 'key3'); + expect(environmentVariableModelcopyWith.value, 'value3'); + expect(environmentVariableModelcopyWith.enabled, false); + // original model unchanged + expect(environmentVariableModel.key, 'key1'); + expect(environmentVariableModel.value, 'value1'); + expect(environmentVariableModel.enabled, true); + }); + + test("Testing EnvironmentVariableModel toJson", () { + var environmentVariable = environmentVariableModel1; + expect(environmentVariable.toJson(), environmentVariableModel1Json); + + var environmentSecret = environmentVariableModel2; + expect(environmentSecret.toJson(), environmentVariableModel2Json); + }); + + test("Testing EnvironmentVariableModel fromJson", () { + var environmentVariableModel = environmentVariableModel1; + final modelFromJson = + EnvironmentVariableModel.fromJson(environmentVariableModel1Json); + expect(modelFromJson, environmentVariableModel); + }); + + test("Testing EnvironmentVariableModel getters", () { + var environmentVariableModel = environmentVariableModel1; + expect(environmentVariableModel.key, 'key1'); + expect(environmentVariableModel.value, 'value1'); + expect(environmentVariableModel.enabled, true); + }); + + test("Testing EnvironmentVariableModel immutability", () { + var testEnvironmentVariableModel = environmentVariableModel1; + final testEnvironmentVariableModel2 = + testEnvironmentVariableModel.copyWith(key: 'key2'); + expect(testEnvironmentVariableModel2.key, 'key2'); + expect(testEnvironmentVariableModel2.value, 'value1'); + expect(testEnvironmentVariableModel2.enabled, true); + + expect( + identical( + testEnvironmentVariableModel, testEnvironmentVariableModel2), + false); + }); + }); + + group("Testing EnvironmentVariableSuggestionModel", () { + test("Testing EnvironmentVariableSuggestionModel copyWith", () { + var environmentVariableSuggestionModel = environmentVariableSuggestion1; + final environmentVariableSuggestionModelcopyWith = + environmentVariableSuggestionModel.copyWith( + environmentId: 'environmentId2', + variable: environmentVariableModel2, + isUnknown: true); + expect(environmentVariableSuggestionModelcopyWith.environmentId, + 'environmentId2'); + expect(environmentVariableSuggestionModelcopyWith.variable, + environmentVariableModel2); + expect(environmentVariableSuggestionModelcopyWith.isUnknown, true); + // original model unchanged + expect(environmentVariableSuggestionModel.environmentId, 'environmentId'); + expect(environmentVariableSuggestionModel.variable, + environmentVariableModel1); + expect(environmentVariableSuggestionModel.isUnknown, false); + }); + + test("Testing EnvironmentVariableSuggestionModel immutability", () { + var testEnvironmentVariableSuggestionModel = + environmentVariableSuggestion1; + final testEnvironmentVariableSuggestionModel2 = + testEnvironmentVariableSuggestionModel.copyWith( + environmentId: 'environmentId2', + variable: environmentVariableModel2, + isUnknown: true); + expect(testEnvironmentVariableSuggestionModel2.environmentId, + 'environmentId2'); + expect(testEnvironmentVariableSuggestionModel2.variable, + environmentVariableModel2); + expect(testEnvironmentVariableSuggestionModel2.isUnknown, true); + + expect( + identical(testEnvironmentVariableSuggestionModel, + testEnvironmentVariableSuggestionModel2), + false); + }); + + test("Testing EnvironmentVariableSuggestionModel hashCode", () { + var environmentVariableSuggestionModel = environmentVariableSuggestion1; + expect(environmentVariableSuggestionModel.hashCode, greaterThan(0)); + }); + }); +} From ad12b4811a04cd87da5abb947749cb77871cd775 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Mon, 29 Jul 2024 23:20:44 +0530 Subject: [PATCH 18/71] fix: expand copyWith tests --- test/models/environment_models_test.dart | 57 +++++++++++++++++++++--- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/test/models/environment_models_test.dart b/test/models/environment_models_test.dart index eb305147..8664b164 100644 --- a/test/models/environment_models_test.dart +++ b/test/models/environment_models_test.dart @@ -128,17 +128,64 @@ void main() { group("Testing EnvironmentVariableSuggestionModel", () { test("Testing EnvironmentVariableSuggestionModel copyWith", () { var environmentVariableSuggestionModel = environmentVariableSuggestion1; - final environmentVariableSuggestionModelcopyWith = + + // Test case where all fields are provided + final environmentVariableSuggestionModelCopyWithAllFields = environmentVariableSuggestionModel.copyWith( environmentId: 'environmentId2', variable: environmentVariableModel2, isUnknown: true); - expect(environmentVariableSuggestionModelcopyWith.environmentId, + expect(environmentVariableSuggestionModelCopyWithAllFields.environmentId, 'environmentId2'); - expect(environmentVariableSuggestionModelcopyWith.variable, + expect(environmentVariableSuggestionModelCopyWithAllFields.variable, environmentVariableModel2); - expect(environmentVariableSuggestionModelcopyWith.isUnknown, true); - // original model unchanged + expect( + environmentVariableSuggestionModelCopyWithAllFields.isUnknown, true); + + // Test case where no fields are provided (should return the same object) + final environmentVariableSuggestionModelCopyWithNoFields = + environmentVariableSuggestionModel.copyWith(); + expect(environmentVariableSuggestionModelCopyWithNoFields.environmentId, + environmentVariableSuggestionModel.environmentId); + expect(environmentVariableSuggestionModelCopyWithNoFields.variable, + environmentVariableSuggestionModel.variable); + expect(environmentVariableSuggestionModelCopyWithNoFields.isUnknown, + environmentVariableSuggestionModel.isUnknown); + + // Test case where only environmentId is provided + final environmentVariableSuggestionModelCopyWithEnvironmentId = + environmentVariableSuggestionModel.copyWith( + environmentId: 'environmentId2'); + expect( + environmentVariableSuggestionModelCopyWithEnvironmentId.environmentId, + 'environmentId2'); + expect(environmentVariableSuggestionModelCopyWithEnvironmentId.variable, + environmentVariableSuggestionModel.variable); + expect(environmentVariableSuggestionModelCopyWithEnvironmentId.isUnknown, + environmentVariableSuggestionModel.isUnknown); + + // Test case where only variable is provided + final environmentVariableSuggestionModelCopyWithVariable = + environmentVariableSuggestionModel.copyWith( + variable: environmentVariableModel2); + expect(environmentVariableSuggestionModelCopyWithVariable.environmentId, + environmentVariableSuggestionModel.environmentId); + expect(environmentVariableSuggestionModelCopyWithVariable.variable, + environmentVariableModel2); + expect(environmentVariableSuggestionModelCopyWithVariable.isUnknown, + environmentVariableSuggestionModel.isUnknown); + + // Test case where only isUnknown is provided + final environmentVariableSuggestionModelCopyWithIsUnknown = + environmentVariableSuggestionModel.copyWith(isUnknown: true); + expect(environmentVariableSuggestionModelCopyWithIsUnknown.environmentId, + environmentVariableSuggestionModel.environmentId); + expect(environmentVariableSuggestionModelCopyWithIsUnknown.variable, + environmentVariableSuggestionModel.variable); + expect( + environmentVariableSuggestionModelCopyWithIsUnknown.isUnknown, true); + + // Ensure the original model remains unchanged expect(environmentVariableSuggestionModel.environmentId, 'environmentId'); expect(environmentVariableSuggestionModel.variable, environmentVariableModel1); From bf0e95921501d7c7f444afe0b1268b84bc0d8747 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Tue, 30 Jul 2024 21:16:34 +0530 Subject: [PATCH 19/71] wip: widget tests --- lib/extensions/string_extensions.dart | 3 + lib/widgets/popup_menu_codegen.dart | 11 +- lib/widgets/popup_menu_env.dart | 10 +- lib/widgets/popup_menu_history.dart | 11 +- lib/widgets/popup_menu_uri.dart | 10 +- test/extensions/context_extensions_test.dart | 113 ++++++++ test/extensions/string_extensions_test.dart | 62 +++++ test/utils/envvar_utils_test.dart | 15 +- test/utils/ui_utils_test.dart | 15 ++ test/widgets/button_clear_response_test.dart | 21 ++ test/widgets/button_copy_test.dart | 53 ++++ test/widgets/button_discord_test.dart | 22 ++ test/widgets/button_repo_test.dart | 44 +++ test/widgets/button_save_download_test.dart | 79 ++++++ test/widgets/button_send_test.dart | 58 ++++ test/widgets/buttons_test.dart | 250 ------------------ test/widgets/card_request_details.dart | 18 ++ .../card_sidebar_environment_test.dart | 206 +++++++++++++++ ...st.dart => card_sidebar_request_test.dart} | 14 +- test/widgets/envvar__indicator_test.dart | 94 +++++++ test/widgets/envvar_popover_test.dart | 36 +++ test/widgets/popup_menu_codegen_test.dart | 76 ++++++ test/widgets/popup_menu_env_test.dart | 118 +++++++++ test/widgets/popup_menu_uri_test.dart | 75 ++++++ test/widgets/sidebar_save_button_test.dart | 24 ++ test/widgets/splitview_dashboard_test.dart | 25 ++ test/widgets/splitview_drawer_test.dart | 97 +++++++ ...ws_test.dart => splitview_equal_test.dart} | 17 -- test/widgets/tabs_test.dart | 43 +++ 29 files changed, 1326 insertions(+), 294 deletions(-) create mode 100644 test/extensions/context_extensions_test.dart create mode 100644 test/extensions/string_extensions_test.dart create mode 100644 test/widgets/button_clear_response_test.dart create mode 100644 test/widgets/button_copy_test.dart create mode 100644 test/widgets/button_discord_test.dart create mode 100644 test/widgets/button_repo_test.dart create mode 100644 test/widgets/button_save_download_test.dart create mode 100644 test/widgets/button_send_test.dart delete mode 100644 test/widgets/buttons_test.dart create mode 100644 test/widgets/card_request_details.dart create mode 100644 test/widgets/card_sidebar_environment_test.dart rename test/widgets/{cards_test.dart => card_sidebar_request_test.dart} (88%) create mode 100644 test/widgets/envvar__indicator_test.dart create mode 100644 test/widgets/envvar_popover_test.dart create mode 100644 test/widgets/popup_menu_codegen_test.dart create mode 100644 test/widgets/popup_menu_env_test.dart create mode 100644 test/widgets/popup_menu_uri_test.dart create mode 100644 test/widgets/sidebar_save_button_test.dart create mode 100644 test/widgets/splitview_dashboard_test.dart create mode 100644 test/widgets/splitview_drawer_test.dart rename test/widgets/{splitviews_test.dart => splitview_equal_test.dart} (60%) create mode 100644 test/widgets/tabs_test.dart diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 37632a2f..86813788 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -10,6 +10,9 @@ extension StringExtension on String { } String clip(int limit) { + if (limit < 0) { + return '...'; + } if (length <= limit) { return this; } diff --git a/lib/widgets/popup_menu_codegen.dart b/lib/widgets/popup_menu_codegen.dart index 421d1ce7..9d1c3bc4 100644 --- a/lib/widgets/popup_menu_codegen.dart +++ b/lib/widgets/popup_menu_codegen.dart @@ -15,7 +15,6 @@ class CodegenPopupMenu extends StatelessWidget { final List? items; @override Widget build(BuildContext context) { - final textClipLength = context.isCompactWindow ? 12 : 22; final double boxLength = context.isCompactWindow ? 150 : 220; return PopupMenuButton( tooltip: "Select Code Generation Language", @@ -37,9 +36,13 @@ class CodegenPopupMenu extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - value.label.clip(textClipLength), - style: kTextStylePopupMenuItem, + Expanded( + child: Text( + value.label, + style: kTextStylePopupMenuItem, + softWrap: false, + overflow: TextOverflow.ellipsis, + ), ), const Icon( Icons.unfold_more, diff --git a/lib/widgets/popup_menu_env.dart b/lib/widgets/popup_menu_env.dart index d61be5b4..63b6034e 100644 --- a/lib/widgets/popup_menu_env.dart +++ b/lib/widgets/popup_menu_env.dart @@ -19,7 +19,6 @@ class EnvironmentPopupMenu extends StatelessWidget { @override Widget build(BuildContext context) { final valueName = getEnvironmentTitle(value?.name); - final textClipLength = context.isCompactWindow ? 6 : 10; final double boxLength = context.isCompactWindow ? 100 : 130; return PopupMenuButton( tooltip: "Select Environment", @@ -54,9 +53,12 @@ class EnvironmentPopupMenu extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - value == null ? "None" : valueName.clip(textClipLength), - softWrap: false, + Expanded( + child: Text( + value == null ? "None" : valueName, + softWrap: false, + overflow: TextOverflow.ellipsis, + ), ), const Icon( Icons.unfold_more, diff --git a/lib/widgets/popup_menu_history.dart b/lib/widgets/popup_menu_history.dart index 72398bb3..4dfba84b 100644 --- a/lib/widgets/popup_menu_history.dart +++ b/lib/widgets/popup_menu_history.dart @@ -40,10 +40,13 @@ class HistoryRetentionPopupMenu extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - value.label, - style: kTextStylePopupMenuItem, - overflow: TextOverflow.ellipsis, + Expanded( + child: Text( + value.label, + style: kTextStylePopupMenuItem, + softWrap: false, + overflow: TextOverflow.ellipsis, + ), ), const Icon( Icons.unfold_more, diff --git a/lib/widgets/popup_menu_uri.dart b/lib/widgets/popup_menu_uri.dart index 5a62c97e..418c6a71 100644 --- a/lib/widgets/popup_menu_uri.dart +++ b/lib/widgets/popup_menu_uri.dart @@ -36,9 +36,13 @@ class URIPopupMenu extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - value, - style: kTextStylePopupMenuItem, + Expanded( + child: Text( + value, + style: kTextStylePopupMenuItem, + softWrap: false, + overflow: TextOverflow.ellipsis, + ), ), const Icon( Icons.unfold_more, diff --git a/test/extensions/context_extensions_test.dart b/test/extensions/context_extensions_test.dart new file mode 100644 index 00000000..6b305b07 --- /dev/null +++ b/test/extensions/context_extensions_test.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/extensions/context_extensions.dart'; + +void main() { + group('Testing MediaQueryExtension', () { + testWidgets('isCompactWindow returns true for compact window size', + (tester) async { + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(size: Size(kCompactWindowWidth - 1, 800)), + child: Builder( + builder: (context) { + expect(context.isCompactWindow, isTrue); + return Container(); + }, + ), + ), + ); + }); + + testWidgets('isMediumWindow returns true for medium window size', + (tester) async { + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(size: Size(kMediumWindowWidth - 1, 800)), + child: Builder( + builder: (context) { + expect(context.isMediumWindow, isTrue); + return Container(); + }, + ), + ), + ); + }); + + testWidgets('isExpandedWindow returns true for expanded window size', + (tester) async { + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(size: Size(kExpandedWindowWidth - 1, 800)), + child: Builder( + builder: (context) { + expect(context.isExpandedWindow, isTrue); + return Container(); + }, + ), + ), + ); + }); + + testWidgets('isLargeWindow returns true for large window size', + (tester) async { + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(size: Size(kLargeWindowWidth - 1, 800)), + child: Builder( + builder: (context) { + expect(context.isLargeWindow, isTrue); + return Container(); + }, + ), + ), + ); + }); + + testWidgets('isExtraLargeWindow returns true for extra large window size', + (tester) async { + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(size: Size(kLargeWindowWidth + 1, 800)), + child: Builder( + builder: (context) { + expect(context.isExtraLargeWindow, isTrue); + return Container(); + }, + ), + ), + ); + }); + + testWidgets('width returns correct width', (tester) async { + const double testWidth = 1024; + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(size: Size(testWidth, 800)), + child: Builder( + builder: (context) { + expect(context.width, testWidth); + return Container(); + }, + ), + ), + ); + }); + + testWidgets('height returns correct height', (tester) async { + const double testHeight = 768; + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(size: Size(1024, testHeight)), + child: Builder( + builder: (context) { + expect(context.height, testHeight); + return Container(); + }, + ), + ), + ); + }); + }); +} diff --git a/test/extensions/string_extensions_test.dart b/test/extensions/string_extensions_test.dart new file mode 100644 index 00000000..c0fcbd58 --- /dev/null +++ b/test/extensions/string_extensions_test.dart @@ -0,0 +1,62 @@ +import 'package:test/test.dart'; +import 'package:apidash/extensions/string_extensions.dart'; + +void main() { + group('Testing StringExtensions', () { + group('Testing capitalize', () { + test('should capitalize the first letter of a lowercase word', () { + expect('hello'.capitalize(), 'Hello'); + }); + + test( + 'should capitalize the first letter and lowercase the rest of an uppercase word', + () { + expect('HELLO'.capitalize(), 'Hello'); + }); + + test('should return the same string if it is already capitalized', () { + expect('Hello'.capitalize(), 'Hello'); + }); + + test('should return an empty string if the input is empty', () { + expect(''.capitalize(), ''); + }); + + test('should capitalize a single lowercase letter', () { + expect('h'.capitalize(), 'H'); + }); + + test('should return the same single uppercase letter', () { + expect('H'.capitalize(), 'H'); + }); + }); + + group('Testing clip', () { + test( + 'should return the same string if its length is less than or equal to the limit', + () { + expect('hello'.clip(5), 'hello'); + expect('hello'.clip(10), 'hello'); + }); + + test( + 'should clip the string and add ellipsis if its length is greater than the limit', + () { + expect('hello world'.clip(5), 'hello...'); + expect('hello world'.clip(8), 'hello wo...'); + }); + + test('should return an empty string if the input is empty', () { + expect(''.clip(5), ''); + }); + + test('should handle limit of 0 correctly', () { + expect('hello'.clip(0), '...'); + }); + + test('should handle negative limit correctly', () { + expect('hello'.clip(-1), '...'); + }); + }); + }); +} diff --git a/test/utils/envvar_utils_test.dart b/test/utils/envvar_utils_test.dart index 59bb4740..84b14652 100644 --- a/test/utils/envvar_utils_test.dart +++ b/test/utils/envvar_utils_test.dart @@ -299,7 +299,7 @@ void main() { }); group("Testing getVariableStatus function", () { - test("Testing getVariableStatus with available variable", () { + test("Testing getVariableStatus with variable available in global", () { const query = "num"; Map> envMap = { kGlobalEnvironmentId: globalVars, @@ -310,6 +310,19 @@ void main() { expect(getVariableStatus(query, envMap, null), expected); }); + test("Testing getVariableStatus with variable available in active", () { + const query = "num"; + Map> envMap = { + kGlobalEnvironmentId: globalVars, + "activeEnvId": activeEnvVars, + }; + const activeEnvironmentId = "activeEnvId"; + const expected = EnvironmentVariableSuggestion( + environmentId: activeEnvironmentId, + variable: EnvironmentVariableModel(key: "num", value: "8940000")); + expect(getVariableStatus(query, envMap, activeEnvironmentId), expected); + }); + test("Testing getVariableStatus with unavailable variable", () { const query = "path"; Map> envMap = { diff --git a/test/utils/ui_utils_test.dart b/test/utils/ui_utils_test.dart index 0a18431a..4d5a686b 100644 --- a/test/utils/ui_utils_test.dart +++ b/test/utils/ui_utils_test.dart @@ -141,4 +141,19 @@ void main() { colMethodDeleteDarkModeExpected); }); }); + + group('Testing getScaffoldKey function', () { + test('Returns kEnvScaffoldKey for railIdx 1', () { + expect(getScaffoldKey(1), kEnvScaffoldKey); + }); + + test('Returns kHisScaffoldKey for railIdx 2', () { + expect(getScaffoldKey(2), kHisScaffoldKey); + }); + + test('Returns kHomeScaffoldKey for railIdx other than 1 or 2', () { + expect(getScaffoldKey(0), kHomeScaffoldKey); + expect(getScaffoldKey(3), kHomeScaffoldKey); + }); + }); } diff --git a/test/widgets/button_clear_response_test.dart b/test/widgets/button_clear_response_test.dart new file mode 100644 index 00000000..e8dfb76c --- /dev/null +++ b/test/widgets/button_clear_response_test.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/button_clear_response.dart'; + +import '../test_consts.dart'; + +void main() { + testWidgets('Testing for ClearResponseButton', (tester) async { + await tester.pumpWidget( + MaterialApp( + title: 'ClearResponseButton', + theme: kThemeDataLight, + home: const Scaffold( + body: ClearResponseButton(), + ), + ), + ); + + expect(find.byIcon(Icons.delete), findsOneWidget); + }); +} diff --git a/test/widgets/button_copy_test.dart b/test/widgets/button_copy_test.dart new file mode 100644 index 00000000..d9397d93 --- /dev/null +++ b/test/widgets/button_copy_test.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/button_copy.dart'; + +void main() { + String copyText = 'This is a sample response generated by '; + + testWidgets('Testing for copy button with label', (tester) async { + await tester.pumpWidget( + MaterialApp( + title: 'Copy Button', + home: Scaffold( + body: CopyButton(toCopy: copyText, showLabel: true), + ), + ), + ); + + final icon = find.byIcon(Icons.content_copy); + expect(icon, findsOneWidget); + + final button = find.ancestor( + of: icon, + matching: find.byWidgetPredicate((widget) => widget is TextButton)); + expect(button, findsOneWidget); + + await tester.tap(button); + await tester.pumpAndSettle(); + + //TODO: The below test works for `flutter run` but not for `flutter test` + // var data = await Clipboard.getData('text/plain'); + // expect(data?.text, copyText); + }); + + testWidgets('Testing for copy button without label', (tester) async { + await tester.pumpWidget( + MaterialApp( + title: 'Copy Button', + home: Scaffold( + body: CopyButton(toCopy: copyText, showLabel: false), + ), + ), + ); + + final icon = find.byIcon(Icons.content_copy); + expect(icon, findsOneWidget); + + final button = find.byType(IconButton); + expect(button, findsOneWidget); + + await tester.tap(button); + await tester.pump(); + }); +} diff --git a/test/widgets/button_discord_test.dart b/test/widgets/button_discord_test.dart new file mode 100644 index 00000000..63a146aa --- /dev/null +++ b/test/widgets/button_discord_test.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/button_discord.dart'; + +import '../test_consts.dart'; + +void main() { + testWidgets('Testing for Discord button', (tester) async { + await tester.pumpWidget( + MaterialApp( + title: 'Discord button', + theme: kThemeDataLight, + home: const Scaffold( + body: DiscordButton(), + ), + ), + ); + + expect(find.byIcon(Icons.discord), findsOneWidget); + expect(find.text("Discord Server"), findsOneWidget); + }); +} diff --git a/test/widgets/button_repo_test.dart b/test/widgets/button_repo_test.dart new file mode 100644 index 00000000..b0023ef7 --- /dev/null +++ b/test/widgets/button_repo_test.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/button_repo.dart'; + +import '../test_consts.dart'; + +void main() { + testWidgets('Testing for Repo button', (tester) async { + await tester.pumpWidget( + MaterialApp( + title: 'Repo button', + theme: kThemeDataLight, + home: const Scaffold( + body: RepoButton( + icon: Icons.code, + ), + ), + ), + ); + + expect(find.byIcon(Icons.code), findsOneWidget); + expect(find.text("GitHub"), findsOneWidget); + }); + + testWidgets('Testing for Repo button icon = null', (tester) async { + await tester.pumpWidget( + MaterialApp( + title: 'Repo button', + theme: kThemeDataLight, + home: const Scaffold( + body: RepoButton(), + ), + ), + ); + + expect(find.byIcon(Icons.code), findsNothing); + expect(find.text("GitHub"), findsOneWidget); + + final button1 = find.byType(FilledButton); + expect(button1, findsOneWidget); + + expect(tester.widget(button1).enabled, isTrue); + }); +} diff --git a/test/widgets/button_save_download_test.dart b/test/widgets/button_save_download_test.dart new file mode 100644 index 00000000..288849a8 --- /dev/null +++ b/test/widgets/button_save_download_test.dart @@ -0,0 +1,79 @@ +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/widgets/button_save_download.dart'; + +import '../test_consts.dart'; + +void main() { + testWidgets('Testing for Save in Downloads button', (tester) async { + await tester.pumpWidget( + MaterialApp( + title: 'Save in Downloads button', + theme: kThemeDataLight, + home: const Scaffold( + body: SaveInDownloadsButton(), + ), + ), + ); + + final icon = find.byIcon(Icons.download); + expect(icon, findsOneWidget); + + Finder button; + if (tester.any(find.ancestor( + of: icon, + matching: find.byWidgetPredicate((widget) => widget is TextButton)))) { + expect(find.text(kLabelDownload), findsOneWidget); + button = find.ancestor( + of: icon, + matching: find.byWidgetPredicate((widget) => widget is TextButton)); + expect(button, findsOneWidget); + expect(tester.widget(button).enabled, isFalse); + } else if (tester + .any(find.ancestor(of: icon, matching: find.byType(IconButton)))) { + button = find.byType(IconButton); + expect(button, findsOneWidget); + expect(tester.widget(button).onPressed == null, isFalse); + } else { + fail('No TextButton or IconButton found'); + } + }); + + testWidgets('Testing for Save in Downloads button 2', (tester) async { + await tester.pumpWidget( + MaterialApp( + title: 'Save in Downloads button', + theme: kThemeDataLight, + home: Scaffold( + body: SaveInDownloadsButton( + content: Uint8List.fromList([1]), + ), + ), + ), + ); + + final icon = find.byIcon(Icons.download); + expect(icon, findsOneWidget); + + Finder button; + if (tester.any(find.ancestor( + of: icon, + matching: find.byWidgetPredicate((widget) => widget is TextButton)))) { + expect(find.text(kLabelDownload), findsOneWidget); + button = find.ancestor( + of: icon, + matching: find.byWidgetPredicate((widget) => widget is TextButton)); + expect(button, findsOneWidget); + expect(tester.widget(button).enabled, isTrue); + } else if (tester + .any(find.ancestor(of: icon, matching: find.byType(IconButton)))) { + button = find.byType(IconButton); + expect(button, findsOneWidget); + expect(tester.widget(button).onPressed == null, isTrue); + } else { + fail('No TextButton or IconButton found'); + } + }); +} diff --git a/test/widgets/button_send_test.dart b/test/widgets/button_send_test.dart new file mode 100644 index 00000000..f02fddb9 --- /dev/null +++ b/test/widgets/button_send_test.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/widgets/button_send.dart'; + +import '../test_consts.dart'; + +void main() { + testWidgets('Testing for Send Request button', (tester) async { + dynamic changedValue; + await tester.pumpWidget( + MaterialApp( + title: 'Send Request button', + theme: kThemeDataLight, + home: Scaffold( + body: SendButton( + isWorking: false, + onTap: () { + changedValue = 'Send'; + }, + ), + ), + ), + ); + + expect(find.byIcon(Icons.send), findsOneWidget); + expect(find.text(kLabelSend), findsOneWidget); + final button1 = find.byType(FilledButton); + expect(button1, findsOneWidget); + + await tester.tap(button1); + expect(changedValue, 'Send'); + }); + + testWidgets( + 'Testing for Send Request button when RequestModel is viewed and is waiting for response', + (tester) async { + await tester.pumpWidget( + MaterialApp( + title: 'Send Request button', + theme: kThemeDataLight, + home: Scaffold( + body: SendButton( + isWorking: true, + onTap: () {}, + ), + ), + ), + ); + + expect(find.byIcon(Icons.send), findsNothing); + expect(find.text(kLabelSending), findsOneWidget); + final button1 = find.byType(FilledButton); + expect(button1, findsOneWidget); + + expect(tester.widget(button1).enabled, isFalse); + }); +} diff --git a/test/widgets/buttons_test.dart b/test/widgets/buttons_test.dart deleted file mode 100644 index d611d4f0..00000000 --- a/test/widgets/buttons_test.dart +++ /dev/null @@ -1,250 +0,0 @@ -import 'dart:typed_data'; -import 'package:apidash/screens/common_widgets/sidebar_save_button.dart'; -import 'package:apidash/widgets/widgets.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:apidash/consts.dart'; -import '../test_consts.dart'; - -void main() { - String copyText = 'This is a sample response generated by '; - testWidgets('Testing for copy button', (tester) async { - await tester.pumpWidget( - MaterialApp( - title: 'Copy Button', - home: Scaffold( - body: CopyButton(toCopy: copyText), - ), - ), - ); - - final icon = find.byIcon(Icons.content_copy); - expect(icon, findsOneWidget); - - Finder button; - if (tester.any(find.ancestor( - of: icon, - matching: find.byWidgetPredicate((widget) => widget is TextButton)))) { - expect(find.text(kLabelCopy), findsOneWidget); - button = find.ancestor( - of: icon, - matching: find.byWidgetPredicate((widget) => widget is TextButton)); - } else if (tester - .any(find.ancestor(of: icon, matching: find.byType(IconButton)))) { - button = find.byType(IconButton); - } else { - fail('No TextButton or IconButton found'); - } - - expect(button, findsOneWidget); - await tester.tap(button); - - //TODO: The below test works for `flutter run` but not for `flutter test` - //var data = await Clipboard.getData('text/plain'); - //expect(data?.text, copyText); - }); - - testWidgets('Testing for Send Request button', (tester) async { - dynamic changedValue; - await tester.pumpWidget( - MaterialApp( - title: 'Send Request button', - theme: kThemeDataLight, - home: Scaffold( - body: SendButton( - isWorking: false, - onTap: () { - changedValue = 'Send'; - }, - ), - ), - ), - ); - - expect(find.byIcon(Icons.send), findsOneWidget); - expect(find.text(kLabelSend), findsOneWidget); - final button1 = find.byType(FilledButton); - expect(button1, findsOneWidget); - - await tester.tap(button1); - expect(changedValue, 'Send'); - }); - - testWidgets( - 'Testing for Send Request button when RequestModel is viewed and is waiting for response', - (tester) async { - await tester.pumpWidget( - MaterialApp( - title: 'Send Request button', - theme: kThemeDataLight, - home: Scaffold( - body: SendButton( - isWorking: true, - onTap: () {}, - ), - ), - ), - ); - - expect(find.byIcon(Icons.send), findsNothing); - expect(find.text(kLabelSending), findsOneWidget); - final button1 = find.byType(FilledButton); - expect(button1, findsOneWidget); - - expect(tester.widget(button1).enabled, isFalse); - }); - - testWidgets('Testing for Save in Downloads button', (tester) async { - await tester.pumpWidget( - MaterialApp( - title: 'Save in Downloads button', - theme: kThemeDataLight, - home: const Scaffold( - body: SaveInDownloadsButton(), - ), - ), - ); - - final icon = find.byIcon(Icons.download); - expect(icon, findsOneWidget); - - Finder button; - if (tester.any(find.ancestor( - of: icon, - matching: find.byWidgetPredicate((widget) => widget is TextButton)))) { - expect(find.text(kLabelDownload), findsOneWidget); - button = find.ancestor( - of: icon, - matching: find.byWidgetPredicate((widget) => widget is TextButton)); - expect(button, findsOneWidget); - expect(tester.widget(button).enabled, isFalse); - } else if (tester - .any(find.ancestor(of: icon, matching: find.byType(IconButton)))) { - button = find.byType(IconButton); - expect(button, findsOneWidget); - expect(tester.widget(button).onPressed == null, isFalse); - } else { - fail('No TextButton or IconButton found'); - } - }); - - testWidgets('Testing for Save in Downloads button 2', (tester) async { - await tester.pumpWidget( - MaterialApp( - title: 'Save in Downloads button', - theme: kThemeDataLight, - home: Scaffold( - body: SaveInDownloadsButton( - content: Uint8List.fromList([1]), - ), - ), - ), - ); - - final icon = find.byIcon(Icons.download); - expect(icon, findsOneWidget); - - Finder button; - if (tester.any(find.ancestor( - of: icon, - matching: find.byWidgetPredicate((widget) => widget is TextButton)))) { - expect(find.text(kLabelDownload), findsOneWidget); - button = find.ancestor( - of: icon, - matching: find.byWidgetPredicate((widget) => widget is TextButton)); - expect(button, findsOneWidget); - expect(tester.widget(button).enabled, isTrue); - } else if (tester - .any(find.ancestor(of: icon, matching: find.byType(IconButton)))) { - button = find.byType(IconButton); - expect(button, findsOneWidget); - expect(tester.widget(button).onPressed == null, isTrue); - } else { - fail('No TextButton or IconButton found'); - } - }); - - testWidgets('Testing for Repo button', (tester) async { - await tester.pumpWidget( - MaterialApp( - title: 'Repo button', - theme: kThemeDataLight, - home: const Scaffold( - body: RepoButton( - icon: Icons.code, - ), - ), - ), - ); - - expect(find.byIcon(Icons.code), findsOneWidget); - expect(find.text("GitHub"), findsOneWidget); - }); - - testWidgets('Testing for Repo button icon = null', (tester) async { - await tester.pumpWidget( - MaterialApp( - title: 'Repo button', - theme: kThemeDataLight, - home: const Scaffold( - body: RepoButton(), - ), - ), - ); - - expect(find.byIcon(Icons.code), findsNothing); - expect(find.text("GitHub"), findsOneWidget); - - final button1 = find.byType(FilledButton); - expect(button1, findsOneWidget); - - expect(tester.widget(button1).enabled, isTrue); - }); - - testWidgets('Testing for Discord button', (tester) async { - await tester.pumpWidget( - MaterialApp( - title: 'Discord button', - theme: kThemeDataLight, - home: const Scaffold( - body: DiscordButton(), - ), - ), - ); - - expect(find.byIcon(Icons.discord), findsOneWidget); - expect(find.text("Discord Server"), findsOneWidget); - }); - - testWidgets('Testing for Save button', (tester) async { - await tester.pumpWidget( - ProviderScope( - child: MaterialApp( - title: 'Save button', - theme: kThemeDataLight, - home: const Scaffold( - body: SaveButton(), - ), - ), - ), - ); - - expect(find.byIcon(Icons.save), findsOneWidget); - expect(find.text("Save"), findsOneWidget); - }); - - testWidgets('Testing for ClearResponseButton', (tester) async { - await tester.pumpWidget( - MaterialApp( - title: 'ClearResponseButton', - theme: kThemeDataLight, - home: const Scaffold( - body: ClearResponseButton(), - ), - ), - ); - - expect(find.byIcon(Icons.delete), findsOneWidget); - }); -} diff --git a/test/widgets/card_request_details.dart b/test/widgets/card_request_details.dart new file mode 100644 index 00000000..bc5df725 --- /dev/null +++ b/test/widgets/card_request_details.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/widgets.dart'; + +void main() { + testWidgets('Testing Request Details Card', (tester) async { + await tester.pumpWidget( + const MaterialApp( + title: 'Request Details Card', + home: Scaffold( + body: RequestDetailsCard(child: SizedBox(height: 10, width: 10))), + ), + ); + + expect(find.byType(Card), findsOneWidget); + expect(find.byType(SizedBox), findsOneWidget); + }); +} diff --git a/test/widgets/card_sidebar_environment_test.dart b/test/widgets/card_sidebar_environment_test.dart new file mode 100644 index 00000000..f053127a --- /dev/null +++ b/test/widgets/card_sidebar_environment_test.dart @@ -0,0 +1,206 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/widgets.dart' + show SidebarEnvironmentCard, ItemCardMenu; + +import '../test_consts.dart'; + +Future pumpSidebarEnvironmentCard( + WidgetTester tester, { + required ThemeData theme, + required String id, + required String selectedId, + String? editRequestId, + bool isGlobal = false, + required String name, + Function()? onTap, + Function()? onDoubleTap, + Function(String)? onChangedNameEditor, + Function()? onTapOutsideNameEditor, + Function(dynamic)? onMenuSelected, +}) async { + await tester.pumpWidget( + MaterialApp( + title: 'Sidebar Environment Card', + theme: theme, + home: Scaffold( + body: ListView( + children: [ + SidebarEnvironmentCard( + id: id, + selectedId: selectedId, + editRequestId: editRequestId, + isGlobal: isGlobal, + name: name, + onTap: onTap, + onDoubleTap: onDoubleTap, + onChangedNameEditor: onChangedNameEditor, + onTapOutsideNameEditor: onTapOutsideNameEditor, + onMenuSelected: onMenuSelected, + ), + ], + ), + ), + ), + ); +} + +void main() { + testWidgets('Testing Sidebar Environment Card', (tester) async { + dynamic changedValue; + + await pumpSidebarEnvironmentCard( + tester, + theme: kThemeDataLight, + id: '23', + selectedId: '2', + name: 'Production', + onTap: () { + changedValue = 'Single Tapped'; + }, + onDoubleTap: () { + changedValue = 'Double Tapped'; + }, + ); + + expect(find.byType(InkWell), findsOneWidget); + expect(find.text('Production'), findsOneWidget); + expect(find.widgetWithText(SizedBox, 'Production'), findsOneWidget); + expect(find.widgetWithText(Card, 'Production'), findsOneWidget); + + var tappable = find.widgetWithText(Card, 'Production'); + await tester.tap(tappable); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(changedValue, 'Single Tapped'); + }); + + testWidgets('Testing Sidebar Environment Card dark mode', (tester) async { + dynamic changedValue; + + await pumpSidebarEnvironmentCard( + tester, + theme: kThemeDataDark, + id: '23', + selectedId: '2', + name: 'Production', + onTap: () { + changedValue = 'Single Tapped'; + }, + onDoubleTap: () { + changedValue = 'Double Tapped'; + }, + ); + + expect(find.byType(InkWell), findsOneWidget); + expect(find.text('Production'), findsOneWidget); + expect(find.widgetWithText(SizedBox, 'Production'), findsOneWidget); + expect(find.widgetWithText(Card, 'Production'), findsOneWidget); + + var tappable = find.widgetWithText(Card, 'Production'); + await tester.tap(tappable); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(changedValue, 'Single Tapped'); + }); + + testWidgets('Testing Sidebar Environment Card inEditMode', (tester) async { + dynamic changedValue; + + await pumpSidebarEnvironmentCard( + tester, + theme: kThemeDataLight, + id: '23', + selectedId: '23', + editRequestId: '23', + name: 'Production', + onChangedNameEditor: (value) { + changedValue = value; + }, + onTapOutsideNameEditor: () { + changedValue = 'Tapped Outside'; + }, + ); + + expect(find.byType(InkWell), findsOneWidget); + + var tappable = find.byType(TextFormField); + await tester.enterText(tappable, 'entering 123 for testing'); + await tester.pumpAndSettle(); + expect(changedValue, 'entering 123 for testing'); + + await tester.tapAt(const Offset(100, 100)); + await tester.pumpAndSettle(); + expect(changedValue, 'Tapped Outside'); + + await tester.enterText(tappable, 'New Name'); + await tester.testTextInput.receiveAction(TextInputAction.done); + expect(changedValue, "Tapped Outside"); + }); + + group("Testing Sidebar Environment Card Item Card Menu visibility", () { + testWidgets( + 'Environment ItemCardMenu should be visible when not in edit mode', + (tester) async { + await pumpSidebarEnvironmentCard( + tester, + theme: kThemeDataLight, + id: '23', + selectedId: '23', + isGlobal: false, + name: 'Production', + onMenuSelected: (value) {}, + ); + + expect(find.byType(ItemCardMenu), findsOneWidget); + }); + + testWidgets( + 'Environment ItemCardMenu should not be visible when in edit mode', + (tester) async { + await pumpSidebarEnvironmentCard( + tester, + theme: kThemeDataLight, + id: '23', + selectedId: '23', + editRequestId: '23', + isGlobal: false, + name: 'Production', + onMenuSelected: (value) {}, + ); + + expect(find.byType(ItemCardMenu), findsNothing); + }); + + testWidgets( + 'Environment ItemCardMenu should not be visible when not selected', + (tester) async { + await pumpSidebarEnvironmentCard( + tester, + theme: kThemeDataLight, + id: '23', + selectedId: '24', + editRequestId: '24', + isGlobal: false, + name: 'Production', + onMenuSelected: (value) {}, + ); + + expect(find.byType(ItemCardMenu), findsNothing); + }); + + testWidgets('Environment ItemCardMenu should not be visible if isGlobal', + (tester) async { + await pumpSidebarEnvironmentCard( + tester, + theme: kThemeDataLight, + id: '23', + selectedId: '23', + editRequestId: '24', + isGlobal: true, + name: 'Production', + onMenuSelected: (value) {}, + ); + + expect(find.byType(ItemCardMenu), findsNothing); + }); + }); +} diff --git a/test/widgets/cards_test.dart b/test/widgets/card_sidebar_request_test.dart similarity index 88% rename from test/widgets/cards_test.dart rename to test/widgets/card_sidebar_request_test.dart index b66288eb..d3ba3155 100644 --- a/test/widgets/cards_test.dart +++ b/test/widgets/card_sidebar_request_test.dart @@ -90,17 +90,9 @@ void main() { await tester.tapAt(const Offset(100, 100)); await tester.pumpAndSettle(); expect(changedValue, 'Tapped Outside'); - }); - testWidgets('Testing Request Details Card', (tester) async { - await tester.pumpWidget( - const MaterialApp( - title: 'Request Details Card', - home: Scaffold( - body: RequestDetailsCard(child: SizedBox(height: 10, width: 10))), - ), - ); - expect(find.byType(Card), findsOneWidget); - expect(find.byType(SizedBox), findsOneWidget); + await tester.enterText(tappable, 'New Name'); + await tester.testTextInput.receiveAction(TextInputAction.done); + expect(changedValue, "Tapped Outside"); }); } diff --git a/test/widgets/envvar__indicator_test.dart b/test/widgets/envvar__indicator_test.dart new file mode 100644 index 00000000..9dc9bdc6 --- /dev/null +++ b/test/widgets/envvar__indicator_test.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/screens/common_widgets/envvar_indicator.dart'; + +void main() { + testWidgets( + 'EnvVarIndicator displays correct icon and color for unknown suggestion', + (WidgetTester tester) async { + const suggestion = EnvironmentVariableSuggestion( + isUnknown: true, + environmentId: 'someId', + variable: EnvironmentVariableModel(key: 'key', value: 'value')); + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: EnvVarIndicator(suggestion: suggestion), + ), + ), + ); + + final container = tester.widget(find.byType(Container)); + final icon = tester.widget(find.byType(Icon)); + + expect(container.decoration, isA()); + final decoration = container.decoration as BoxDecoration; + expect( + decoration.color, + Theme.of(tester.element(find.byType(Container))) + .colorScheme + .errorContainer); + expect(icon.icon, Icons.block); + }); + + testWidgets( + 'EnvVarIndicator displays correct icon and color for global suggestion', + (WidgetTester tester) async { + const suggestion = EnvironmentVariableSuggestion( + isUnknown: false, + environmentId: kGlobalEnvironmentId, + variable: EnvironmentVariableModel(key: 'key', value: 'value')); + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: EnvVarIndicator(suggestion: suggestion), + ), + ), + ); + + final container = tester.widget(find.byType(Container)); + final icon = tester.widget(find.byType(Icon)); + + expect(container.decoration, isA()); + final decoration = container.decoration as BoxDecoration; + expect( + decoration.color, + Theme.of(tester.element(find.byType(Container))) + .colorScheme + .secondaryContainer); + expect(icon.icon, Icons.public); + }); + + testWidgets( + 'EnvVarIndicator displays correct icon and color for non-global suggestion', + (WidgetTester tester) async { + const suggestion = EnvironmentVariableSuggestion( + isUnknown: false, + environmentId: 'someOtherId', + variable: EnvironmentVariableModel(key: 'key', value: 'value')); + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: EnvVarIndicator(suggestion: suggestion), + ), + ), + ); + + final container = tester.widget(find.byType(Container)); + final icon = tester.widget(find.byType(Icon)); + + expect(container.decoration, isA()); + final decoration = container.decoration as BoxDecoration; + expect( + decoration.color, + Theme.of(tester.element(find.byType(Container))) + .colorScheme + .primaryContainer); + expect(icon.icon, Icons.computer); + }); +} diff --git a/test/widgets/envvar_popover_test.dart b/test/widgets/envvar_popover_test.dart new file mode 100644 index 00000000..8621a68a --- /dev/null +++ b/test/widgets/envvar_popover_test.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/screens/common_widgets/envvar_indicator.dart'; +import 'package:apidash/screens/common_widgets/envvar_popover.dart'; + +void main() { + testWidgets('EnvVarPopover displays correct information', + (WidgetTester tester) async { + const suggestion = EnvironmentVariableSuggestion( + isUnknown: false, + environmentId: 'someId', + variable: EnvironmentVariableModel(key: 'API_KEY', value: '12345'), + ); + const scope = 'Global'; + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: EnvVarPopover(suggestion: suggestion, scope: scope), + ), + ), + ); + + expect(find.byType(EnvVarIndicator), findsOneWidget); + + expect(find.text('API_KEY'), findsOneWidget); + + expect(find.text('VALUE'), findsOneWidget); + expect(find.text('12345'), findsOneWidget); + + expect(find.text('SCOPE'), findsOneWidget); + expect(find.text('Global'), findsOneWidget); + }); +} diff --git a/test/widgets/popup_menu_codegen_test.dart b/test/widgets/popup_menu_codegen_test.dart new file mode 100644 index 00000000..b613c686 --- /dev/null +++ b/test/widgets/popup_menu_codegen_test.dart @@ -0,0 +1,76 @@ +import 'package:apidash/consts.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/popup_menu_codegen.dart'; + +void main() { + testWidgets('CodegenPopupMenu displays initial value', + (WidgetTester tester) async { + const codegenLanguage = CodegenLanguage.dartDio; + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: CodegenPopupMenu( + value: codegenLanguage, + items: [codegenLanguage], + ), + ), + ), + ); + + expect(find.text(codegenLanguage.label), findsOneWidget); + }); + + testWidgets('CodegenPopupMenu displays popup menu items', + (WidgetTester tester) async { + const codegenLanguage1 = CodegenLanguage.dartDio; + const codegenLanguage2 = CodegenLanguage.pythonRequests; + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: CodegenPopupMenu( + items: [codegenLanguage1, codegenLanguage2], + value: codegenLanguage1, + ), + ), + ), + ); + + await tester.tap(find.byIcon(Icons.unfold_more)); + await tester.pumpAndSettle(); + + expect(find.text(codegenLanguage1.label), findsExactly(2)); + expect(find.text(codegenLanguage2.label), findsOneWidget); + }); + + testWidgets('CodegenPopupMenu calls onChanged when an item is selected', + (WidgetTester tester) async { + const codegenLanguage1 = CodegenLanguage.dartDio; + const codegenLanguage2 = CodegenLanguage.pythonRequests; + CodegenLanguage? selectedLanguage; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: CodegenPopupMenu( + value: codegenLanguage1, + items: const [codegenLanguage1, codegenLanguage2], + onChanged: (value) { + selectedLanguage = value; + }, + ), + ), + ), + ); + + await tester.tap(find.byIcon(Icons.unfold_more)); + await tester.pumpAndSettle(); + + await tester.tap(find.text(codegenLanguage2.label).last); + await tester.pumpAndSettle(); + + expect(selectedLanguage, codegenLanguage2); + }); +} diff --git a/test/widgets/popup_menu_env_test.dart b/test/widgets/popup_menu_env_test.dart new file mode 100644 index 00000000..cbb3b5f5 --- /dev/null +++ b/test/widgets/popup_menu_env_test.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/widgets/popup_menu_env.dart'; + +void main() { + testWidgets('EnvironmentPopupMenu displays initial value', + (WidgetTester tester) async { + const environment = EnvironmentModel(name: 'Production', id: 'prod'); + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: EnvironmentPopupMenu( + value: environment, + items: [environment], + ), + ), + ), + ); + + expect(find.text('Production'), findsOneWidget); + }); + + testWidgets('EnvironmentPopupMenu displays "None" when no value is provided', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: EnvironmentPopupMenu( + items: [], + ), + ), + ), + ); + + expect(find.text('None'), findsOneWidget); + }); + + testWidgets('EnvironmentPopupMenu displays popup menu items', + (WidgetTester tester) async { + const environment1 = EnvironmentModel(name: 'Production', id: 'prod'); + const environment2 = EnvironmentModel(name: 'Development', id: 'dev'); + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: EnvironmentPopupMenu( + items: [environment1, environment2], + ), + ), + ), + ); + + await tester.tap(find.byIcon(Icons.unfold_more)); + await tester.pumpAndSettle(); + + expect(find.text('None'), findsExactly(2)); + expect(find.text('Production'), findsOneWidget); + expect(find.text('Development'), findsOneWidget); + }); + + testWidgets('EnvironmentPopupMenu calls onChanged when an item is selected', + (WidgetTester tester) async { + const environment1 = EnvironmentModel(name: 'Production', id: 'prod'); + const environment2 = EnvironmentModel(name: 'Development', id: 'dev'); + EnvironmentModel? selectedEnvironment; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: EnvironmentPopupMenu( + items: const [environment1, environment2], + onChanged: (value) { + selectedEnvironment = value; + }, + ), + ), + ), + ); + + await tester.tap(find.byIcon(Icons.unfold_more)); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Development').last); + await tester.pumpAndSettle(); + + expect(selectedEnvironment, environment2); + }); + + testWidgets( + 'EnvironmentPopupMenu calls onChanged with null when "None" is selected', + (WidgetTester tester) async { + const environment = EnvironmentModel(name: 'Production', id: 'prod'); + EnvironmentModel? selectedEnvironment = environment; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: EnvironmentPopupMenu( + items: const [environment], + onChanged: (value) { + selectedEnvironment = value; + }, + ), + ), + ), + ); + + await tester.tap(find.byIcon(Icons.unfold_more)); + await tester.pumpAndSettle(); + + await tester.tap(find.text('None').last); + await tester.pumpAndSettle(); + + expect(selectedEnvironment, isNull); + }); +} diff --git a/test/widgets/popup_menu_uri_test.dart b/test/widgets/popup_menu_uri_test.dart new file mode 100644 index 00000000..f27432ee --- /dev/null +++ b/test/widgets/popup_menu_uri_test.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/popup_menu_uri.dart'; + +void main() { + testWidgets('URIPopupMenu displays initial value', + (WidgetTester tester) async { + const uriScheme = 'https'; + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: URIPopupMenu( + value: uriScheme, + items: [uriScheme], + ), + ), + ), + ); + + expect(find.text(uriScheme), findsOneWidget); + }); + + testWidgets('URIPopupMenu displays popup menu items', + (WidgetTester tester) async { + const uriScheme1 = 'https'; + const uriScheme2 = 'http'; + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: URIPopupMenu( + items: [uriScheme1, uriScheme2], + value: uriScheme1, + ), + ), + ), + ); + + await tester.tap(find.byIcon(Icons.unfold_more)); + await tester.pumpAndSettle(); + + expect(find.text(uriScheme1), findsExactly(2)); + expect(find.text(uriScheme2), findsOneWidget); + }); + + testWidgets('URIPopupMenu calls onChanged when an item is selected', + (WidgetTester tester) async { + const uriScheme1 = 'https'; + const uriScheme2 = 'http'; + String? selectedScheme; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: URIPopupMenu( + value: uriScheme1, + items: const [uriScheme1, uriScheme2], + onChanged: (value) { + selectedScheme = value; + }, + ), + ), + ), + ); + + await tester.tap(find.byIcon(Icons.unfold_more)); + await tester.pumpAndSettle(); + + await tester.tap(find.text(uriScheme2).last); + await tester.pumpAndSettle(); + + expect(selectedScheme, uriScheme2); + }); +} diff --git a/test/widgets/sidebar_save_button_test.dart b/test/widgets/sidebar_save_button_test.dart new file mode 100644 index 00000000..2ecb0cec --- /dev/null +++ b/test/widgets/sidebar_save_button_test.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/screens/common_widgets/sidebar_save_button.dart'; +import '../test_consts.dart'; + +void main() { + testWidgets('Testing for Save button', (tester) async { + await tester.pumpWidget( + ProviderScope( + child: MaterialApp( + title: 'Save button', + theme: kThemeDataLight, + home: const Scaffold( + body: SaveButton(), + ), + ), + ), + ); + + expect(find.byIcon(Icons.save), findsOneWidget); + expect(find.text("Save"), findsOneWidget); + }); +} diff --git a/test/widgets/splitview_dashboard_test.dart b/test/widgets/splitview_dashboard_test.dart new file mode 100644 index 00000000..b0c3ab11 --- /dev/null +++ b/test/widgets/splitview_dashboard_test.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:multi_split_view/multi_split_view.dart'; +import 'package:apidash/widgets/widgets.dart'; + +void main() { + testWidgets('Testing for Dashboard Splitview', (tester) async { + await tester.pumpWidget( + const MaterialApp( + title: 'Dashboard Splitview', + home: Scaffold( + body: DashboardSplitView( + sidebarWidget: Column(children: [Text("Hello")]), + mainWidget: Column(children: [Text("World")]), + ), + ), + ), + ); + + expect(find.text("World"), findsOneWidget); + expect(find.text("Hello"), findsOneWidget); + expect(find.byType(MultiSplitViewTheme), findsOneWidget); + }); + //TODO: Divider not visible on flutter run. Investigate. +} diff --git a/test/widgets/splitview_drawer_test.dart b/test/widgets/splitview_drawer_test.dart new file mode 100644 index 00000000..da244097 --- /dev/null +++ b/test/widgets/splitview_drawer_test.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/splitview_drawer.dart'; + +void main() { + testWidgets('DrawerSplitView displays main components', (tester) async { + final scaffoldKey = GlobalKey(); + + await tester.pumpWidget( + MaterialApp( + home: DrawerSplitView( + scaffoldKey: scaffoldKey, + mainContent: const Text('Main Content'), + title: const Text('Title'), + leftDrawerContent: const Text('Left Drawer Content'), + rightDrawerContent: const Text('Right Drawer Content'), + ), + ), + ); + + // Verify the main content is displayed + expect(find.text('Main Content'), findsOneWidget); + + // Verify the title is displayed + expect(find.text('Title'), findsOneWidget); + + // Verify the left drawer content is not displayed initially + expect(find.text('Left Drawer Content'), findsNothing); + + // Verify the right drawer content is not displayed initially + expect(find.text('Right Drawer Content'), findsNothing); + }); + + testWidgets('DrawerSplitView opens left drawer', (tester) async { + final scaffoldKey = GlobalKey(); + + await tester.pumpWidget( + MaterialApp( + home: DrawerSplitView( + scaffoldKey: scaffoldKey, + mainContent: const Text('Main Content'), + title: const Text('Title'), + leftDrawerContent: const Text('Left Drawer Content'), + ), + ), + ); + + // Tap the leading icon to open the left drawer + await tester.tap(find.byIcon(Icons.format_list_bulleted_rounded)); + await tester.pumpAndSettle(); + + // Verify the left drawer content is displayed + expect(find.text('Left Drawer Content'), findsOneWidget); + }); + + testWidgets('DrawerSplitView opens right drawer', (tester) async { + final scaffoldKey = GlobalKey(); + + await tester.pumpWidget( + MaterialApp( + home: DrawerSplitView( + scaffoldKey: scaffoldKey, + mainContent: const Text('Main Content'), + title: const Text('Title'), + rightDrawerContent: const Text('Right Drawer Content'), + ), + ), + ); + + // Tap the right drawer icon to open the right drawer + await tester.tap(find.byIcon(Icons.arrow_forward)); + await tester.pumpAndSettle(); + + // Verify the right drawer content is displayed + expect(find.text('Right Drawer Content'), findsOneWidget); + }); + + testWidgets('DrawerSplitView displays actions', (tester) async { + final scaffoldKey = GlobalKey(); + + await tester.pumpWidget( + MaterialApp( + home: DrawerSplitView( + scaffoldKey: scaffoldKey, + mainContent: const Text('Main Content'), + title: const Text('Title'), + actions: [ + IconButton(icon: const Icon(Icons.search), onPressed: () {}) + ], + ), + ), + ); + + // Verify the action icon is displayed + expect(find.byIcon(Icons.search), findsOneWidget); + }); +} diff --git a/test/widgets/splitviews_test.dart b/test/widgets/splitview_equal_test.dart similarity index 60% rename from test/widgets/splitviews_test.dart rename to test/widgets/splitview_equal_test.dart index b8d074b1..c917a22b 100644 --- a/test/widgets/splitviews_test.dart +++ b/test/widgets/splitview_equal_test.dart @@ -4,23 +4,6 @@ import 'package:multi_split_view/multi_split_view.dart'; import 'package:apidash/widgets/widgets.dart'; void main() { - testWidgets('Testing for Dashboard Splitview', (tester) async { - await tester.pumpWidget( - const MaterialApp( - title: 'Dashboard Splitview', - home: Scaffold( - body: DashboardSplitView( - sidebarWidget: Column(children: [Text("Hello")]), - mainWidget: Column(children: [Text("World")]), - ), - ), - ), - ); - - expect(find.text("World"), findsOneWidget); - expect(find.text("Hello"), findsOneWidget); - expect(find.byType(MultiSplitViewTheme), findsOneWidget); - }); testWidgets('Testing for Equal SplitView', (tester) async { await tester.pumpWidget( const MaterialApp( diff --git a/test/widgets/tabs_test.dart b/test/widgets/tabs_test.dart new file mode 100644 index 00000000..7aaee00c --- /dev/null +++ b/test/widgets/tabs_test.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/tabs.dart'; + +void main() { + testWidgets('TabLabel shows indicator when showIndicator is true', + (tester) async { + const String labelText = 'URL Params'; + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: TabLabel( + text: labelText, + showIndicator: true, + ), + ), + ), + ); + + expect(find.text(labelText), findsOneWidget); + expect(find.byIcon(Icons.circle), findsOneWidget); + }); + + testWidgets('TabLabel does not show indicator when showIndicator is false', + (tester) async { + const String labelText = 'Request'; + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: TabLabel( + text: labelText, + showIndicator: false, + ), + ), + ), + ); + + expect(find.text(labelText), findsOneWidget); + expect(find.byIcon(Icons.circle), findsNothing); + }); +} From 654fef5db5a308e8fb34da1f021d67f96c50ed25 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Wed, 31 Jul 2024 23:39:34 +0530 Subject: [PATCH 20/71] wip: common_widgets tests --- pubspec.lock | 4 +- .../common_widgets/button_navbar_test.dart | 121 ++++++++++++++++++ .../env_trigger_options_test.dart | 120 +++++++++++++++++ .../envvar__indicator_test.dart | 0 .../common_widgets}/envvar_popover_test.dart | 1 - .../sidebar_save_button_test.dart | 67 ++++++++++ test/widgets/sidebar_save_button_test.dart | 24 ---- 7 files changed, 310 insertions(+), 27 deletions(-) create mode 100644 test/screens/common_widgets/button_navbar_test.dart create mode 100644 test/screens/common_widgets/env_trigger_options_test.dart rename test/{widgets => screens/common_widgets}/envvar__indicator_test.dart (100%) rename test/{widgets => screens/common_widgets}/envvar_popover_test.dart (96%) create mode 100644 test/screens/common_widgets/sidebar_save_button_test.dart delete mode 100644 test/widgets/sidebar_save_button_test.dart diff --git a/pubspec.lock b/pubspec.lock index 12247062..6778709a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -671,10 +671,10 @@ packages: dependency: "direct main" description: name: http - sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" http_multi_server: dependency: transitive description: diff --git a/test/screens/common_widgets/button_navbar_test.dart b/test/screens/common_widgets/button_navbar_test.dart new file mode 100644 index 00000000..4a19dddc --- /dev/null +++ b/test/screens/common_widgets/button_navbar_test.dart @@ -0,0 +1,121 @@ +import 'package:apidash/providers/ui_providers.dart'; +import 'package:apidash/screens/common_widgets/button_navbar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Testing NavbarButton shows label when showLabel is true', + (WidgetTester tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + mobileScaffoldKeyStateProvider + .overrideWith((ref) => GlobalKey()) + ], + child: const MaterialApp( + home: Scaffold( + body: NavbarButton( + railIdx: 0, + buttonIdx: 1, + selectedIcon: Icons.check, + icon: Icons.add, + label: 'Test Label', + showLabel: true, + ), + ), + ), + ), + ); + + expect(find.text('Test Label'), findsOneWidget); + }); + + testWidgets('Testing NavbarButton hides label when showLabel is false', + (WidgetTester tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + mobileScaffoldKeyStateProvider + .overrideWith((ref) => GlobalKey()) + ], + child: const MaterialApp( + home: Scaffold( + body: NavbarButton( + railIdx: 0, + buttonIdx: 1, + selectedIcon: Icons.check, + icon: Icons.add, + label: 'Test Label', + showLabel: false, + ), + ), + ), + ), + ); + + expect(find.text('Test Label'), findsNothing); + }); + + testWidgets('Testing NavbarButton label style with isSelected', + (WidgetTester tester) async { + const testKey = Key('navbar_button'); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + mobileScaffoldKeyStateProvider + .overrideWith((ref) => GlobalKey()) + ], + child: const MaterialApp( + home: Scaffold( + body: NavbarButton( + key: testKey, + railIdx: 1, + buttonIdx: 1, + selectedIcon: Icons.check, + icon: Icons.check_box_outline_blank, + label: 'Test Label', + ), + ), + ), + ), + ); + + Text label = tester.widget(find.text('Test Label')); + expect( + label.style?.color, + equals(Theme.of(tester.element(find.byKey(testKey))) + .colorScheme + .onSecondaryContainer)); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + mobileScaffoldKeyStateProvider + .overrideWith((ref) => GlobalKey()) + ], + child: const MaterialApp( + home: Scaffold( + body: NavbarButton( + key: testKey, + railIdx: 1, + buttonIdx: 2, + selectedIcon: Icons.check, + icon: Icons.check_box_outline_blank, + label: 'Test Label', + ), + ), + ), + ), + ); + + label = tester.widget(find.text('Test Label')); + expect( + label.style?.color, + equals(Theme.of(tester.element(find.byKey(testKey))) + .colorScheme + .onSurface + .withOpacity(0.65))); + }); +} diff --git a/test/screens/common_widgets/env_trigger_options_test.dart b/test/screens/common_widgets/env_trigger_options_test.dart new file mode 100644 index 00000000..d1e49747 --- /dev/null +++ b/test/screens/common_widgets/env_trigger_options_test.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/screens/common_widgets/env_trigger_options.dart'; + +void main() { + const envMap = { + kGlobalEnvironmentId: [ + EnvironmentVariableModel(key: 'key1', value: 'value1'), + EnvironmentVariableModel(key: 'key2', value: 'value2'), + ], + 'activeEnvId': [ + EnvironmentVariableModel(key: 'key2', value: 'value1'), + EnvironmentVariableModel(key: 'key3', value: 'value2'), + ], + }; + + const suggestions = [ + EnvironmentVariableSuggestion( + environmentId: 'activeEnvId', + variable: EnvironmentVariableModel(key: 'key2', value: 'value1'), + ), + EnvironmentVariableSuggestion( + environmentId: 'activeEnvId', + variable: EnvironmentVariableModel(key: 'key3', value: 'value2'), + ), + EnvironmentVariableSuggestion( + environmentId: kGlobalEnvironmentId, + variable: EnvironmentVariableModel(key: 'key1', value: 'value1'), + ), + EnvironmentVariableSuggestion( + environmentId: kGlobalEnvironmentId, + variable: EnvironmentVariableModel(key: 'key2', value: 'value2'), + ), + ]; + testWidgets( + 'EnvironmentAutocompleteOptions shows no suggestions when suggestions are empty', + (WidgetTester tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + availableEnvironmentVariablesStateProvider.overrideWith((ref) => {}), + activeEnvironmentIdStateProvider.overrideWith((ref) => null), + ], + child: MaterialApp( + home: Scaffold( + body: EnvironmentAutocompleteOptions( + query: 'test', + onSuggestionTap: (suggestion) {}, + ), + ), + ), + ), + ); + + expect(find.byType(SizedBox), findsOneWidget); + expect(find.byType(ClipRRect), findsNothing); + expect(find.byType(ListView), findsNothing); + }); + + testWidgets('EnvironmentAutocompleteOptions shows suggestions when available', + (WidgetTester tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + availableEnvironmentVariablesStateProvider + .overrideWith((ref) => envMap), + activeEnvironmentIdStateProvider.overrideWith((ref) => 'activeEnvId'), + ], + child: MaterialApp( + home: Scaffold( + body: EnvironmentAutocompleteOptions( + query: 'key', + onSuggestionTap: (suggestion) {}, + ), + ), + ), + ), + ); + + expect(find.byType(ClipRRect), findsOneWidget); + expect(find.byType(ListView), findsOneWidget); + expect(find.byType(ListTile), findsNWidgets(3)); + }); + + testWidgets( + 'EnvironmentAutocompleteOptions calls onSuggestionTap when a suggestion is tapped', + (WidgetTester tester) async { + EnvironmentVariableSuggestion? tappedSuggestion; + + await tester.pumpWidget( + ProviderScope( + overrides: [ + availableEnvironmentVariablesStateProvider + .overrideWith((ref) => envMap), + activeEnvironmentIdStateProvider.overrideWith((ref) => 'activeEnvId'), + ], + child: MaterialApp( + home: Scaffold( + body: EnvironmentAutocompleteOptions( + query: 'key', + onSuggestionTap: (suggestion) { + tappedSuggestion = suggestion; + }, + ), + ), + ), + ), + ); + + await tester.tap(find.byType(ListTile).first); + await tester.pump(); + + expect(tappedSuggestion, isNotNull); + expect(tappedSuggestion, equals(suggestions.first)); + }); +} diff --git a/test/widgets/envvar__indicator_test.dart b/test/screens/common_widgets/envvar__indicator_test.dart similarity index 100% rename from test/widgets/envvar__indicator_test.dart rename to test/screens/common_widgets/envvar__indicator_test.dart diff --git a/test/widgets/envvar_popover_test.dart b/test/screens/common_widgets/envvar_popover_test.dart similarity index 96% rename from test/widgets/envvar_popover_test.dart rename to test/screens/common_widgets/envvar_popover_test.dart index 8621a68a..d1eb268f 100644 --- a/test/widgets/envvar_popover_test.dart +++ b/test/screens/common_widgets/envvar_popover_test.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:apidash/consts.dart'; import 'package:apidash/models/models.dart'; import 'package:apidash/screens/common_widgets/envvar_indicator.dart'; import 'package:apidash/screens/common_widgets/envvar_popover.dart'; diff --git a/test/screens/common_widgets/sidebar_save_button_test.dart b/test/screens/common_widgets/sidebar_save_button_test.dart new file mode 100644 index 00000000..5764d079 --- /dev/null +++ b/test/screens/common_widgets/sidebar_save_button_test.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/providers/ui_providers.dart'; +import 'package:apidash/screens/common_widgets/sidebar_save_button.dart'; +import '../../test_consts.dart'; + +void main() { + group("Testing Save Button", () { + testWidgets('Testing for Save button enabled', (tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + saveDataStateProvider.overrideWith((ref) => false), + hasUnsavedChangesProvider.overrideWith((ref) => true), + ], + child: MaterialApp( + title: 'Save button', + theme: kThemeDataLight, + home: const Scaffold( + body: SaveButton(), + ), + ), + ), + ); + final icon = find.byIcon(Icons.save); + expect(icon, findsOneWidget); + + final saveButton = find.ancestor( + of: icon, + matching: find.byWidgetPredicate((widget) => widget is TextButton)); + expect(saveButton, findsOneWidget); + + final saveButtonWidget = tester.widget(saveButton); + expect(saveButtonWidget.enabled, true); + }); + + testWidgets('Testing for Save button disabled', (tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + saveDataStateProvider.overrideWith((ref) => false), + hasUnsavedChangesProvider.overrideWith((ref) => false), + ], + child: MaterialApp( + title: 'Save button', + theme: kThemeDataLight, + home: const Scaffold( + body: SaveButton(), + ), + ), + ), + ); + + final icon = find.byIcon(Icons.save); + expect(icon, findsOneWidget); + + final saveButton = find.ancestor( + of: icon, + matching: find.byWidgetPredicate((widget) => widget is TextButton)); + expect(saveButton, findsOneWidget); + + final saveButtonWidget = tester.widget(saveButton); + expect(saveButtonWidget.enabled, false); + }); + }); +} diff --git a/test/widgets/sidebar_save_button_test.dart b/test/widgets/sidebar_save_button_test.dart deleted file mode 100644 index 2ecb0cec..00000000 --- a/test/widgets/sidebar_save_button_test.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:apidash/screens/common_widgets/sidebar_save_button.dart'; -import '../test_consts.dart'; - -void main() { - testWidgets('Testing for Save button', (tester) async { - await tester.pumpWidget( - ProviderScope( - child: MaterialApp( - title: 'Save button', - theme: kThemeDataLight, - home: const Scaffold( - body: SaveButton(), - ), - ), - ), - ); - - expect(find.byIcon(Icons.save), findsOneWidget); - expect(find.text("Save"), findsOneWidget); - }); -} From df0e08ae2f83379dbf9701fce4423edf737903d2 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Fri, 2 Aug 2024 19:38:18 +0530 Subject: [PATCH 21/71] wip: env widget tests --- .../common_widgets/env_trigger_field.dart | 8 +-- .../common_widgets/env_trigger_options.dart | 4 +- pubspec.lock | 8 --- pubspec.yaml | 1 - .../env_regexp_span_builder_test.dart | 25 ++++++++ .../env_trigger_field_test.dart | 48 ++++++++++++++ .../env_trigger_options_test.dart | 12 ++-- test/widgets/dialog_about_test.dart | 42 ++++++++++++ test/widgets/field_cell_obscurable_test.dart | 64 +++++++++++++++++++ test/widgets/field_read_only_test.dart | 36 +++++++++++ 10 files changed, 227 insertions(+), 21 deletions(-) create mode 100644 test/screens/common_widgets/env_regexp_span_builder_test.dart create mode 100644 test/screens/common_widgets/env_trigger_field_test.dart create mode 100644 test/widgets/dialog_about_test.dart create mode 100644 test/widgets/field_cell_obscurable_test.dart create mode 100644 test/widgets/field_read_only_test.dart diff --git a/lib/screens/common_widgets/env_trigger_field.dart b/lib/screens/common_widgets/env_trigger_field.dart index 5c3096f0..dcdbde3c 100644 --- a/lib/screens/common_widgets/env_trigger_field.dart +++ b/lib/screens/common_widgets/env_trigger_field.dart @@ -26,10 +26,10 @@ class EnvironmentTriggerField extends StatefulWidget { @override State createState() => - _EnvironmentTriggerFieldState(); + EnvironmentTriggerFieldState(); } -class _EnvironmentTriggerFieldState extends State { +class EnvironmentTriggerFieldState extends State { final TextEditingController controller = TextEditingController(); final FocusNode focusNode = FocusNode(); @@ -71,7 +71,7 @@ class _EnvironmentTriggerFieldState extends State { triggerEnd: "}}", triggerOnlyAfterSpace: false, optionsViewBuilder: (context, autocompleteQuery, controller) { - return EnvironmentAutocompleteOptions( + return EnvironmentTriggerOptions( query: autocompleteQuery.query, onSuggestionTap: (suggestion) { final autocomplete = MultiTriggerAutocomplete.of(context); @@ -86,7 +86,7 @@ class _EnvironmentTriggerFieldState extends State { triggerEnd: "}}", triggerOnlyAfterSpace: false, optionsViewBuilder: (context, autocompleteQuery, controller) { - return EnvironmentAutocompleteOptions( + return EnvironmentTriggerOptions( query: autocompleteQuery.query, onSuggestionTap: (suggestion) { final autocomplete = MultiTriggerAutocomplete.of(context); diff --git a/lib/screens/common_widgets/env_trigger_options.dart b/lib/screens/common_widgets/env_trigger_options.dart index e6550503..04a21e7d 100644 --- a/lib/screens/common_widgets/env_trigger_options.dart +++ b/lib/screens/common_widgets/env_trigger_options.dart @@ -7,8 +7,8 @@ import 'package:apidash/utils/utils.dart'; import 'envvar_indicator.dart'; -class EnvironmentAutocompleteOptions extends ConsumerWidget { - const EnvironmentAutocompleteOptions({ +class EnvironmentTriggerOptions extends ConsumerWidget { + const EnvironmentTriggerOptions({ super.key, required this.query, required this.onSuggestionTap, diff --git a/pubspec.lock b/pubspec.lock index 6778709a..4e463339 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1383,14 +1383,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.0" - test_cov_console: - dependency: "direct dev" - description: - name: test_cov_console - sha256: "73519e8be3689d73f5cffb652c12c310acacf48379396d834da937094836e65e" - url: "https://pub.dev" - source: hosted - version: "0.2.2" textwrap: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 93053652..515fc0c9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -89,7 +89,6 @@ dev_dependencies: freezed: ^2.5.2 json_serializable: ^6.7.1 spot: ^0.13.0 - test_cov_console: ^0.2.2 flutter: uses-material-design: true diff --git a/test/screens/common_widgets/env_regexp_span_builder_test.dart b/test/screens/common_widgets/env_regexp_span_builder_test.dart new file mode 100644 index 00000000..7c7f64ea --- /dev/null +++ b/test/screens/common_widgets/env_regexp_span_builder_test.dart @@ -0,0 +1,25 @@ +import 'package:apidash/screens/common_widgets/env_regexp_span_builder.dart'; +import 'package:apidash/screens/common_widgets/envvar_span.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:extended_text_field/extended_text_field.dart'; +import 'package:flutter/widgets.dart'; + +void main() { + test('Testing RegExpSpanBuilder returns correct ExtendedWidgetSpan', () { + final regExpEnvText = RegExpEnvText(); + final match = RegExp(r'\{\{.*?\}\}').firstMatch('{{variable}}')!; + const start = 0; + + final span = regExpEnvText.finishText(start, match); + + expect(span, isA()); + final extendedWidgetSpan = span as ExtendedWidgetSpan; + expect(extendedWidgetSpan.actualText, '{{variable}}'); + expect(extendedWidgetSpan.start, start); + expect(extendedWidgetSpan.alignment, PlaceholderAlignment.middle); + expect(extendedWidgetSpan.child, isA()); + + final envVarSpan = extendedWidgetSpan.child as EnvVarSpan; + expect(envVarSpan.variableKey, 'variable'); + }); +} diff --git a/test/screens/common_widgets/env_trigger_field_test.dart b/test/screens/common_widgets/env_trigger_field_test.dart new file mode 100644 index 00000000..622f10ac --- /dev/null +++ b/test/screens/common_widgets/env_trigger_field_test.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:extended_text_field/extended_text_field.dart'; +import 'package:apidash/screens/common_widgets/env_trigger_field.dart'; + +void main() { + testWidgets('Testing EnvironmentTriggerField updates the controller text', + (WidgetTester tester) async { + final fieldKey = GlobalKey(); + const initialValue = 'initial'; + const updatedValue = 'updated'; + + await tester.pumpWidget( + Portal( + child: MaterialApp( + home: Scaffold( + body: EnvironmentTriggerField( + key: fieldKey, + keyId: 'testKey', + initialValue: initialValue, + ), + ), + ), + ), + ); + + Finder field = find.byType(ExtendedTextField); + expect(field, findsOneWidget); + expect(fieldKey.currentState!.controller.text, initialValue); + + await tester.pumpWidget( + Portal( + child: MaterialApp( + home: Scaffold( + body: EnvironmentTriggerField( + key: fieldKey, + keyId: 'testKey', + initialValue: updatedValue, + ), + ), + ), + ), + ); + + expect(fieldKey.currentState!.controller.text, updatedValue); + }); +} diff --git a/test/screens/common_widgets/env_trigger_options_test.dart b/test/screens/common_widgets/env_trigger_options_test.dart index d1e49747..0bbe5bf4 100644 --- a/test/screens/common_widgets/env_trigger_options_test.dart +++ b/test/screens/common_widgets/env_trigger_options_test.dart @@ -37,7 +37,7 @@ void main() { ), ]; testWidgets( - 'EnvironmentAutocompleteOptions shows no suggestions when suggestions are empty', + 'EnvironmentTriggerOptions shows no suggestions when suggestions are empty', (WidgetTester tester) async { await tester.pumpWidget( ProviderScope( @@ -47,7 +47,7 @@ void main() { ], child: MaterialApp( home: Scaffold( - body: EnvironmentAutocompleteOptions( + body: EnvironmentTriggerOptions( query: 'test', onSuggestionTap: (suggestion) {}, ), @@ -61,7 +61,7 @@ void main() { expect(find.byType(ListView), findsNothing); }); - testWidgets('EnvironmentAutocompleteOptions shows suggestions when available', + testWidgets('EnvironmentTriggerOptions shows suggestions when available', (WidgetTester tester) async { await tester.pumpWidget( ProviderScope( @@ -72,7 +72,7 @@ void main() { ], child: MaterialApp( home: Scaffold( - body: EnvironmentAutocompleteOptions( + body: EnvironmentTriggerOptions( query: 'key', onSuggestionTap: (suggestion) {}, ), @@ -87,7 +87,7 @@ void main() { }); testWidgets( - 'EnvironmentAutocompleteOptions calls onSuggestionTap when a suggestion is tapped', + 'EnvironmentTriggerOptions calls onSuggestionTap when a suggestion is tapped', (WidgetTester tester) async { EnvironmentVariableSuggestion? tappedSuggestion; @@ -100,7 +100,7 @@ void main() { ], child: MaterialApp( home: Scaffold( - body: EnvironmentAutocompleteOptions( + body: EnvironmentTriggerOptions( query: 'key', onSuggestionTap: (suggestion) { tappedSuggestion = suggestion; diff --git a/test/widgets/dialog_about_test.dart b/test/widgets/dialog_about_test.dart new file mode 100644 index 00000000..cd46e62d --- /dev/null +++ b/test/widgets/dialog_about_test.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/widgets.dart'; + +void main() { + testWidgets( + 'Testing showAboutAppDialog displays the dialog with IntroMessage and Close button', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (BuildContext context) { + return Scaffold( + body: Center( + child: ElevatedButton( + onPressed: () { + showAboutAppDialog(context); + }, + child: const Text('Show About Dialog'), + ), + ), + ); + }, + ), + ), + ); + + await tester.tap(find.text('Show About Dialog')); + await tester.pump(); + + expect(find.byType(AlertDialog), findsOneWidget); + + expect(find.byType(IntroMessage), findsOneWidget); + + expect(find.text('Close'), findsOneWidget); + + await tester.tap(find.text('Close')); + await tester.pump(); + + expect(find.byType(AlertDialog), findsNothing); + }); +} diff --git a/test/widgets/field_cell_obscurable_test.dart b/test/widgets/field_cell_obscurable_test.dart new file mode 100644 index 00000000..6bbf3c0e --- /dev/null +++ b/test/widgets/field_cell_obscurable_test.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/field_cell_obscurable.dart'; + +void main() { + testWidgets( + 'Testing ObscurableCellField toggles obscure text on icon button press', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: ObscurableCellField( + keyId: 'testKey', + initialValue: 'password', + hintText: 'Enter password', + ), + ), + ), + ); + + final iconButton = find.byType(IconButton); + expect(iconButton, findsOneWidget); + + Icon icon = tester.widget( + find.descendant(of: iconButton, matching: find.byType(Icon))); + expect(icon.icon, Icons.visibility); + + await tester.tap(iconButton); + await tester.pump(); + + icon = tester.widget( + find.descendant(of: iconButton, matching: find.byType(Icon))); + expect(icon.icon, Icons.visibility_off); + }, + ); + + testWidgets('ObscurableCellField calls onChanged when text is changed', + (WidgetTester tester) async { + String? changedValue; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: ObscurableCellField( + keyId: 'testKey', + initialValue: 'password', + hintText: 'Enter password', + onChanged: (value) { + changedValue = value; + }, + ), + ), + ), + ); + + final textField = find.byType(TextFormField); + expect(textField, findsOneWidget); + + await tester.enterText(textField, 'newpassword'); + await tester.pump(); + + expect(changedValue, 'newpassword'); + }); +} diff --git a/test/widgets/field_read_only_test.dart b/test/widgets/field_read_only_test.dart new file mode 100644 index 00000000..4c198246 --- /dev/null +++ b/test/widgets/field_read_only_test.dart @@ -0,0 +1,36 @@ +import 'package:apidash/widgets/field_read_only.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/consts.dart'; + +void main() { + testWidgets('Testing ReadOnlyTextField displays initial value and decoration', + (WidgetTester tester) async { + const testInitialValue = 'Test Value'; + const testDecoration = InputDecoration( + hintText: 'Test Hint', + isDense: true, + border: InputBorder.none, + contentPadding: kPv8, + ); + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: ReadOnlyTextField( + initialValue: testInitialValue, + decoration: testDecoration, + ), + ), + ), + ); + + expect(find.text(testInitialValue), findsOneWidget); + + final textField = tester.widget(find.byType(TextField)); + expect(textField.decoration?.hintText, testDecoration.hintText); + expect(textField.decoration?.isDense, testDecoration.isDense); + expect(textField.decoration?.border, testDecoration.border); + expect(textField.decoration?.contentPadding, testDecoration.contentPadding); + }); +} From 572aa018bf2b297f293df17db3a879a33203f861 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sat, 3 Aug 2024 17:33:18 +0530 Subject: [PATCH 22/71] test: history widgets --- test/widgets/button_form_data_file_test.dart | 71 ++++++++++++ test/widgets/button_group_filled_test.dart | 102 ++++++++++++++++++ test/widgets/button_save_download_test.dart | 42 ++------ test/widgets/card_history_request_test.dart | 47 ++++++++ ...ls.dart => card_request_details_test.dart} | 0 test/widgets/card_sidebar_history_test.dart | 97 +++++++++++++++++ test/widgets/popup_menu_history_test.dart | 44 ++++++++ test/widgets/splitview_drawer_test.dart | 18 +--- test/widgets/splitview_history_test.dart | 25 +++++ .../{tables_test.dart => table_map_test.dart} | 0 test/widgets/table_request_form_test.dart | 36 +++++++ test/widgets/table_request_test.dart | 32 ++++++ test/widgets/texts_test.dart | 22 ++++ 13 files changed, 490 insertions(+), 46 deletions(-) create mode 100644 test/widgets/button_form_data_file_test.dart create mode 100644 test/widgets/button_group_filled_test.dart create mode 100644 test/widgets/card_history_request_test.dart rename test/widgets/{card_request_details.dart => card_request_details_test.dart} (100%) create mode 100644 test/widgets/card_sidebar_history_test.dart create mode 100644 test/widgets/popup_menu_history_test.dart create mode 100644 test/widgets/splitview_history_test.dart rename test/widgets/{tables_test.dart => table_map_test.dart} (100%) create mode 100644 test/widgets/table_request_form_test.dart create mode 100644 test/widgets/table_request_test.dart diff --git a/test/widgets/button_form_data_file_test.dart b/test/widgets/button_form_data_file_test.dart new file mode 100644 index 00000000..81a5d3b2 --- /dev/null +++ b/test/widgets/button_form_data_file_test.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/widgets/button_form_data_file.dart'; + +void main() { + testWidgets('Testing FormDataFileButton for default label', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: FormDataFileButton(), + ), + ), + ); + + final icon = find.byIcon(Icons.snippet_folder_rounded); + Finder button = find.ancestor( + of: icon, + matching: find.byWidgetPredicate((widget) => widget is ElevatedButton)); + + expect(button, findsOneWidget); + expect(find.text(kLabelSelectFile), findsOneWidget); + }); + + testWidgets('Testing FormDataFileButton with provided label', + (WidgetTester tester) async { + const testValue = 'test_file.txt'; + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: FormDataFileButton(initialValue: testValue), + ), + ), + ); + + final icon = find.byIcon(Icons.snippet_folder_rounded); + Finder button = find.ancestor( + of: icon, + matching: find.byWidgetPredicate((widget) => widget is ElevatedButton)); + + expect(button, findsOneWidget); + expect(find.text(testValue), findsOneWidget); + }); + + testWidgets('Testing FormDataFileButton triggers onPressed callback', + (WidgetTester tester) async { + bool pressed = false; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FormDataFileButton( + onPressed: () { + pressed = true; + }, + ), + ), + ), + ); + + final icon = find.byIcon(Icons.snippet_folder_rounded); + Finder button = find.ancestor( + of: icon, + matching: find.byWidgetPredicate((widget) => widget is ElevatedButton)); + + await tester.tap(button); + await tester.pump(); + + expect(pressed, isTrue); + }); +} diff --git a/test/widgets/button_group_filled_test.dart b/test/widgets/button_group_filled_test.dart new file mode 100644 index 00000000..44fe9e6e --- /dev/null +++ b/test/widgets/button_group_filled_test.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/widgets/button_group_filled.dart'; + +void main() { + testWidgets('Testing FilledButtonGroup', (WidgetTester tester) async { + final buttons = [ + ButtonData( + icon: Icons.add, + label: 'Add', + tooltip: 'Add Item', + onPressed: () {}, + ), + ButtonData( + icon: Icons.remove, + label: 'Remove', + tooltip: 'Remove Item', + onPressed: () {}, + ), + ]; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FilledButtonGroup(buttons: buttons), + ), + ), + ); + + expect(find.byType(FilledButtonWidget), findsNWidgets(2)); + }); + + testWidgets('Testing FilledButtonWidget with label', + (WidgetTester tester) async { + final buttonData = ButtonData( + icon: Icons.add, + label: 'Add', + tooltip: 'Add Item', + onPressed: () {}, + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FilledButtonWidget(buttonData: buttonData, showLabel: true), + ), + ), + ); + + expect(find.byIcon(Icons.add), findsOneWidget); + expect(find.text('Add'), findsOneWidget); + }); + + testWidgets('Testing FilledButtonWidget without label', + (WidgetTester tester) async { + final buttonData = ButtonData( + icon: Icons.add, + label: 'Add', + tooltip: 'Add Item', + onPressed: () {}, + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FilledButtonWidget(buttonData: buttonData, showLabel: false), + ), + ), + ); + + expect(find.byIcon(Icons.add), findsOneWidget); + expect(find.text('Add'), findsNothing); + }); + + testWidgets('Testing FilledButtonWidget with onPressed callback', + (WidgetTester tester) async { + bool pressed = false; + + final buttonData = ButtonData( + icon: Icons.add, + label: 'Add', + tooltip: 'Add Item', + onPressed: () { + pressed = true; + }, + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FilledButtonWidget(buttonData: buttonData, showLabel: true), + ), + ), + ); + + await tester.tap(find.byType(FilledButtonWidget)); + await tester.pump(); + + expect(pressed, isTrue); + }); +} diff --git a/test/widgets/button_save_download_test.dart b/test/widgets/button_save_download_test.dart index 288849a8..5cbd6a95 100644 --- a/test/widgets/button_save_download_test.dart +++ b/test/widgets/button_save_download_test.dart @@ -22,23 +22,12 @@ void main() { expect(icon, findsOneWidget); Finder button; - if (tester.any(find.ancestor( + expect(find.text(kLabelDownload), findsOneWidget); + button = find.ancestor( of: icon, - matching: find.byWidgetPredicate((widget) => widget is TextButton)))) { - expect(find.text(kLabelDownload), findsOneWidget); - button = find.ancestor( - of: icon, - matching: find.byWidgetPredicate((widget) => widget is TextButton)); - expect(button, findsOneWidget); - expect(tester.widget(button).enabled, isFalse); - } else if (tester - .any(find.ancestor(of: icon, matching: find.byType(IconButton)))) { - button = find.byType(IconButton); - expect(button, findsOneWidget); - expect(tester.widget(button).onPressed == null, isFalse); - } else { - fail('No TextButton or IconButton found'); - } + matching: find.byWidgetPredicate((widget) => widget is TextButton)); + expect(button, findsOneWidget); + expect(tester.widget(button).enabled, isFalse); }); testWidgets('Testing for Save in Downloads button 2', (tester) async { @@ -48,6 +37,7 @@ void main() { theme: kThemeDataLight, home: Scaffold( body: SaveInDownloadsButton( + showLabel: false, content: Uint8List.fromList([1]), ), ), @@ -58,22 +48,8 @@ void main() { expect(icon, findsOneWidget); Finder button; - if (tester.any(find.ancestor( - of: icon, - matching: find.byWidgetPredicate((widget) => widget is TextButton)))) { - expect(find.text(kLabelDownload), findsOneWidget); - button = find.ancestor( - of: icon, - matching: find.byWidgetPredicate((widget) => widget is TextButton)); - expect(button, findsOneWidget); - expect(tester.widget(button).enabled, isTrue); - } else if (tester - .any(find.ancestor(of: icon, matching: find.byType(IconButton)))) { - button = find.byType(IconButton); - expect(button, findsOneWidget); - expect(tester.widget(button).onPressed == null, isTrue); - } else { - fail('No TextButton or IconButton found'); - } + button = find.byType(IconButton); + expect(button, findsOneWidget); + expect(tester.widget(button).onPressed == null, isFalse); }); } diff --git a/test/widgets/card_history_request_test.dart b/test/widgets/card_history_request_test.dart new file mode 100644 index 00000000..becc9d7f --- /dev/null +++ b/test/widgets/card_history_request_test.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/widgets/texts.dart'; +import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/widgets/card_history_request.dart'; + +void main() { + testWidgets( + 'HistoryRequestCard displays correct information and handles onTap', + (WidgetTester tester) async { + final mockModel = HistoryMetaModel( + historyId: 'historyId', + requestId: 'requestId', + url: 'https://api.apidash.dev', + method: HTTPVerb.get, + timeStamp: DateTime.now(), + responseStatus: 200, + ); + + bool wasTapped = false; + void mockOnTap() { + wasTapped = true; + } + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: HistoryRequestCard( + id: '1', + model: mockModel, + isSelected: true, + onTap: mockOnTap, + ), + ), + ), + ); + + expect(find.text(humanizeTime(mockModel.timeStamp)), findsOneWidget); + + expect(find.byType(StatusCode), findsOneWidget); + + await tester.tap(find.byType(InkWell)); + expect(wasTapped, isTrue); + }); +} diff --git a/test/widgets/card_request_details.dart b/test/widgets/card_request_details_test.dart similarity index 100% rename from test/widgets/card_request_details.dart rename to test/widgets/card_request_details_test.dart diff --git a/test/widgets/card_sidebar_history_test.dart b/test/widgets/card_sidebar_history_test.dart new file mode 100644 index 00000000..472831df --- /dev/null +++ b/test/widgets/card_sidebar_history_test.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/models/models.dart'; + +import '../test_consts.dart'; + +void main() { + final List sampleModels = [ + HistoryMetaModel( + historyId: 'historyId', + requestId: 'requestId', + url: 'https://api.apidash.dev', + method: HTTPVerb.get, + timeStamp: DateTime.now(), + responseStatus: 200, + ) + ]; + testWidgets('Testing Sidebar History Card', (tester) async { + dynamic changedValue; + + await tester.pumpWidget( + MaterialApp( + title: 'Sidebar History Card', + theme: kThemeDataLight, + home: Scaffold( + body: ListView( + children: [ + SidebarHistoryCard( + id: '1', + models: sampleModels, + method: HTTPVerb.get, + onTap: () { + changedValue = 'Tapped'; + }, + ), + ], + ), + ), + ), + ); + + expect(find.byType(InkWell), findsOneWidget); + + expect(find.text('https://api.apidash.dev'), findsOneWidget); + expect(find.widgetWithText(SizedBox, 'https://api.apidash.dev'), + findsOneWidget); + expect( + find.widgetWithText(Card, 'https://api.apidash.dev'), findsOneWidget); + await tester.pumpAndSettle(); + var tappable = find.widgetWithText(Card, 'https://api.apidash.dev'); + await tester.tap(tappable); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(changedValue, 'Tapped'); + await tester.pumpAndSettle(); + }); + + testWidgets('Testing Sidebar History Card dark mode', (tester) async { + dynamic changedValue; + + await tester.pumpWidget( + MaterialApp( + title: 'Sidebar History Card', + theme: kThemeDataDark, + home: Scaffold( + body: ListView( + children: [ + SidebarHistoryCard( + id: '1', + models: sampleModels, + method: HTTPVerb.get, + onTap: () { + changedValue = 'Tapped'; + }, + ), + ], + ), + ), + ), + ); + + expect(find.byType(InkWell), findsOneWidget); + + expect(find.text('https://api.apidash.dev'), findsOneWidget); + expect(find.widgetWithText(SizedBox, 'https://api.apidash.dev'), + findsOneWidget); + expect( + find.widgetWithText(Card, 'https://api.apidash.dev'), findsOneWidget); + await tester.pumpAndSettle(); + var tappable = find.widgetWithText(Card, 'https://api.apidash.dev'); + await tester.tap(tappable); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(changedValue, 'Tapped'); + await tester.pumpAndSettle(); + }); +} diff --git a/test/widgets/popup_menu_history_test.dart b/test/widgets/popup_menu_history_test.dart new file mode 100644 index 00000000..97ffae4f --- /dev/null +++ b/test/widgets/popup_menu_history_test.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/consts.dart'; + +void main() { + testWidgets('Testing HistoryRetentionPopupMenu widget', + (WidgetTester tester) async { + const historyPeriod1 = HistoryRetentionPeriod.oneMonth; + const historyPeriod2 = HistoryRetentionPeriod.threeMonths; + const HistoryRetentionPeriod initialValue = HistoryRetentionPeriod.oneWeek; + HistoryRetentionPeriod? selectedValue; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: HistoryRetentionPopupMenu( + value: initialValue, + onChanged: (value) { + selectedValue = value; + }, + items: const [historyPeriod1, historyPeriod2], + ), + ), + ), + ); + + expect(find.byType(HistoryRetentionPopupMenu), findsOneWidget); + + expect(find.text(initialValue.label), findsOneWidget); + + await tester.tap(find.byType(HistoryRetentionPopupMenu)); + await tester.pumpAndSettle(); + + for (var item in [historyPeriod1, historyPeriod2]) { + expect(find.text(item.label), findsOneWidget); + } + + await tester.tap(find.text(historyPeriod2.label)); + await tester.pumpAndSettle(); + + expect(selectedValue, historyPeriod2); + }); +} diff --git a/test/widgets/splitview_drawer_test.dart b/test/widgets/splitview_drawer_test.dart index da244097..398ee225 100644 --- a/test/widgets/splitview_drawer_test.dart +++ b/test/widgets/splitview_drawer_test.dart @@ -3,7 +3,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:apidash/widgets/splitview_drawer.dart'; void main() { - testWidgets('DrawerSplitView displays main components', (tester) async { + testWidgets('Testing DrawerSplitView displays main components', + (tester) async { final scaffoldKey = GlobalKey(); await tester.pumpWidget( @@ -18,20 +19,16 @@ void main() { ), ); - // Verify the main content is displayed expect(find.text('Main Content'), findsOneWidget); - // Verify the title is displayed expect(find.text('Title'), findsOneWidget); - // Verify the left drawer content is not displayed initially expect(find.text('Left Drawer Content'), findsNothing); - // Verify the right drawer content is not displayed initially expect(find.text('Right Drawer Content'), findsNothing); }); - testWidgets('DrawerSplitView opens left drawer', (tester) async { + testWidgets('Testing DrawerSplitView opens left drawer', (tester) async { final scaffoldKey = GlobalKey(); await tester.pumpWidget( @@ -45,15 +42,13 @@ void main() { ), ); - // Tap the leading icon to open the left drawer await tester.tap(find.byIcon(Icons.format_list_bulleted_rounded)); await tester.pumpAndSettle(); - // Verify the left drawer content is displayed expect(find.text('Left Drawer Content'), findsOneWidget); }); - testWidgets('DrawerSplitView opens right drawer', (tester) async { + testWidgets('Testing DrawerSplitView opens right drawer', (tester) async { final scaffoldKey = GlobalKey(); await tester.pumpWidget( @@ -67,15 +62,13 @@ void main() { ), ); - // Tap the right drawer icon to open the right drawer await tester.tap(find.byIcon(Icons.arrow_forward)); await tester.pumpAndSettle(); - // Verify the right drawer content is displayed expect(find.text('Right Drawer Content'), findsOneWidget); }); - testWidgets('DrawerSplitView displays actions', (tester) async { + testWidgets('Testing DrawerSplitView displays actions', (tester) async { final scaffoldKey = GlobalKey(); await tester.pumpWidget( @@ -91,7 +84,6 @@ void main() { ), ); - // Verify the action icon is displayed expect(find.byIcon(Icons.search), findsOneWidget); }); } diff --git a/test/widgets/splitview_history_test.dart b/test/widgets/splitview_history_test.dart new file mode 100644 index 00000000..b96d881b --- /dev/null +++ b/test/widgets/splitview_history_test.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:multi_split_view/multi_split_view.dart'; +import 'package:apidash/widgets/widgets.dart'; + +void main() { + testWidgets('Testing for History Splitview', (tester) async { + await tester.pumpWidget( + const MaterialApp( + title: 'History Splitview', + home: Scaffold( + body: HistorySplitView( + sidebarWidget: Column(children: [Text("Pane")]), + mainWidget: Column(children: [Text("Viewer")]), + ), + ), + ), + ); + + expect(find.text("Pane"), findsOneWidget); + expect(find.text("Viewer"), findsOneWidget); + expect(find.byType(MultiSplitViewTheme), findsOneWidget); + }); + //TODO: Divider not visible on flutter run. Investigate. +} diff --git a/test/widgets/tables_test.dart b/test/widgets/table_map_test.dart similarity index 100% rename from test/widgets/tables_test.dart rename to test/widgets/table_map_test.dart diff --git a/test/widgets/table_request_form_test.dart b/test/widgets/table_request_form_test.dart new file mode 100644 index 00000000..a1c67424 --- /dev/null +++ b/test/widgets/table_request_form_test.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:data_table_2/data_table_2.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/consts.dart'; + +void main() { + testWidgets('Testing RequestFormDataTable', (WidgetTester tester) async { + const List sampleData = [ + FormDataModel(name: 'Key1', value: 'Value1', type: FormDataType.file), + FormDataModel(name: 'Key2', value: 'Value2', type: FormDataType.text), + ]; + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: RequestFormDataTable( + rows: sampleData, + keyName: 'Key', + valueName: 'Value', + ), + ), + ), + ); + + expect(find.byType(DataTable2), findsOneWidget); + expect(find.byType(ReadOnlyTextField), findsNWidgets(3)); + expect(find.byType(FormDataFileButton), findsOneWidget); + + expect(find.text('Key1'), findsOneWidget); + expect(find.text('Value1'), findsOneWidget); + expect(find.text('Key2'), findsOneWidget); + expect(find.text('Value2'), findsOneWidget); + }); +} diff --git a/test/widgets/table_request_test.dart b/test/widgets/table_request_test.dart new file mode 100644 index 00000000..6e8c2c9f --- /dev/null +++ b/test/widgets/table_request_test.dart @@ -0,0 +1,32 @@ +import 'package:apidash/widgets/widgets.dart'; +import 'package:data_table_2/data_table_2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Testing RequestDataTable', (WidgetTester tester) async { + final Map sampleData = { + 'Key1': 'Value1', + 'Key2': 'Value2', + }; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: RequestDataTable( + rows: sampleData, + keyName: 'Key', + valueName: 'Value', + ), + ), + ), + ); + expect(find.byType(DataTable2), findsOneWidget); + expect(find.byType(ReadOnlyTextField), findsNWidgets(4)); + + expect(find.text('Key1'), findsOneWidget); + expect(find.text('Value1'), findsOneWidget); + expect(find.text('Key2'), findsOneWidget); + expect(find.text('Value2'), findsOneWidget); + }); +} diff --git a/test/widgets/texts_test.dart b/test/widgets/texts_test.dart index e4bb4f5b..8fcdfa30 100644 --- a/test/widgets/texts_test.dart +++ b/test/widgets/texts_test.dart @@ -24,6 +24,7 @@ void main() { widget is Text && widget.style!.color == kColorHttpMethodGet); expect(getTextWithColor, findsOneWidget); }); + testWidgets('Testing when method is DELETE', (tester) async { var methodDel = HTTPVerb.delete; await tester.pumpWidget( @@ -44,4 +45,25 @@ void main() { (widget) => widget is Text && widget.style!.color == colDelDarkMode); expect(delTextWithColor, findsOneWidget); }); + + testWidgets('Testing StatusCode', (WidgetTester tester) async { + const int testStatusCode = 200; + const TextStyle testStyle = TextStyle(fontSize: 20); + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: StatusCode( + statusCode: testStatusCode, + style: testStyle, + ), + ), + ), + ); + + Finder code = find.text(testStatusCode.toString()); + expect(code, findsOneWidget); + final Text textWidget = tester.widget(code); + expect(textWidget.style?.fontSize, testStyle.fontSize); + }); } From f7d91c5b4353e00d9b8020fd0a4f6eec07fb4b07 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sat, 3 Aug 2024 17:33:27 +0530 Subject: [PATCH 23/71] test: history models --- lib/widgets/button_group_filled.dart | 62 ++++++++++++--------- test/models/history_models.dart | 75 ++++++++++++++++++++++++++ test/models/history_models_test.dart | 80 ++++++++++++++++++++++++++++ test/models/http_request_models.dart | 19 +++++++ 4 files changed, 210 insertions(+), 26 deletions(-) create mode 100644 test/models/history_models.dart create mode 100644 test/models/history_models_test.dart diff --git a/lib/widgets/button_group_filled.dart b/lib/widgets/button_group_filled.dart index 24ae622d..bfd57759 100644 --- a/lib/widgets/button_group_filled.dart +++ b/lib/widgets/button_group_filled.dart @@ -6,7 +6,42 @@ class FilledButtonGroup extends StatelessWidget { final List buttons; - Widget buildButton(ButtonData buttonData, {bool showLabel = true}) { + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + final showLabel = constraints.maxWidth > buttons.length * 110; + List buttonWidgets = buttons + .map((button) => + FilledButtonWidget(buttonData: button, showLabel: showLabel)) + .toList(); + + List buttonsWithSpacers = []; + for (int i = 0; i < buttonWidgets.length; i++) { + buttonsWithSpacers.add(buttonWidgets[i]); + if (i < buttonWidgets.length - 1) { + buttonsWithSpacers.add(kHSpacer2); + } + } + return ClipRRect( + borderRadius: kBorderRadius20, + child: Row( + mainAxisSize: MainAxisSize.min, + children: buttonsWithSpacers, + ), + ); + }); + } +} + +class FilledButtonWidget extends StatelessWidget { + const FilledButtonWidget( + {super.key, required this.buttonData, this.showLabel = true}); + + final ButtonData buttonData; + final bool showLabel; + + @override + Widget build(BuildContext context) { final icon = Icon(buttonData.icon, size: 20); final label = Text( buttonData.label, @@ -32,29 +67,4 @@ class FilledButtonGroup extends StatelessWidget { ), ); } - - @override - Widget build(BuildContext context) { - return LayoutBuilder(builder: (context, constraints) { - final showLabel = constraints.maxWidth > buttons.length * 110; - List buttonWidgets = buttons - .map((button) => buildButton(button, showLabel: showLabel)) - .toList(); - - List buttonsWithSpacers = []; - for (int i = 0; i < buttonWidgets.length; i++) { - buttonsWithSpacers.add(buttonWidgets[i]); - if (i < buttonWidgets.length - 1) { - buttonsWithSpacers.add(kHSpacer2); - } - } - return ClipRRect( - borderRadius: kBorderRadius20, - child: Row( - mainAxisSize: MainAxisSize.min, - children: buttonsWithSpacers, - ), - ); - }); - } } diff --git a/test/models/history_models.dart b/test/models/history_models.dart new file mode 100644 index 00000000..13d1603f --- /dev/null +++ b/test/models/history_models.dart @@ -0,0 +1,75 @@ +import 'package:apidash/consts.dart'; +import 'package:apidash/models/models.dart' + show HistoryMetaModel, HistoryRequestModel; + +import 'http_request_models.dart'; +import 'http_response_models.dart'; + +/// Basic History Meta model 1 +final historyMetaModel1 = HistoryMetaModel( + historyId: 'historyId1', + requestId: 'requestId1', + url: 'https://api.apidash.dev/humanize/social', + method: HTTPVerb.get, + timeStamp: DateTime(2024, 1, 1), + responseStatus: 200, +); + +/// Basic History Request model 1 +final historyRequestModel1 = HistoryRequestModel( + historyId: 'historyId1', + metaData: historyMetaModel1, + httpRequestModel: httpRequestModelGet4, + httpResponseModel: responseModel, +); + +final historyMetaModel2 = HistoryMetaModel( + historyId: 'historyId2', + requestId: 'requestId2', + url: 'https://api.apidash.dev/case/lower', + method: HTTPVerb.post, + timeStamp: DateTime(2024, 1, 1), + responseStatus: 200, +); + +final historyRequestModel2 = HistoryRequestModel( + historyId: 'historyId2', + metaData: historyMetaModel2, + httpRequestModel: httpRequestModelPost10, + httpResponseModel: responseModel, +); + +/// JSONs +final Map historyMetaModelJson1 = { + "historyId": "historyId1", + "requestId": "requestId1", + "name": "", + "url": "https://api.apidash.dev/humanize/social", + "method": "get", + "timeStamp": '2024-01-01T00:00:00.000', + "responseStatus": 200, +}; + +final Map historyRequestModelJson1 = { + "historyId": "historyId1", + "metaData": historyMetaModelJson1, + "httpRequestModel": httpRequestModelGet4Json, + "httpResponseModel": responseModelJson, +}; + +final Map historyMetaModelJson2 = { + "historyId": "historyId2", + "requestId": "requestId2", + "name": "", + "url": "https://api.apidash.dev/case/lower", + "method": "post", + "timeStamp": '2024-01-01T00:00:00.000', + "responseStatus": 200, +}; + +final Map historyRequestModelJson2 = { + "historyId": "historyId2", + "metaData": historyMetaModelJson2, + "httpRequestModel": httpRequestModelPost10Json, + "httpResponseModel": responseModelJson, +}; diff --git a/test/models/history_models_test.dart b/test/models/history_models_test.dart new file mode 100644 index 00000000..7f2ae5aa --- /dev/null +++ b/test/models/history_models_test.dart @@ -0,0 +1,80 @@ +import 'package:test/test.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/consts.dart'; + +import 'history_models.dart'; +import 'http_request_models.dart'; +import 'http_response_models.dart'; + +void main() { + group('Testing History Meta Models', () { + test("Testing HistoryMetaModel copyWith", () { + var historyMetaModel = historyMetaModel1; + final historyMetaModelcopyWith = historyMetaModel.copyWith( + url: 'https://api.apidash.dev/humanize/social', + ); + expect(historyMetaModelcopyWith.url, + 'https://api.apidash.dev/humanize/social'); + // original model unchanged + expect(historyMetaModel.url, 'https://api.apidash.dev/humanize/social'); + }); + + test("Testing HistoryMetaModel toJson", () { + var historyMetaModel = historyMetaModel1; + expect(historyMetaModel.toJson(), historyMetaModelJson1); + }); + + test("Testing HistoryMetaModel fromJson", () { + var historyMetaModel = historyMetaModel1; + final modelFromJson = HistoryMetaModel.fromJson(historyMetaModelJson1); + expect(modelFromJson, historyMetaModel); + expect(modelFromJson.timeStamp, DateTime(2024, 1, 1)); + expect(modelFromJson.responseStatus, 200); + }); + + test("Testing HistoryMetaModel getters", () { + var historyMetaModel = historyMetaModel1; + expect(historyMetaModel.historyId, 'historyId1'); + expect(historyMetaModel.requestId, 'requestId1'); + expect(historyMetaModel.url, 'https://api.apidash.dev/humanize/social'); + expect(historyMetaModel.method, HTTPVerb.get); + expect(historyMetaModel.timeStamp, DateTime(2024, 1, 1)); + expect(historyMetaModel.responseStatus, 200); + }); + }); + + group('Testing History Request Models', () { + test("Testing HistoryRequestModel copyWith", () { + var historyRequestModel = historyRequestModel1; + final historyRequestModelcopyWith = historyRequestModel.copyWith( + metaData: historyMetaModel2, + ); + expect(historyRequestModelcopyWith.metaData, historyMetaModel2); + // original model unchanged + expect(historyRequestModel.metaData, historyMetaModel1); + }); + + test("Testing HistoryRequestModel toJson", () { + var historyRequestModel = historyRequestModel1; + expect(historyRequestModel.toJson(), historyRequestModelJson1); + }); + + test("Testing HistoryRequestModel fromJson", () { + var historyRequestModel = historyRequestModel1; + final modelFromJson = + HistoryRequestModel.fromJson(historyRequestModelJson1); + expect(modelFromJson, historyRequestModel); + expect(modelFromJson.metaData, historyMetaModel1); + expect(modelFromJson.httpRequestModel, httpRequestModelGet4); + expect(modelFromJson.httpResponseModel, responseModel); + }); + + test("Testing HistoryRequestModel getters", () { + var historyRequestModel = historyRequestModel1; + expect(historyRequestModel.historyId, 'historyId1'); + expect(historyRequestModel.metaData, historyMetaModel1); + expect(historyRequestModel.httpRequestModel, httpRequestModelGet4); + expect(historyRequestModel.httpResponseModel, responseModel); + }); + }); +} diff --git a/test/models/http_request_models.dart b/test/models/http_request_models.dart index a7dd9ca8..a9eeed3f 100644 --- a/test/models/http_request_models.dart +++ b/test/models/http_request_models.dart @@ -374,6 +374,25 @@ const httpRequestModelDelete2 = HttpRequestModel( ); // JSONs + +const httpRequestModelGet4Json = { + "method": 'get', + "url": 'https://api.apidash.dev/humanize/social', + "headers": null, + "params": [ + {'name': 'num', 'value': '8700000'}, + {'name': 'digits', 'value': '3'}, + {'name': 'system', 'value': 'SS'}, + {'name': 'add_space', 'value': 'true'}, + {'name': 'trailing_zeros', 'value': 'true'} + ], + "isHeaderEnabledList": null, + "isParamEnabledList": null, + "bodyContentType": "json", + "body": null, + "formData": null +}; + const httpRequestModelPost10Json = { "method": 'post', "url": 'https://api.apidash.dev/case/lower', From 85468ac55ee36745c5f4a9f59bcd749e64a1e9a8 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sun, 4 Aug 2024 02:04:35 +0530 Subject: [PATCH 24/71] test: dialog tests --- lib/widgets/dialog_rename.dart | 113 ++++++++---- .../his_response_pane_test.dart | 56 ++++++ .../history_widgets/his_url_card_test.dart | 51 +++++ .../dialog_history_retention_test.dart | 150 +++++++++++++++ test/widgets/dialog_rename_test.dart | 146 +++++++++++++++ test/widgets/dropdown_codegen_test.dart | 49 +++++ test/widgets/dropdown_content_type_test.dart | 49 +++++ test/widgets/dropdown_formdata_test.dart | 49 +++++ test/widgets/dropdowns_http_method_test.dart | 48 +++++ test/widgets/dropdowns_test.dart | 174 ------------------ 10 files changed, 673 insertions(+), 212 deletions(-) create mode 100644 test/screens/history/history_widgets/his_response_pane_test.dart create mode 100644 test/screens/history/history_widgets/his_url_card_test.dart create mode 100644 test/widgets/dialog_history_retention_test.dart create mode 100644 test/widgets/dialog_rename_test.dart create mode 100644 test/widgets/dropdown_codegen_test.dart create mode 100644 test/widgets/dropdown_content_type_test.dart create mode 100644 test/widgets/dropdown_formdata_test.dart create mode 100644 test/widgets/dropdowns_http_method_test.dart delete mode 100644 test/widgets/dropdowns_test.dart diff --git a/lib/widgets/dialog_rename.dart b/lib/widgets/dialog_rename.dart index 4b78e987..ee2f9f77 100644 --- a/lib/widgets/dialog_rename.dart +++ b/lib/widgets/dialog_rename.dart @@ -10,44 +10,81 @@ showRenameDialog( showDialog( context: context, builder: (context) { - final controller = TextEditingController(text: name ?? ""); - controller.selection = - TextSelection(baseOffset: 0, extentOffset: controller.text.length); - return AlertDialog( - icon: const Icon(Icons.edit_rounded), - iconColor: Theme.of(context).colorScheme.primary, - title: Text(dialogTitle), - titleTextStyle: Theme.of(context).textTheme.titleLarge, - content: Container( - padding: kPt20, - width: 300, - child: TextField( - autofocus: true, - controller: controller, - decoration: const InputDecoration( - hintText: "Enter new name", - border: OutlineInputBorder( - borderRadius: kBorderRadius12, - )), - ), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: const Text('Cancel')), - TextButton( - onPressed: () { - final val = controller.text.trim(); - onRename(val); - Navigator.pop(context); - Future.delayed(const Duration(milliseconds: 100), () { - controller.dispose(); - }); - }, - child: const Text('Ok')), - ], + return RenameDialogContent( + dialogTitle: dialogTitle, + onRename: onRename, + name: name, ); }); } + +class RenameDialogContent extends StatefulWidget { + const RenameDialogContent({ + super.key, + required this.dialogTitle, + required this.onRename, + this.name, + }); + + final String dialogTitle; + final Function(String) onRename; + final String? name; + + @override + State createState() => _RenameDialogContentState(); +} + +class _RenameDialogContentState extends State { + late TextEditingController controller; + + @override + void initState() { + super.initState(); + controller = TextEditingController(text: widget.name ?? ""); + controller.selection = + TextSelection(baseOffset: 0, extentOffset: controller.text.length); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + icon: const Icon(Icons.edit_rounded), + iconColor: Theme.of(context).colorScheme.primary, + title: Text(widget.dialogTitle), + titleTextStyle: Theme.of(context).textTheme.titleLarge, + content: Container( + padding: kPt20, + width: 300, + child: TextField( + autofocus: true, + controller: controller, + decoration: const InputDecoration( + hintText: "Enter new name", + border: OutlineInputBorder( + borderRadius: kBorderRadius12, + )), + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('Cancel')), + TextButton( + onPressed: () { + final val = controller.text.trim(); + widget.onRename(val); + Navigator.pop(context); + }, + child: const Text('Ok')), + ], + ); + } +} diff --git a/test/screens/history/history_widgets/his_response_pane_test.dart b/test/screens/history/history_widgets/his_response_pane_test.dart new file mode 100644 index 00000000..f45aadda --- /dev/null +++ b/test/screens/history/history_widgets/his_response_pane_test.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/response_widgets.dart'; +import 'package:apidash/screens/history/history_widgets/his_response_pane.dart'; + +import '../../../models/history_models.dart'; + +void main() { + group('HistoryResponsePane Widget Tests', () { + testWidgets('displays "No Request Selected" when no request is selected', + (tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + selectedHistoryIdStateProvider.overrideWith((ref) => null), + selectedHistoryRequestModelProvider.overrideWith((ref) => null), + ], + child: const MaterialApp( + home: Scaffold( + body: HistoryResponsePane(), + ), + ), + ), + ); + + expect(find.text("No Request Selected"), findsOneWidget); + }); + + testWidgets( + 'displays ResponsePaneHeader and ResponseTabView when a request is selected', + (tester) async { + final historyRequestModel = historyRequestModel1; + + await tester.pumpWidget( + ProviderScope( + overrides: [ + selectedHistoryIdStateProvider + .overrideWith((ref) => historyRequestModel.historyId), + selectedHistoryRequestModelProvider + .overrideWith((ref) => historyRequestModel), + ], + child: const MaterialApp( + home: Scaffold( + body: HistoryResponsePane(), + ), + ), + ), + ); + + expect(find.byType(ResponsePaneHeader), findsOneWidget); + expect(find.byType(ResponseTabView), findsOneWidget); + }); + }); +} diff --git a/test/screens/history/history_widgets/his_url_card_test.dart b/test/screens/history/history_widgets/his_url_card_test.dart new file mode 100644 index 00000000..6723fefb --- /dev/null +++ b/test/screens/history/history_widgets/his_url_card_test.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/screens/history/history_widgets/his_url_card.dart'; + +import '../../../models/history_models.dart'; + +void main() { + group('Testing HistoryURLCard', () { + final historyRequestModel = historyRequestModel1; + + testWidgets('Testing with given HistoryRequestModel', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: HistoryURLCard(historyRequestModel: historyRequestModel), + ), + ), + ); + + expect(find.byType(HistoryURLCard), findsOneWidget); + }); + + testWidgets('Testing if displays correct request method', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: HistoryURLCard(historyRequestModel: historyRequestModel), + ), + ), + ); + + expect( + find.text( + historyRequestModel.httpRequestModel.method.name.toUpperCase()), + findsOneWidget); + }); + + testWidgets('Testing if displays correct URL', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: HistoryURLCard(historyRequestModel: historyRequestModel), + ), + ), + ); + + expect( + find.text(historyRequestModel.httpRequestModel.url), findsOneWidget); + }); + }); +} diff --git a/test/widgets/dialog_history_retention_test.dart b/test/widgets/dialog_history_retention_test.dart new file mode 100644 index 00000000..b48c7018 --- /dev/null +++ b/test/widgets/dialog_history_retention_test.dart @@ -0,0 +1,150 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/widgets/dialog_history_retention.dart'; + +void main() { + group('showHistoryRetentionDialog Tests', () { + testWidgets('Testing History Retention dialog content', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + return ElevatedButton( + onPressed: () { + showHistoryRetentionDialog( + context, + HistoryRetentionPeriod.forever, + (period) {}, + ); + }, + child: const Text('Show Dialog'), + ); + }, + ), + ), + ); + + await tester.tap(find.text('Show Dialog')); + await tester.pumpAndSettle(); + + expect(find.text('Manage History'), findsOneWidget); + expect(find.byIcon(Icons.manage_history_rounded), findsOneWidget); + + expect(find.byType(RadioListTile), + findsNWidgets(HistoryRetentionPeriod.values.length)); + }); + + testWidgets('updates selected retention period correctly', (tester) async { + HistoryRetentionPeriod selectedPeriod = HistoryRetentionPeriod.forever; + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + return ElevatedButton( + onPressed: () { + showHistoryRetentionDialog( + context, + selectedPeriod, + (period) { + selectedPeriod = period; + }, + ); + }, + child: const Text('Show Dialog'), + ); + }, + ), + ), + ); + + await tester.tap(find.text('Show Dialog')); + await tester.pumpAndSettle(); + + await tester.tap(find.text(HistoryRetentionPeriod.oneWeek.label)); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Confirm')); + await tester.pumpAndSettle(); + + expect(selectedPeriod, HistoryRetentionPeriod.oneWeek); + }); + + testWidgets('Cancel button closes dialog without changing retention period', + (tester) async { + HistoryRetentionPeriod selectedPeriod = HistoryRetentionPeriod.oneWeek; + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + return ElevatedButton( + onPressed: () { + showHistoryRetentionDialog( + context, + selectedPeriod, + (period) { + selectedPeriod = period; + }, + ); + }, + child: const Text('Show Dialog'), + ); + }, + ), + ), + ); + + await tester.tap(find.text('Show Dialog')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + + expect(find.byType(AlertDialog), findsNothing); + expect(selectedPeriod, HistoryRetentionPeriod.oneWeek); + }); + + testWidgets( + 'Confirm button closes dialog and calls onRetentionPeriodChange', + (tester) async { + HistoryRetentionPeriod selectedPeriod = + HistoryRetentionPeriod.threeMonths; + HistoryRetentionPeriod newPeriod = HistoryRetentionPeriod.oneWeek; + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + return ElevatedButton( + onPressed: () { + showHistoryRetentionDialog( + context, + selectedPeriod, + (period) { + selectedPeriod = period; + }, + ); + }, + child: const Text('Show Dialog'), + ); + }, + ), + ), + ); + + await tester.tap(find.text('Show Dialog')); + await tester.pumpAndSettle(); + + await tester.tap(find.text(newPeriod.label)); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Confirm')); + await tester.pumpAndSettle(); + + expect(find.byType(AlertDialog), findsNothing); + expect(selectedPeriod, newPeriod); + }); + }); +} diff --git a/test/widgets/dialog_rename_test.dart b/test/widgets/dialog_rename_test.dart new file mode 100644 index 00000000..208a7b13 --- /dev/null +++ b/test/widgets/dialog_rename_test.dart @@ -0,0 +1,146 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/dialog_rename.dart'; + +void main() { + group('showRenameDialog Tests', () { + testWidgets('displays dialog with correct content', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + return ElevatedButton( + onPressed: () { + showRenameDialog( + context, + 'Rename Item', + 'Old Name', + (newName) {}, + ); + }, + child: const Text('Show Dialog'), + ); + }, + ), + ), + ); + + await tester.tap(find.text('Show Dialog')); + await tester.pumpAndSettle(); + + expect(find.text('Rename Item'), findsOneWidget); + expect(find.byIcon(Icons.edit_rounded), findsOneWidget); + + expect(find.byType(TextField), findsOneWidget); + expect(find.text('Old Name'), findsOneWidget); + }); + + testWidgets('calls onRename with new name when Ok is pressed', + (tester) async { + String? renamedValue; + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + return ElevatedButton( + onPressed: () { + showRenameDialog( + context, + 'Rename Item', + 'Old Name', + (newName) { + renamedValue = newName; + }, + ); + }, + child: const Text('Show Dialog'), + ); + }, + ), + ), + ); + + await tester.tap(find.text('Show Dialog')); + await tester.pumpAndSettle(); + + await tester.enterText(find.byType(TextField), 'New Name'); + await tester.tap(find.text('Ok')); + await tester.pumpAndSettle(); + + expect(renamedValue, 'New Name'); + }); + + testWidgets('does not call onRename when Cancel is pressed', + (tester) async { + String? renamedValue; + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + return ElevatedButton( + onPressed: () { + showRenameDialog( + context, + 'Rename Item', + 'Old Name', + (newName) { + renamedValue = newName; + }, + ); + }, + child: const Text('Show Dialog'), + ); + }, + ), + ), + ); + + await tester.tap(find.text('Show Dialog')); + await tester.pumpAndSettle(); + + await tester.enterText(find.byType(TextField), 'New Name'); + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + + expect(renamedValue, isNull); + }); + + testWidgets('disposes TextEditingController after dialog is closed', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return ElevatedButton( + onPressed: () { + showRenameDialog( + context, + 'Rename Item', + 'Old Name', + (newName) {}, + ); + }, + child: const Text('Show Dialog'), + ); + }, + ), + ), + ), + ); + + await tester.tap(find.text('Show Dialog')); + await tester.pumpAndSettle(); + + final textFieldFinder = find.byType(TextField); + expect(textFieldFinder, findsOneWidget); + + await tester.tap(find.text('Ok')); + await tester.pumpAndSettle(); + + expect(textFieldFinder, findsNothing); + }); + }); +} diff --git a/test/widgets/dropdown_codegen_test.dart b/test/widgets/dropdown_codegen_test.dart new file mode 100644 index 00000000..01432b10 --- /dev/null +++ b/test/widgets/dropdown_codegen_test.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/consts.dart'; +import '../test_consts.dart'; + +void main() { + testWidgets('Testing Dropdown for Codegen', (tester) async { + dynamic changedValue; + await tester.pumpWidget( + MaterialApp( + title: 'Dropdown Codegen Type testing', + theme: kThemeDataLight, + home: Scaffold( + body: Center( + child: Column( + children: [ + DropdownButtonCodegenLanguage( + codegenLanguage: CodegenLanguage.curl, + onChanged: (value) { + changedValue = value!; + }, + ), + ], + ), + ), + ), + ), + ); + + expect(find.byIcon(Icons.unfold_more_rounded), findsOneWidget); + expect(find.byType(DropdownButton), findsOneWidget); + expect( + (tester.widget(find.byType(DropdownButton)) + as DropdownButton) + .value, + equals(CodegenLanguage.curl)); + + await tester.tap(find.text('cURL')); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + await tester.tap(find.text('Dart (dio)').last); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + expect(changedValue, CodegenLanguage.dartDio); + }); +} diff --git a/test/widgets/dropdown_content_type_test.dart b/test/widgets/dropdown_content_type_test.dart new file mode 100644 index 00000000..62e10cc6 --- /dev/null +++ b/test/widgets/dropdown_content_type_test.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/consts.dart'; +import '../test_consts.dart'; + +void main() { + testWidgets('Testing Dropdown for Content Type', (tester) async { + dynamic changedValue; + await tester.pumpWidget( + MaterialApp( + title: 'Dropdown Content Type testing', + theme: kThemeDataLight, + home: Scaffold( + body: Center( + child: Column( + children: [ + DropdownButtonContentType( + contentType: ContentType.json, + onChanged: (value) { + changedValue = value!; + }, + ), + ], + ), + ), + ), + ), + ); + + expect(find.byIcon(Icons.unfold_more_rounded), findsOneWidget); + expect(find.byType(DropdownButton), findsOneWidget); + expect( + (tester.widget(find.byType(DropdownButton)) + as DropdownButton) + .value, + equals(ContentType.json)); + + await tester.tap(find.text('json')); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + await tester.tap(find.text('text').last); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + expect(changedValue, ContentType.text); + }); +} diff --git a/test/widgets/dropdown_formdata_test.dart b/test/widgets/dropdown_formdata_test.dart new file mode 100644 index 00000000..2b7b5238 --- /dev/null +++ b/test/widgets/dropdown_formdata_test.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/consts.dart'; +import '../test_consts.dart'; + +void main() { + testWidgets('Testing Dropdown for FormData', (tester) async { + dynamic changedValue; + await tester.pumpWidget( + MaterialApp( + title: 'Dropdown FormData Type testing', + theme: kThemeDataLight, + home: Scaffold( + body: Center( + child: Column( + children: [ + DropdownButtonFormData( + formDataType: FormDataType.file, + onChanged: (value) { + changedValue = value!; + }, + ), + ], + ), + ), + ), + ), + ); + + expect(find.byIcon(Icons.unfold_more_rounded), findsOneWidget); + expect(find.byType(DropdownButton), findsOneWidget); + expect( + (tester.widget(find.byType(DropdownButton)) + as DropdownButton) + .value, + equals(FormDataType.file)); + + await tester.tap(find.text('file')); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + await tester.tap(find.text('text').last); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + expect(changedValue, FormDataType.text); + }); +} diff --git a/test/widgets/dropdowns_http_method_test.dart b/test/widgets/dropdowns_http_method_test.dart new file mode 100644 index 00000000..663face7 --- /dev/null +++ b/test/widgets/dropdowns_http_method_test.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/consts.dart'; +import '../test_consts.dart'; + +void main() { + testWidgets('Testing Dropdown for Http Method', (tester) async { + dynamic changedValue; + await tester.pumpWidget( + MaterialApp( + title: 'Dropdown Http method testing', + theme: kThemeDataLight, + home: Scaffold( + body: Center( + child: Column( + children: [ + DropdownButtonHttpMethod( + method: HTTPVerb.post, + onChanged: (value) { + changedValue = value!; + }, + ), + ], + ), + ), + ), + ), + ); + + expect(find.byIcon(Icons.unfold_more_rounded), findsOneWidget); + expect(find.byType(DropdownButton), findsOneWidget); + expect( + (tester.widget(find.byType(DropdownButton)) as DropdownButton) + .value, + equals(HTTPVerb.post)); + + await tester.tap(find.text('POST')); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + await tester.tap(find.text('PUT').last); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + expect(changedValue, HTTPVerb.put); + }); +} diff --git a/test/widgets/dropdowns_test.dart b/test/widgets/dropdowns_test.dart deleted file mode 100644 index cca255fc..00000000 --- a/test/widgets/dropdowns_test.dart +++ /dev/null @@ -1,174 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:apidash/widgets/widgets.dart'; -import 'package:apidash/consts.dart'; -import '../test_consts.dart'; - -void main() { - testWidgets('Testing Dropdowns', (tester) async { - dynamic changedValue; - await tester.pumpWidget( - MaterialApp( - title: 'Dropdown testing', - theme: kThemeDataLight, - home: Scaffold( - body: Center( - child: Column( - children: [ - DropdownButtonHttpMethod( - method: HTTPVerb.post, - onChanged: (value) { - changedValue = value!; - }, - ), - ], - ), - ), - ), - ), - ); - - expect(find.byIcon(Icons.unfold_more_rounded), findsOneWidget); - expect(find.byType(DropdownButton), findsOneWidget); - expect( - (tester.widget(find.byType(DropdownButton)) as DropdownButton) - .value, - equals(HTTPVerb.post)); - - await tester.tap(find.text('POST')); - await tester.pump(); - await tester.pump(const Duration(seconds: 1)); - - await tester.tap(find.text('PUT').last); - await tester.pump(); - await tester.pump(const Duration(seconds: 1)); - - expect(changedValue, HTTPVerb.put); - }); - - testWidgets('Testing Dropdown for Content Type', (tester) async { - dynamic changedValue; - await tester.pumpWidget( - MaterialApp( - title: 'Dropdown Content Type testing', - theme: kThemeDataLight, - home: Scaffold( - body: Center( - child: Column( - children: [ - DropdownButtonContentType( - contentType: ContentType.json, - onChanged: (value) { - changedValue = value!; - }, - ), - ], - ), - ), - ), - ), - ); - - expect(find.byIcon(Icons.unfold_more_rounded), findsOneWidget); - expect(find.byType(DropdownButton), findsOneWidget); - expect( - (tester.widget(find.byType(DropdownButton)) - as DropdownButton) - .value, - equals(ContentType.json)); - - await tester.tap(find.text('json')); - await tester.pump(); - await tester.pump(const Duration(seconds: 1)); - - await tester.tap(find.text('text').last); - await tester.pump(); - await tester.pump(const Duration(seconds: 1)); - - expect(changedValue, ContentType.text); - }); - - testWidgets('Testing Dropdown for FormData', (tester) async { - dynamic changedValue; - await tester.pumpWidget( - MaterialApp( - title: 'Dropdown FormData Type testing', - theme: kThemeDataLight, - home: Scaffold( - body: Center( - child: Column( - children: [ - DropdownButtonFormData( - formDataType: FormDataType.file, - onChanged: (value) { - changedValue = value!; - }, - ), - ], - ), - ), - ), - ), - ); - - expect(find.byIcon(Icons.unfold_more_rounded), findsOneWidget); - expect(find.byType(DropdownButton), findsOneWidget); - expect( - (tester.widget(find.byType(DropdownButton)) - as DropdownButton) - .value, - equals(FormDataType.file)); - - await tester.tap(find.text('file')); - await tester.pump(); - await tester.pump(const Duration(seconds: 1)); - - await tester.tap(find.text('text').last); - await tester.pump(); - await tester.pump(const Duration(seconds: 1)); - - expect(changedValue, FormDataType.text); - }); - - testWidgets('Testing Dropdown for Codegen', (tester) async { - dynamic changedValue; - await tester.pumpWidget( - MaterialApp( - title: 'Dropdown Codegen Type testing', - theme: kThemeDataLight, - home: Scaffold( - body: Center( - child: Column( - children: [ - DropdownButtonCodegenLanguage( - codegenLanguage: CodegenLanguage.curl, - onChanged: (value) { - changedValue = value!; - }, - ), - ], - ), - ), - ), - ), - ); - - expect(find.byIcon(Icons.unfold_more_rounded), findsOneWidget); - expect(find.byType(DropdownButton), findsOneWidget); - expect( - (tester.widget(find.byType(DropdownButton)) - as DropdownButton) - .value, - equals(CodegenLanguage.curl)); - - await tester.tap(find.text('cURL')); - await tester.pump(); - await tester.pump(const Duration(seconds: 1)); - - await tester.tap(find.text('Dart (dio)').last); - await tester.pump(); - await tester.pump(const Duration(seconds: 1)); - - expect(changedValue, CodegenLanguage.dartDio); - }); -} From 85f3508321d50a4b0355b90f056031298559389a Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sun, 4 Aug 2024 02:30:49 +0530 Subject: [PATCH 25/71] test: history_utils tests --- test/utils/history_utils_test.dart | 195 +++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 test/utils/history_utils_test.dart diff --git a/test/utils/history_utils_test.dart b/test/utils/history_utils_test.dart new file mode 100644 index 00000000..cdae4c22 --- /dev/null +++ b/test/utils/history_utils_test.dart @@ -0,0 +1,195 @@ +import 'package:apidash/consts.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/utils/history_utils.dart'; + +import '../models/history_models.dart'; + +void main() { + group('Testing getHistoryRequestName function', () { + test('returns name when name is not empty', () { + final model = historyMetaModel1.copyWith(name: 'Social'); + final result = getHistoryRequestName(model); + expect(result, 'Social'); + }); + + test('returns url when name is empty', () { + final model = historyMetaModel1; + final result = getHistoryRequestName(model); + expect(result, 'https://api.apidash.dev/humanize/social'); + }); + }); + group('Testing getHistoryRequestKey function', () { + test('returns name + method + timestamp when name is not empty', () { + final model = historyMetaModel1.copyWith(name: 'Social'); + + final result = getHistoryRequestKey(model); + expect(result, 'SocialgetJanuary 1, 2024'); + }); + + test('returns url + method + timestamp when name is empty', () { + final model = historyMetaModel1; + + final result = getHistoryRequestKey(model); + expect( + result, 'https://api.apidash.dev/humanize/socialgetJanuary 1, 2024'); + }); + }); + + group('Testing getDateTimeKey function', () { + test('returns currentKey when keys is empty', () { + final now = DateTime.now(); + final currentKey = DateTime(now.year, now.month, now.day); + final result = getDateTimeKey([], currentKey); + expect(result, currentKey); + }); + + test('returns currentKey when keys does not contain currentKey', () { + final now = DateTime.now(); + final currentKey = DateTime(now.year, now.month, now.day); + final keys = [DateTime(2024, 1, 1), DateTime(2024, 1, 2)]; + + final result = getDateTimeKey(keys, currentKey); + expect(result, currentKey); + }); + + test('returns currentKey when keys contains currentKey', () { + final currentKey = DateTime(2024, 1, 1); + final keys = [DateTime(2024, 1, 1), DateTime(2024, 1, 2)]; + + final result = getDateTimeKey(keys, currentKey); + expect(result, currentKey); + }); + }); + + group('Testing getRequestGroups function', () { + test('returns empty map when models is null', () { + final result = getRequestGroups(null); + expect(result, isEmpty); + }); + + test('returns empty map when models is empty', () { + final result = getRequestGroups([]); + expect(result, isEmpty); + }); + + test('groups models by request key and sorts by timestamp', () { + final models = [ + historyMetaModel1, + historyMetaModel1.copyWith( + historyId: 'historyId1-1', + timeStamp: + historyMetaModel1.timeStamp.add(const Duration(seconds: 1))), + historyMetaModel1.copyWith( + historyId: 'historyId1-2', + timeStamp: + historyMetaModel1.timeStamp.add(const Duration(seconds: 2))), + historyMetaModel2, + historyMetaModel2.copyWith( + historyId: 'historyId2-1', + timeStamp: + historyMetaModel2.timeStamp.add(const Duration(seconds: 1))), + historyMetaModel2.copyWith( + historyId: 'historyId2-2', + timeStamp: + historyMetaModel2.timeStamp.add(const Duration(seconds: 2))), + ]; + + final result = getRequestGroups(models); + expect(result.keys.length, 2); + + expect(result.values.toList()[0].length, 3); + expect(result.values.toList()[1].length, 3); + + result.forEach((key, value) { + for (int i = 0; i < value.length - 1; i++) { + expect(value[i].timeStamp.isAfter(value[i + 1].timeStamp), isTrue); + } + }); + }); + }); + + group('Testing getRequestGroup function', () { + test('returns empty list when models is null', () { + final result = getRequestGroup(null, historyMetaModel1); + expect(result, isEmpty); + }); + + test('returns empty list when models is empty', () { + final result = getRequestGroup([], historyMetaModel1); + expect(result, isEmpty); + }); + + test('returns empty list when selectedModel is null', () { + final result = getRequestGroup([historyMetaModel1], null); + expect(result, isEmpty); + }); + + test('returns empty list when selectedModel is not in models', () { + final result = getRequestGroup([historyMetaModel1], historyMetaModel2); + expect(result, isEmpty); + }); + + test( + 'returns list of models with same request key as selectedModel and sorted', + () { + final models = [ + historyMetaModel1, + historyMetaModel1.copyWith( + historyId: 'historyId1-1', + timeStamp: + historyMetaModel1.timeStamp.add(const Duration(seconds: 1))), + historyMetaModel1.copyWith( + historyId: 'historyId1-2', + timeStamp: + historyMetaModel1.timeStamp.add(const Duration(seconds: 2))), + historyMetaModel2, + historyMetaModel2.copyWith( + historyId: 'historyId2-1', + timeStamp: + historyMetaModel2.timeStamp.add(const Duration(seconds: 1))), + historyMetaModel2.copyWith( + historyId: 'historyId2-2', + timeStamp: + historyMetaModel2.timeStamp.add(const Duration(seconds: 2))), + ]; + + final result = getRequestGroup(models, models[1]); + expect(result.length, 3); + + for (int i = 0; i < result.length - 1; i++) { + expect(result[i].timeStamp.isAfter(result[i + 1].timeStamp), isTrue); + } + }); + }); + + group('Testing getRetentionDate functon', () { + test('HistoryRetentionPeriod oneWeek', () { + final result = getRetentionDate(HistoryRetentionPeriod.oneWeek); + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final expected = today.subtract(const Duration(days: 7)); + expect(result, expected); + }); + + test('HistoryRetentionPeriod oneMonth', () { + final result = getRetentionDate(HistoryRetentionPeriod.oneMonth); + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final expected = today.subtract(const Duration(days: 30)); + expect(result, expected); + }); + + test('HistoryRetentionPeriod threeMonths', () { + final result = getRetentionDate(HistoryRetentionPeriod.threeMonths); + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final expected = today.subtract(const Duration(days: 90)); + expect(result, expected); + }); + + test('HistoryRetentionPeriod forever', () { + final result = getRetentionDate(HistoryRetentionPeriod.forever); + expect(result, null); + }); + }); +} From 4bf32c963d57e5cde3bcfc1889908b00169d04ae Mon Sep 17 00:00:00 2001 From: abhishekshah5486 Date: Mon, 5 Aug 2024 23:31:24 +0530 Subject: [PATCH 26/71] Updated the README.md file to replace the term mimetypes with the more accurate MIME types for consistency and clarity in the documentation. --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5569f582..2da72ff9 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ API Dash can be downloaded from the links below: - Inspect the API Response (HTTP status code, error message, headers, body, time taken). - View formatted code previews for responses of various content types like `JSON`, `XML`, `YAML`, `HTML`, `SQL`, etc. -- API Dash helps explore, test & preview Multimedia API responses which is **not supported by any other API client**. You can directly test APIs that return images, PDF, audio & more. Check out the [full list of supported mimetypes/formats here](https://github.com/foss42/apidash?tab=readme-ov-file#mime-types-supported-by-api-dash-response-previewer). +- API Dash helps explore, test & preview Multimedia API responses which is **not supported by any other API client**. You can directly test APIs that return images, PDF, audio & more. Check out the [full list of supported MIME types/formats here](https://github.com/foss42/apidash?tab=readme-ov-file#mime-types-supported-by-api-dash-response-previewer). - Save 💾 response body of any mimetype (`image`, `text`, etc.) directly in the `Downloads` folder of your system by clicking on the `Download` button. **👩🏻‍💻 Code Generation** @@ -148,7 +148,7 @@ We welcome contributions to support other programming languages/libraries/framew API Dash is a next-gen API client that supports exploring, testing & previewing various data & multimedia API responses which is limited/not supported by other API clients. You can directly test APIs that return images, PDF, audio & more. -Here is the complete list of mimetypes that can be directly previewed in API Dash: +Here is the complete list of MIME types that can be directly previewed in API Dash: | File Type | Mimetype | Extension | Comment | | --------- | -------------------------- | ----------------- | -------- | @@ -195,15 +195,15 @@ Here is the complete list of mimetypes that can be directly previewed in API Das | Audio | `audio/wave` | `.wav` | | | CSV | `text/csv` | `.csv` | Can be improved | -We welcome PRs to add support for previewing other multimedia mimetypes. Please go ahead and raise an issue so that we can discuss the approach. -We are adding support for other mimetypes with each release. But, if you are looking for any particular mimetype support, please go ahead and open an issue. We will prioritize it's addition. +We welcome PRs to add support for previewing other multimedia MIME types. Please go ahead and raise an issue so that we can discuss the approach. +We are adding support for other MIME types with each release. But, if you are looking for any particular mimetype support, please go ahead and open an issue. We will prioritize it's addition. -Here is the complete list of mimetypes that are syntax highlighted in API Dash: +Here is the complete list of MIME types that are syntax highlighted in API Dash: | Mimetype | Extension | Comment | | ------------------ | --------- | ------------------------------------------------------------------------------------------------------------------ | -| `application/json` | `.json` | Other mimetypes like `application/geo+json`, `application/vcard+json` that are based on `json` are also supported. | -| `application/xml` | `.xml` | Other mimetypes like `application/xhtml+xml`, `application/vcard+xml` that are based on `xml` are also supported. | +| `application/json` | `.json` | Other MIME types like `application/geo+json`, `application/vcard+json` that are based on `json` are also supported. | +| `application/xml` | `.xml` | Other MIME types like `application/xhtml+xml`, `application/vcard+xml` that are based on `xml` are also supported. | | `text/xml` | `.xml` | | | `application/yaml` | `.yaml` | Others - `application/x-yaml` or `application/x-yml` | | `text/yaml` | `.yaml` | Others - `text/yml` | From 464a1521c2cddcf0849d7ae0d91a0fc5bbb119d9 Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Sat, 10 Aug 2024 18:33:42 +0530 Subject: [PATCH 27/71] move class --- lib/consts.dart | 14 -------------- lib/widgets/button_group_filled.dart | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/consts.dart b/lib/consts.dart index 73de67f6..09efd409 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -322,20 +322,6 @@ final kColorHttpMethodPut = Colors.amber.shade900; final kColorHttpMethodPatch = kColorHttpMethodPut; final kColorHttpMethodDelete = Colors.red.shade800; -class ButtonData { - ButtonData({ - required this.label, - required this.icon, - this.onPressed, - this.tooltip = "", - }); - - final String label; - final IconData icon; - final VoidCallback? onPressed; - final String tooltip; -} - enum HistoryRetentionPeriod { oneWeek("1 Week", Icons.calendar_view_week_rounded), oneMonth("1 Month", Icons.calendar_view_month_rounded), diff --git a/lib/widgets/button_group_filled.dart b/lib/widgets/button_group_filled.dart index 24ae622d..7e1de0db 100644 --- a/lib/widgets/button_group_filled.dart +++ b/lib/widgets/button_group_filled.dart @@ -1,6 +1,20 @@ import 'package:flutter/material.dart'; import 'package:apidash/consts.dart'; +class ButtonData { + ButtonData({ + required this.label, + required this.icon, + this.onPressed, + this.tooltip = "", + }); + + final String label; + final IconData icon; + final VoidCallback? onPressed; + final String tooltip; +} + class FilledButtonGroup extends StatelessWidget { const FilledButtonGroup({super.key, required this.buttons}); From ec29debedc9bc3449ddbdea4a31be7dd5c44be74 Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Sat, 10 Aug 2024 20:51:36 +0530 Subject: [PATCH 28/71] Update consts.dart --- lib/consts.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/consts.dart b/lib/consts.dart index 09efd409..5d757229 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -105,7 +105,7 @@ const kP8 = EdgeInsets.all(8); const kPs8 = EdgeInsets.only(left: 8); const kPs2 = EdgeInsets.only(left: 2); const kPe4 = EdgeInsets.only(right: 4); -const kPe8 = EdgeInsets.only(right: 8.0); +const kPe8 = EdgeInsets.only(right: 8); const kPh20v5 = EdgeInsets.symmetric(horizontal: 20, vertical: 5); const kPh20v10 = EdgeInsets.symmetric(horizontal: 20, vertical: 10); const kP10 = EdgeInsets.all(10); From d0351b314310a267067a79a359d762a45e14863e Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Sat, 10 Aug 2024 21:51:13 +0530 Subject: [PATCH 29/71] Rename envvar__indicator_test.dart to envvar_indicator_test.dart --- .../{envvar__indicator_test.dart => envvar_indicator_test.dart} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/screens/common_widgets/{envvar__indicator_test.dart => envvar_indicator_test.dart} (100%) diff --git a/test/screens/common_widgets/envvar__indicator_test.dart b/test/screens/common_widgets/envvar_indicator_test.dart similarity index 100% rename from test/screens/common_widgets/envvar__indicator_test.dart rename to test/screens/common_widgets/envvar_indicator_test.dart From cb5a045b0f9a7a2977796e468e4d1fa9fc87971a Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Sat, 10 Aug 2024 22:16:09 +0530 Subject: [PATCH 30/71] Delete duplicate file as it was renamed --- .../envvar__indicator_test.dart | 94 ------------------- 1 file changed, 94 deletions(-) delete mode 100644 test/screens/common_widgets/envvar__indicator_test.dart diff --git a/test/screens/common_widgets/envvar__indicator_test.dart b/test/screens/common_widgets/envvar__indicator_test.dart deleted file mode 100644 index 9dc9bdc6..00000000 --- a/test/screens/common_widgets/envvar__indicator_test.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:apidash/consts.dart'; -import 'package:apidash/models/models.dart'; -import 'package:apidash/screens/common_widgets/envvar_indicator.dart'; - -void main() { - testWidgets( - 'EnvVarIndicator displays correct icon and color for unknown suggestion', - (WidgetTester tester) async { - const suggestion = EnvironmentVariableSuggestion( - isUnknown: true, - environmentId: 'someId', - variable: EnvironmentVariableModel(key: 'key', value: 'value')); - - await tester.pumpWidget( - const MaterialApp( - home: Scaffold( - body: EnvVarIndicator(suggestion: suggestion), - ), - ), - ); - - final container = tester.widget(find.byType(Container)); - final icon = tester.widget(find.byType(Icon)); - - expect(container.decoration, isA()); - final decoration = container.decoration as BoxDecoration; - expect( - decoration.color, - Theme.of(tester.element(find.byType(Container))) - .colorScheme - .errorContainer); - expect(icon.icon, Icons.block); - }); - - testWidgets( - 'EnvVarIndicator displays correct icon and color for global suggestion', - (WidgetTester tester) async { - const suggestion = EnvironmentVariableSuggestion( - isUnknown: false, - environmentId: kGlobalEnvironmentId, - variable: EnvironmentVariableModel(key: 'key', value: 'value')); - - await tester.pumpWidget( - const MaterialApp( - home: Scaffold( - body: EnvVarIndicator(suggestion: suggestion), - ), - ), - ); - - final container = tester.widget(find.byType(Container)); - final icon = tester.widget(find.byType(Icon)); - - expect(container.decoration, isA()); - final decoration = container.decoration as BoxDecoration; - expect( - decoration.color, - Theme.of(tester.element(find.byType(Container))) - .colorScheme - .secondaryContainer); - expect(icon.icon, Icons.public); - }); - - testWidgets( - 'EnvVarIndicator displays correct icon and color for non-global suggestion', - (WidgetTester tester) async { - const suggestion = EnvironmentVariableSuggestion( - isUnknown: false, - environmentId: 'someOtherId', - variable: EnvironmentVariableModel(key: 'key', value: 'value')); - - await tester.pumpWidget( - const MaterialApp( - home: Scaffold( - body: EnvVarIndicator(suggestion: suggestion), - ), - ), - ); - - final container = tester.widget(find.byType(Container)); - final icon = tester.widget(find.byType(Icon)); - - expect(container.decoration, isA()); - final decoration = container.decoration as BoxDecoration; - expect( - decoration.color, - Theme.of(tester.element(find.byType(Container))) - .colorScheme - .primaryContainer); - expect(icon.icon, Icons.computer); - }); -} From 79201ebc7fcd681ca688da90dfc495b1d8ff398e Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Tue, 13 Aug 2024 03:12:08 +0530 Subject: [PATCH 31/71] wip: interation test helper --- integration_test/env_manager_test.dart | 57 +++++++ integration_test/test_helper.dart | 49 ++++++ lib/main.dart | 24 ++- lib/screens/dashboard.dart | 4 +- pubspec.lock | 142 +++++++++--------- pubspec.yaml | 2 + test/extensions/widget_tester_extensions.dart | 17 +++ test/providers/ui_providers_test.dart | 2 +- test/widgets/button_group_filled_test.dart | 1 - test_driver/integration_test.dart | 3 + 10 files changed, 219 insertions(+), 82 deletions(-) create mode 100644 integration_test/env_manager_test.dart create mode 100644 integration_test/test_helper.dart create mode 100644 test_driver/integration_test.dart diff --git a/integration_test/env_manager_test.dart b/integration_test/env_manager_test.dart new file mode 100644 index 00000000..21d9aa20 --- /dev/null +++ b/integration_test/env_manager_test.dart @@ -0,0 +1,57 @@ +import 'package:apidash/widgets/menu_item_card.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; +import 'package:apidash/app.dart'; +import 'package:apidash/consts.dart'; + +import 'package:apidash/screens/envvar/environments_pane.dart'; +import '../test/extensions/widget_tester_extensions.dart'; +import 'test_helper.dart'; + +void main() async { + await ApidashTestHelper.initialize(); + apidashWidgetTest("Testing Environment Manager end-to-end", + (WidgetTester tester, helper) async { + await tester.pumpUntilFound(find.byType(DashApp)); + await Future.delayed(const Duration(seconds: 2)); + + /// Navigate to Environment Manager + Finder envNavbutton = find.byIcon(Icons.laptop_windows_outlined); + expect(envNavbutton, findsOneWidget); + await tester.tap(envNavbutton); + await tester.pumpAndSettle(); + await Future.delayed(const Duration(seconds: 1)); + + /// Create New Environment + final newEnvButton = + spot().spot().spotText(kLabelPlusNew); + newEnvButton.existsOnce(); + await act.tap(newEnvButton); + await tester.pumpAndSettle(); + await Future.delayed(const Duration(seconds: 1)); + + /// Open ItemCardMenu of the new environment + spot() + .spot() + .spot() + .existsAtLeastNTimes(2); + Finder envItems = find.byType(EnvironmentItem); + Finder newEnvItem = envItems.at(1); + expect(find.descendant(of: newEnvItem, matching: find.text('untitled')), + findsOneWidget); + Finder itemCardMenu = + find.descendant(of: newEnvItem, matching: find.byType(ItemCardMenu)); + await tester.tap(itemCardMenu); + await tester.pumpAndSettle(); + + /// Rename the new environment + await tester.tap(find.text('Rename').last); + await tester.pump(); + await tester.enterText(newEnvItem, "New Environment"); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pump(); + + await Future.delayed(const Duration(seconds: 2)); + }); +} diff --git a/integration_test/test_helper.dart b/integration_test/test_helper.dart new file mode 100644 index 00000000..88b07f6c --- /dev/null +++ b/integration_test/test_helper.dart @@ -0,0 +1,49 @@ +import 'dart:ui'; + +import 'package:apidash/app.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:apidash/main.dart' as app; + +class ApidashTestHelper { + final WidgetTester tester; + + ApidashTestHelper(this.tester); + + static Future initialize( + {Size? size}) async { + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; + + await app.initApp(); + await app.initWindow(sz: size); + + return binding; + } + + static Future loadApp(WidgetTester tester) async { + await app.initApp(); + await tester.pumpWidget( + const ProviderScope( + child: DashApp(), + ), + ); + } +} + +@isTest +void apidashWidgetTest( + String description, + Future Function(WidgetTester, ApidashTestHelper) test, +) { + testWidgets( + description, + (widgetTester) async { + await ApidashTestHelper.loadApp(widgetTester); + await test(widgetTester, ApidashTestHelper(widgetTester)); + }, + semanticsEnabled: false, + ); +} diff --git a/lib/main.dart b/lib/main.dart index d9730a03..54b521ca 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,19 +7,29 @@ import 'app.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + + await initApp(); + await initWindow(); + + runApp( + const ProviderScope( + child: DashApp(), + ), + ); +} + +Future initApp() async { GoogleFonts.config.allowRuntimeFetching = false; await openBoxes(); await autoClearHistory(); +} + +Future initWindow({Size? sz}) async { if (kIsLinux) { - await setupInitialWindow(); + await setupInitialWindow(sz: sz); } if (kIsMacOS || kIsWindows) { - var win = getInitialSize(); + var win = sz == null ? (sz, null) : getInitialSize(); await setupWindow(sz: win.$1, off: win.$2); } - runApp( - const ProviderScope( - child: DashApp(), - ), - ); } diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index c0f1532f..d5bce2f1 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -46,8 +46,8 @@ class Dashboard extends ConsumerWidget { onPressed: () { ref.read(navRailIndexStateProvider.notifier).state = 1; }, - icon: const Icon(Icons.computer_outlined), - selectedIcon: const Icon(Icons.computer_rounded), + icon: const Icon(Icons.laptop_windows_outlined), + selectedIcon: const Icon(Icons.laptop_windows), ), Text( 'Variables', diff --git a/pubspec.lock b/pubspec.lock index 4e463339..80c375d3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: audio_session - sha256: a49af9981eec5d7cd73b37bacb6ee73f8143a6a9f9bd5b6021e6c346b9b6cf4e + sha256: "343e83bc7809fbda2591a49e525d6b63213ade10c76f15813be9aed6657b3261" url: "https://pub.dev" source: hosted - version: "0.1.19" + version: "0.1.21" barcode: dependency: transitive description: @@ -93,10 +93,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" build_resolvers: dependency: transitive description: @@ -117,10 +117,10 @@ packages: dependency: transitive description: name: build_runner_core - sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" + sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.3.1" built_collection: dependency: transitive description: @@ -205,18 +205,18 @@ packages: dependency: transitive description: name: coverage - sha256: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e" + sha256: "576aaab8b1abdd452e0f656c3e73da9ead9d7880e15bdc494189d9c1a1baf0db" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.0" cross_file: dependency: transitive description: name: cross_file - sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" url: "https://pub.dev" source: hosted - version: "0.3.4+1" + version: "0.3.4+2" crypto: dependency: transitive description: @@ -350,10 +350,10 @@ packages: dependency: transitive description: name: file_selector_android - sha256: "8bcc3af859e9d47fab9c7dc315537406511a894ab578e198bd8f9ed745ea5a01" + sha256: d1e8655c1a4850a900a0cfaed55fdd273881d53a4bb78e4736dc170a0b17db78 url: "https://pub.dev" source: hosted - version: "0.5.1+2" + version: "0.5.1+5" file_selector_ios: dependency: transitive description: @@ -390,18 +390,18 @@ packages: dependency: transitive description: name: file_selector_web - sha256: "619e431b224711a3869e30dbd7d516f5f5a4f04b265013a50912f39e1abc88c8" + sha256: c4c0ea4224d97a60a7067eca0c8fd419e708ff830e0c83b11a48faf566cec3e7 url: "https://pub.dev" source: hosted - version: "0.9.4+1" + version: "0.9.4+2" file_selector_windows: dependency: transitive description: name: file_selector_windows - sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69" url: "https://pub.dev" source: hosted - version: "0.9.3+1" + version: "0.9.3+2" fixnum: dependency: transitive description: @@ -512,10 +512,10 @@ packages: dependency: "direct main" description: name: flutter_markdown - sha256: "85cc6f7daeae537844c92e2d56e2aff61b00095f8f77913b529ea4be12fc45ea" + sha256: "2e8a801b1ded5ea001a4529c97b1f213dcb11c6b20668e081cafb23468593514" url: "https://pub.dev" source: hosted - version: "0.7.2+1" + version: "0.7.3" flutter_portal: dependency: "direct main" description: @@ -570,18 +570,18 @@ packages: dependency: "direct main" description: name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.4" frontend_server_client: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" fuchsia_remote_debug_protocol: dependency: transitive description: flutter @@ -615,10 +615,10 @@ packages: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" highlighter: dependency: "direct main" description: @@ -700,7 +700,7 @@ packages: source: hosted version: "4.2.0" integration_test: - dependency: transitive + dependency: "direct dev" description: flutter source: sdk version: "0.0.0" @@ -732,10 +732,10 @@ packages: dependency: transitive description: name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.7.1" json_annotation: dependency: "direct main" description: @@ -773,10 +773,10 @@ packages: dependency: "direct main" description: name: just_audio - sha256: "5abfab1d199e01ab5beffa61b3e782350df5dad036cb8c83b79fa45fc656614e" + sha256: ee50602364ba83fa6308f5512dd560c713ec3e1f2bc75f0db43618f0d82ef71a url: "https://pub.dev" source: hosted - version: "0.9.38" + version: "0.9.39" just_audio_mpv: dependency: "direct main" description: @@ -917,10 +917,10 @@ packages: dependency: "direct main" description: name: multi_split_view - sha256: "1ee1974d9aae6bdc08e2abdead6066c914cefe4b0c5999cac1a2e4722fcf33ba" + sha256: "30548c5e4cc6f24d5d4ca784dc5dff80d599ef1f704b1565819eb88a74f0eb62" url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.5.0" multi_trigger_autocomplete: dependency: "direct main" description: @@ -966,18 +966,18 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0 + sha256: "4de6c36df77ffbcef0a5aefe04669d33f2d18397fea228277b852a2d4e58e860" url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "8.0.1" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e + sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.1" path: dependency: "direct main" description: @@ -998,18 +998,18 @@ packages: dependency: "direct main" description: name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + sha256: "490539678396d4c3c0b06efdaab75ae60675c3e0c66f72bc04c2e2c1e0e2abeb" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.2.9" path_provider_foundation: dependency: transitive description: @@ -1038,18 +1038,18 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" pdf: dependency: transitive description: name: pdf - sha256: "243f05342fc0bdf140eba5b069398985cdbdd3dbb1d776cf43d5ea29cc570ba6" + sha256: "81d5522bddc1ef5c28e8f0ee40b71708761753c163e0c93a40df56fd515ea0f0" url: "https://pub.dev" source: hosted - version: "3.10.8" + version: "3.11.0" pdf_widget_wrapper: dependency: "direct overridden" description: @@ -1086,10 +1086,10 @@ packages: dependency: transitive description: name: pointer_interceptor - sha256: d0a8e660d1204eaec5bd34b34cc92174690e076d2e4f893d9d68c486a13b07c4 + sha256: "57210410680379aea8b1b7ed6ae0c3ad349bfd56fe845b8ea934a53344b9d523" url: "https://pub.dev" source: hosted - version: "0.10.1+1" + version: "0.10.1+2" pointer_interceptor_ios: dependency: transitive description: @@ -1110,10 +1110,10 @@ packages: dependency: transitive description: name: pointer_interceptor_web - sha256: a6237528b46c411d8d55cdfad8fcb3269fc4cbb26060b14bff94879165887d1e + sha256: "7a7087782110f8c1827170660b09f8aa893e0e9a61431dbbe2ac3fc482e8c044" url: "https://pub.dev" source: hosted - version: "0.10.2" + version: "0.10.2+1" pool: dependency: transitive description: @@ -1126,10 +1126,10 @@ packages: dependency: "direct main" description: name: printing - sha256: "1c99cab90ebcc1fff65831d264627d5b529359d563e53f33ab9b8117f2d280bc" + sha256: cc4b256a5a89d5345488e3318897b595867f5181b8c5ed6fc63bfa5f2044aec3 url: "https://pub.dev" source: hosted - version: "5.12.0" + version: "5.13.1" process: dependency: transitive description: @@ -1166,10 +1166,10 @@ packages: dependency: transitive description: name: qr - sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" riverpod: dependency: "direct main" description: @@ -1182,10 +1182,10 @@ packages: dependency: transitive description: name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" url: "https://pub.dev" source: hosted - version: "0.27.7" + version: "0.28.0" screen_retriever: dependency: transitive description: @@ -1427,18 +1427,18 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9" + sha256: "94d8ad05f44c6d4e2ffe5567ab4d741b82d62e3c8e288cc1fcea45965edf47c9" url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.8" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_linux: dependency: transitive description: @@ -1467,26 +1467,26 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" uuid: dependency: "direct main" description: name: uuid - sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" + sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.4.2" vector_graphics: dependency: transitive description: @@ -1523,18 +1523,18 @@ packages: dependency: "direct main" description: name: video_player - sha256: aced48e701e24c02b0b7f881a8819e4937794e46b5a5821005e2bf3b40a324cc + sha256: e30df0d226c4ef82e2c150ebf6834b3522cf3f654d8e2f9419d376cdc071425d url: "https://pub.dev" source: hosted - version: "2.8.7" + version: "2.9.1" video_player_android: dependency: transitive description: name: video_player_android - sha256: "134e1ad410d67e18a19486ed9512c72dfc6d8ffb284d0e8f2e99e903d1ba8fa3" + sha256: "4de50df9ee786f5891d3281e1e633d7b142ef1acf47392592eb91cba5d355849" url: "https://pub.dev" source: hosted - version: "2.4.14" + version: "2.6.0" video_player_avfoundation: dependency: transitive description: @@ -1555,10 +1555,10 @@ packages: dependency: transitive description: name: video_player_web - sha256: ff4d69a6614b03f055397c27a71c9d3ddea2b2a23d71b2ba0164f59ca32b8fe2 + sha256: "6dcdd298136523eaf7dfc31abaf0dfba9aa8a8dbc96670e87e9d42b6f2caf774" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" vm_service: dependency: transitive description: @@ -1611,10 +1611,10 @@ packages: dependency: transitive description: name: win32 - sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" + sha256: "015002c060f1ae9f41a818f2d5640389cc05283e368be19dc8d77cecb43c40c9" url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "5.5.3" window_manager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 515fc0c9..5563422f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -89,6 +89,8 @@ dev_dependencies: freezed: ^2.5.2 json_serializable: ^6.7.1 spot: ^0.13.0 + integration_test: + sdk: flutter flutter: uses-material-design: true diff --git a/test/extensions/widget_tester_extensions.dart b/test/extensions/widget_tester_extensions.dart index d17c26e5..fe08e568 100644 --- a/test/extensions/widget_tester_extensions.dart +++ b/test/extensions/widget_tester_extensions.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:ui'; import 'package:flutter_test/flutter_test.dart'; import '../test_consts.dart'; @@ -22,3 +23,19 @@ extension ScreenSizeManager on WidgetTester { view.devicePixelRatio = pixelDensity; } } + +extension PumpUntilFound on WidgetTester { + Future pumpUntilFound( + Finder finder, { + Duration timeout = const Duration(seconds: 20), + }) async { + bool found = false; + final timer = Timer( + timeout, () => throw TimeoutException("Pump until has timed out")); + while (found != true) { + await pump(); + found = any(finder); + } + timer.cancel(); + } +} diff --git a/test/providers/ui_providers_test.dart b/test/providers/ui_providers_test.dart index 9b0653d3..82116586 100644 --- a/test/providers/ui_providers_test.dart +++ b/test/providers/ui_providers_test.dart @@ -217,7 +217,7 @@ void main() { // Verify that the EnvironmentPage is displayed expect(find.byType(EnvironmentPage), findsOneWidget); // Verify that the selected icon is the filled version (selectedIcon) - expect(find.byIcon(Icons.computer_rounded), findsOneWidget); + expect(find.byIcon(Icons.laptop_windows_outlined), findsOneWidget); // Go to HistoryPage container.read(navRailIndexStateProvider.notifier).state = 2; diff --git a/test/widgets/button_group_filled_test.dart b/test/widgets/button_group_filled_test.dart index 44fe9e6e..a69d2e95 100644 --- a/test/widgets/button_group_filled_test.dart +++ b/test/widgets/button_group_filled_test.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:apidash/consts.dart'; import 'package:apidash/widgets/button_group_filled.dart'; void main() { diff --git a/test_driver/integration_test.dart b/test_driver/integration_test.dart new file mode 100644 index 00000000..b38629cc --- /dev/null +++ b/test_driver/integration_test.dart @@ -0,0 +1,3 @@ +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); From 87bcb2719ac9053c3445af6a097801d215bac0f5 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Wed, 14 Aug 2024 15:15:14 +0530 Subject: [PATCH 32/71] test: env_manager_test --- integration_test/env_manager_test.dart | 86 ++++++++++++++++++++++---- integration_test/test_helper.dart | 8 ++- 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/integration_test/env_manager_test.dart b/integration_test/env_manager_test.dart index 21d9aa20..5aa4bef8 100644 --- a/integration_test/env_manager_test.dart +++ b/integration_test/env_manager_test.dart @@ -1,10 +1,16 @@ -import 'package:apidash/widgets/menu_item_card.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:apidash/app.dart'; import 'package:apidash/consts.dart'; - +import 'package:apidash/screens/screens.dart'; +import 'package:apidash/widgets/field_cell.dart'; +import 'package:apidash/widgets/menu_item_card.dart'; +import 'package:apidash/widgets/popup_menu_env.dart'; +import 'package:apidash/screens/common_widgets/env_trigger_options.dart'; +import 'package:apidash/screens/envvar/editor_pane/variables_pane.dart'; +import 'package:apidash/screens/home_page/editor_pane/editor_request.dart'; +import 'package:apidash/screens/home_page/editor_pane/url_card.dart'; import 'package:apidash/screens/envvar/environments_pane.dart'; import '../test/extensions/widget_tester_extensions.dart'; import 'test_helper.dart'; @@ -14,14 +20,11 @@ void main() async { apidashWidgetTest("Testing Environment Manager end-to-end", (WidgetTester tester, helper) async { await tester.pumpUntilFound(find.byType(DashApp)); - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(const Duration(seconds: 1)); /// Navigate to Environment Manager - Finder envNavbutton = find.byIcon(Icons.laptop_windows_outlined); - expect(envNavbutton, findsOneWidget); - await tester.tap(envNavbutton); - await tester.pumpAndSettle(); - await Future.delayed(const Duration(seconds: 1)); + await navigateByIcon(Icons.laptop_windows_outlined, tester); + await Future.delayed(const Duration(milliseconds: 500)); /// Create New Environment final newEnvButton = @@ -29,13 +32,9 @@ void main() async { newEnvButton.existsOnce(); await act.tap(newEnvButton); await tester.pumpAndSettle(); - await Future.delayed(const Duration(seconds: 1)); + await Future.delayed(const Duration(milliseconds: 500)); /// Open ItemCardMenu of the new environment - spot() - .spot() - .spot() - .existsAtLeastNTimes(2); Finder envItems = find.byType(EnvironmentItem); Finder newEnvItem = envItems.at(1); expect(find.descendant(of: newEnvItem, matching: find.text('untitled')), @@ -51,6 +50,67 @@ void main() async { await tester.enterText(newEnvItem, "New Environment"); await tester.testTextInput.receiveAction(TextInputAction.done); await tester.pump(); + await Future.delayed(const Duration(milliseconds: 500)); + + /// Edit Environment Variables + final envCells = find.descendant( + of: find.byType(EditEnvironmentVariables), + matching: find.byType(CellField)); + await tester.enterText(envCells.at(0), "test-key"); + await tester.enterText(envCells.at(1), "test-value"); + await Future.delayed(const Duration(milliseconds: 500)); + + /// Navigate to Request Editor + await navigateByIcon(Icons.auto_awesome_mosaic_outlined, tester); + await Future.delayed(const Duration(milliseconds: 500)); + + /// Create a new request + await act.tap( + spot().spot().spotText(kLabelPlusNew)); + await tester.pumpAndSettle(); + + /// Set active environment + await tester.tap(find.descendant( + of: find.byType(EnvironmentPopupMenu), + matching: find.byIcon(Icons.unfold_more))); + await tester.pumpAndSettle(); + + await tester.tap(find.text('New Environment').last); + await tester.pumpAndSettle(); + + /// Check if environment suggestions are working + await act.tap(spot().spot()); + tester.testTextInput.enterText("{{test-k"); + await tester.pumpAndSettle( + const Duration(milliseconds: 500)); // wait for suggestions + spot() + .spot() + .spotText('test-key') + .existsOnce(); + + /// Navigate to Environment Manager + await navigateByIcon(Icons.laptop_windows_outlined, tester); + await Future.delayed(const Duration(milliseconds: 500)); + + /// Delete the environment variable + final delButtons = find.descendant( + of: find.byType(EditEnvironmentVariables), + matching: find.byIcon(Icons.remove_circle)); + await tester.tap(delButtons.at(0)); + + /// Navigate back to Request Editor + await navigateByIcon(Icons.auto_awesome_mosaic_outlined, tester); + await Future.delayed(const Duration(milliseconds: 500)); + + /// Check if environment suggestions are shown + await act.tap(spot().spot()); + tester.testTextInput.enterText("{{test-k"); + await tester.pumpAndSettle( + const Duration(milliseconds: 500)); // wait for suggestions + spot() + .spot() + .spotText('test-key') + .doesNotExist(); await Future.delayed(const Duration(seconds: 2)); }); diff --git a/integration_test/test_helper.dart b/integration_test/test_helper.dart index 88b07f6c..6300d99f 100644 --- a/integration_test/test_helper.dart +++ b/integration_test/test_helper.dart @@ -1,6 +1,5 @@ -import 'dart:ui'; - import 'package:apidash/app.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -47,3 +46,8 @@ void apidashWidgetTest( semanticsEnabled: false, ); } + +Future navigateByIcon(IconData icon, WidgetTester tester) async { + await tester.tap(find.byIcon(icon)); + await tester.pumpAndSettle(); +} From 52d275ee8b2915107eebd9cd152cc417fb9ff0ed Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Fri, 16 Aug 2024 03:06:52 +0530 Subject: [PATCH 33/71] test: env_manager desktop --- .../{ => desktop}/env_manager_test.dart | 107 ++++++++++++------ integration_test/test_helper.dart | 2 +- 2 files changed, 75 insertions(+), 34 deletions(-) rename integration_test/{ => desktop}/env_manager_test.dart (50%) diff --git a/integration_test/env_manager_test.dart b/integration_test/desktop/env_manager_test.dart similarity index 50% rename from integration_test/env_manager_test.dart rename to integration_test/desktop/env_manager_test.dart index 5aa4bef8..0610580c 100644 --- a/integration_test/env_manager_test.dart +++ b/integration_test/desktop/env_manager_test.dart @@ -1,29 +1,38 @@ +import 'dart:ui'; + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:apidash/app.dart'; import 'package:apidash/consts.dart'; +import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/screens/screens.dart'; -import 'package:apidash/widgets/field_cell.dart'; -import 'package:apidash/widgets/menu_item_card.dart'; -import 'package:apidash/widgets/popup_menu_env.dart'; import 'package:apidash/screens/common_widgets/env_trigger_options.dart'; import 'package:apidash/screens/envvar/editor_pane/variables_pane.dart'; import 'package:apidash/screens/home_page/editor_pane/editor_request.dart'; import 'package:apidash/screens/home_page/editor_pane/url_card.dart'; import 'package:apidash/screens/envvar/environments_pane.dart'; -import '../test/extensions/widget_tester_extensions.dart'; -import 'test_helper.dart'; +import '../../test/extensions/widget_tester_extensions.dart'; +import '../test_helper.dart'; void main() async { + const environmentName = "test-env-name"; + const envVarName = "test-env-var"; + const envVarValue = "8700000"; + const testEndpoint = "api.apidash.dev/humanize/social?num="; + const unknown = "unknown"; + const untitled = "untitled"; + const expectedCurlCode = + "curl --url 'https://api.apidash.dev/humanize/social?num=8700000'"; + await ApidashTestHelper.initialize(); - apidashWidgetTest("Testing Environment Manager end-to-end", + apidashWidgetTest("Testing Environment Manager in desktop end-to-end", (WidgetTester tester, helper) async { await tester.pumpUntilFound(find.byType(DashApp)); await Future.delayed(const Duration(seconds: 1)); /// Navigate to Environment Manager - await navigateByIcon(Icons.laptop_windows_outlined, tester); + await navigateByIcon(tester, Icons.laptop_windows_outlined); await Future.delayed(const Duration(milliseconds: 500)); /// Create New Environment @@ -37,7 +46,7 @@ void main() async { /// Open ItemCardMenu of the new environment Finder envItems = find.byType(EnvironmentItem); Finder newEnvItem = envItems.at(1); - expect(find.descendant(of: newEnvItem, matching: find.text('untitled')), + expect(find.descendant(of: newEnvItem, matching: find.text(untitled)), findsOneWidget); Finder itemCardMenu = find.descendant(of: newEnvItem, matching: find.byType(ItemCardMenu)); @@ -45,9 +54,9 @@ void main() async { await tester.pumpAndSettle(); /// Rename the new environment - await tester.tap(find.text('Rename').last); + await tester.tap(find.text(ItemMenuOption.edit.label).last); await tester.pump(); - await tester.enterText(newEnvItem, "New Environment"); + await tester.enterText(newEnvItem, environmentName); await tester.testTextInput.receiveAction(TextInputAction.done); await tester.pump(); await Future.delayed(const Duration(milliseconds: 500)); @@ -56,13 +65,13 @@ void main() async { final envCells = find.descendant( of: find.byType(EditEnvironmentVariables), matching: find.byType(CellField)); - await tester.enterText(envCells.at(0), "test-key"); - await tester.enterText(envCells.at(1), "test-value"); + await tester.enterText(envCells.at(0), envVarName); + await tester.enterText(envCells.at(1), envVarValue); await Future.delayed(const Duration(milliseconds: 500)); /// Navigate to Request Editor - await navigateByIcon(Icons.auto_awesome_mosaic_outlined, tester); - await Future.delayed(const Duration(milliseconds: 500)); + await navigateByIcon(tester, Icons.auto_awesome_mosaic_outlined); + await Future.delayed(const Duration(milliseconds: 200)); /// Create a new request await act.tap( @@ -75,43 +84,75 @@ void main() async { matching: find.byIcon(Icons.unfold_more))); await tester.pumpAndSettle(); - await tester.tap(find.text('New Environment').last); + await tester.tap(find.text(environmentName).last); await tester.pumpAndSettle(); /// Check if environment suggestions are working await act.tap(spot().spot()); - tester.testTextInput.enterText("{{test-k"); + tester.testTextInput.enterText("$testEndpoint{{$envVarName"); await tester.pumpAndSettle( const Duration(milliseconds: 500)); // wait for suggestions - spot() + await act.tap(spot() .spot() - .spotText('test-key') - .existsOnce(); + .spotText(envVarValue)); + await tester.pumpAndSettle(); - /// Navigate to Environment Manager - await navigateByIcon(Icons.laptop_windows_outlined, tester); + final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: Offset.zero); + addTearDown(gesture.removePointer); + await tester.pump(); + + /// Check if environment variable is shown on hover + await gesture.moveTo(tester.getCenter(find.descendant( + of: find.byType(URLTextField), + matching: find.text('{{$envVarName}}')))); + await tester.pumpAndSettle(); + expect(find.text(envVarValue), findsOneWidget); await Future.delayed(const Duration(milliseconds: 500)); + /// Change codegen language to curl + await navigateByIcon(tester, Icons.settings_outlined); + await tester.tap(find.descendant( + of: find.byType(CodegenPopupMenu), + matching: find.byIcon(Icons.unfold_more))); + await tester.pumpAndSettle(); + + await tester.tap(find.text(CodegenLanguage.curl.label).last); + await tester.pumpAndSettle(); + await navigateByIcon(tester, Icons.auto_awesome_mosaic_outlined); + await Future.delayed(const Duration(milliseconds: 200)); + + /// Check variable substitution in request + await act.tap(spot().spotText(kLabelViewCode)); + await tester.pumpAndSettle(); + expect( + find.descendant( + of: find.byType(CodeGenPreviewer), + matching: find.text(expectedCurlCode)), + findsOneWidget); + + /// Navigate to Environment Manager + await navigateByIcon(tester, Icons.laptop_windows_outlined); + await Future.delayed(const Duration(milliseconds: 200)); + /// Delete the environment variable final delButtons = find.descendant( of: find.byType(EditEnvironmentVariables), matching: find.byIcon(Icons.remove_circle)); await tester.tap(delButtons.at(0)); + await Future.delayed(const Duration(milliseconds: 500)); /// Navigate back to Request Editor - await navigateByIcon(Icons.auto_awesome_mosaic_outlined, tester); - await Future.delayed(const Duration(milliseconds: 500)); + await navigateByIcon(tester, Icons.auto_awesome_mosaic_outlined); + await Future.delayed(const Duration(milliseconds: 200)); - /// Check if environment suggestions are shown - await act.tap(spot().spot()); - tester.testTextInput.enterText("{{test-k"); - await tester.pumpAndSettle( - const Duration(milliseconds: 500)); // wait for suggestions - spot() - .spot() - .spotText('test-key') - .doesNotExist(); + /// Check if environment variable is now shown on hover + await gesture.moveTo(tester.getCenter(find.descendant( + of: find.byType(URLTextField), + matching: find.text('{{$envVarName}}')))); + await tester.pumpAndSettle(); + expect(find.text(unknown), findsNWidgets(2)); - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(const Duration(milliseconds: 500)); }); } diff --git a/integration_test/test_helper.dart b/integration_test/test_helper.dart index 6300d99f..f92a106c 100644 --- a/integration_test/test_helper.dart +++ b/integration_test/test_helper.dart @@ -47,7 +47,7 @@ void apidashWidgetTest( ); } -Future navigateByIcon(IconData icon, WidgetTester tester) async { +Future navigateByIcon(WidgetTester tester, IconData icon) async { await tester.tap(find.byIcon(icon)); await tester.pumpAndSettle(); } From 833494540fc407d840c016f2599cb110886c0a65 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Fri, 16 Aug 2024 03:45:57 +0530 Subject: [PATCH 34/71] test: env_manager mobile --- .../desktop/env_manager_test.dart | 2 +- integration_test/mobile/env_manager_test.dart | 186 ++++++++++++++++++ lib/main.dart | 2 +- .../common_widgets/common_widgets.dart | 1 + lib/screens/mobile/navbar.dart | 4 +- 5 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 integration_test/mobile/env_manager_test.dart diff --git a/integration_test/desktop/env_manager_test.dart b/integration_test/desktop/env_manager_test.dart index 0610580c..28327d6c 100644 --- a/integration_test/desktop/env_manager_test.dart +++ b/integration_test/desktop/env_manager_test.dart @@ -146,7 +146,7 @@ void main() async { await navigateByIcon(tester, Icons.auto_awesome_mosaic_outlined); await Future.delayed(const Duration(milliseconds: 200)); - /// Check if environment variable is now shown on hover + /// Check if environment variable is now shown on hover await gesture.moveTo(tester.getCenter(find.descendant( of: find.byType(URLTextField), matching: find.text('{{$envVarName}}')))); diff --git a/integration_test/mobile/env_manager_test.dart b/integration_test/mobile/env_manager_test.dart new file mode 100644 index 00000000..ea4632e4 --- /dev/null +++ b/integration_test/mobile/env_manager_test.dart @@ -0,0 +1,186 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; +import 'package:apidash/app.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/screens/screens.dart'; +import 'package:apidash/screens/common_widgets/env_trigger_options.dart'; +import 'package:apidash/screens/envvar/editor_pane/variables_pane.dart'; +import 'package:apidash/screens/home_page/editor_pane/url_card.dart'; +import 'package:apidash/screens/envvar/environments_pane.dart'; +import '../../test/extensions/widget_tester_extensions.dart'; +import '../test_helper.dart'; + +void main() async { + const environmentName = "test-env-name"; + const envVarName = "test-env-var"; + const envVarValue = "8700000"; + const testEndpoint = "api.apidash.dev/humanize/social?num="; + const unknown = "unknown"; + const untitled = "untitled"; + const expectedCurlCode = + "curl --url 'https://api.apidash.dev/humanize/social?num=8700000'"; + + await ApidashTestHelper.initialize( + size: Size(kCompactWindowWidth, kMinWindowSize.height)); + apidashWidgetTest("Testing Environment Manager in desktop end-to-end", + (WidgetTester tester, helper) async { + await tester.pumpUntilFound(find.byType(DashApp)); + await Future.delayed(const Duration(seconds: 1)); + + kHomeScaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + + /// Navigate to Environment Manager + await navigateByIcon(tester, Icons.laptop_windows_outlined); + await Future.delayed(const Duration(milliseconds: 500)); + + kEnvScaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + + /// Create New Environment + final newEnvButton = + spot().spot().spotText(kLabelPlusNew); + newEnvButton.existsOnce(); + await act.tap(newEnvButton); + await tester.pumpAndSettle(); + await Future.delayed(const Duration(milliseconds: 500)); + + /// Open ItemCardMenu of the new environment + Finder envItems = find.byType(EnvironmentItem); + Finder newEnvItem = envItems.at(1); + expect(find.descendant(of: newEnvItem, matching: find.text(untitled)), + findsOneWidget); + Finder itemCardMenu = + find.descendant(of: newEnvItem, matching: find.byType(ItemCardMenu)); + await tester.tap(itemCardMenu); + await tester.pumpAndSettle(); + + /// Rename the new environment + await tester.tap(find.text(ItemMenuOption.edit.label).last); + await tester.pump(); + await tester.enterText(newEnvItem, environmentName); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pump(); + await Future.delayed(const Duration(milliseconds: 500)); + + kEnvScaffoldKey.currentState!.closeDrawer(); + await tester.pumpAndSettle(); + + /// Edit Environment Variables + final envCells = find.descendant( + of: find.byType(EditEnvironmentVariables), + matching: find.byType(CellField)); + await tester.enterText(envCells.at(0), envVarName); + await tester.enterText(envCells.at(1), envVarValue); + await Future.delayed(const Duration(milliseconds: 500)); + + kEnvScaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + + /// Navigate to Request Editor + await navigateByIcon(tester, Icons.auto_awesome_mosaic_outlined); + await Future.delayed(const Duration(milliseconds: 200)); + + kHomeScaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + + /// Create a new request + await act.tap( + spot().spot().spotText(kLabelPlusNew)); + await tester.pumpAndSettle(); + + kHomeScaffoldKey.currentState!.closeDrawer(); + await tester.pumpAndSettle(); + + /// Set active environment + await tester.tap(find.descendant( + of: find.byType(EnvironmentPopupMenu), + matching: find.byIcon(Icons.unfold_more))); + await tester.pumpAndSettle(); + + await tester.tap(find.text(environmentName).last); + await tester.pumpAndSettle(); + + /// Check if environment suggestions are working + await act.tap(spot()); + tester.testTextInput.enterText("$testEndpoint{{$envVarName"); + await tester.pumpAndSettle( + const Duration(milliseconds: 500)); // wait for suggestions + await act.tap(spot() + .spot() + .spotText(envVarValue)); + await tester.pumpAndSettle(); + + final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: Offset.zero); + addTearDown(gesture.removePointer); + await tester.pump(); + + /// Check if environment variable is shown on hover + await gesture.moveTo(tester.getCenter(find.descendant( + of: find.byType(URLTextField), + matching: find.text('{{$envVarName}}')))); + await tester.pumpAndSettle(); + expect(find.text(envVarValue), findsOneWidget); + await gesture.moveBy(const Offset(0, 100)); + await Future.delayed(const Duration(milliseconds: 500)); + + kHomeScaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + + /// Change codegen language to curl + await navigateByIcon(tester, Icons.settings_outlined); + await tester.tap(find.descendant( + of: find.byType(CodegenPopupMenu), + matching: find.byIcon(Icons.unfold_more))); + await tester.pumpAndSettle(); + + await tester.tap(find.text(CodegenLanguage.curl.label).last); + await tester.pumpAndSettle(); + await navigateByIcon(tester, Icons.auto_awesome_mosaic_outlined); + await Future.delayed(const Duration(milliseconds: 200)); + + /// Check variable substitution in request + await act.tap(spot().spotText(kLabelCode)); + await tester.pumpAndSettle(); + expect( + find.descendant( + of: find.byType(CodeGenPreviewer), + matching: find.text(expectedCurlCode)), + findsOneWidget); + + kHomeScaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + + /// Navigate to Environment Manager + await navigateByIcon(tester, Icons.laptop_windows_outlined); + await Future.delayed(const Duration(milliseconds: 200)); + + /// Delete the environment variable + final delButtons = find.descendant( + of: find.byType(EditEnvironmentVariables), + matching: find.byIcon(Icons.remove_circle)); + await tester.tap(delButtons.at(0)); + await Future.delayed(const Duration(milliseconds: 500)); + + kEnvScaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + + /// Navigate back to Request Editor + await navigateByIcon(tester, Icons.auto_awesome_mosaic_outlined); + await Future.delayed(const Duration(milliseconds: 200)); + + /// Check if environment variable is now shown on hover + await gesture.moveTo(tester.getCenter(find.descendant( + of: find.byType(URLTextField), + matching: find.text('{{$envVarName}}')))); + await tester.pumpAndSettle(); + expect(find.text(unknown), findsNWidgets(2)); + + await Future.delayed(const Duration(milliseconds: 500)); + }); +} diff --git a/lib/main.dart b/lib/main.dart index 54b521ca..4a8b07fd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,7 +29,7 @@ Future initWindow({Size? sz}) async { await setupInitialWindow(sz: sz); } if (kIsMacOS || kIsWindows) { - var win = sz == null ? (sz, null) : getInitialSize(); + var win = sz != null ? (sz, const Offset(100, 100)) : getInitialSize(); await setupWindow(sz: win.$1, off: win.$2); } } diff --git a/lib/screens/common_widgets/common_widgets.dart b/lib/screens/common_widgets/common_widgets.dart index 02a1f583..ee150dd8 100644 --- a/lib/screens/common_widgets/common_widgets.dart +++ b/lib/screens/common_widgets/common_widgets.dart @@ -8,6 +8,7 @@ export 'environment_dropdown.dart'; export 'envvar_indicator.dart'; export 'envvar_span.dart'; export 'envvar_popover.dart'; +export 'env_trigger_options.dart'; export 'sidebar_filter.dart'; export 'sidebar_header.dart'; export 'sidebar_save_button.dart'; diff --git a/lib/screens/mobile/navbar.dart b/lib/screens/mobile/navbar.dart index b5402b88..b1495b6a 100644 --- a/lib/screens/mobile/navbar.dart +++ b/lib/screens/mobile/navbar.dart @@ -34,8 +34,8 @@ class BottomNavBar extends ConsumerWidget { child: NavbarButton( railIdx: railIdx, buttonIdx: 0, - selectedIcon: Icons.dashboard, - icon: Icons.dashboard_outlined, + selectedIcon: Icons.auto_awesome_mosaic_rounded, + icon: Icons.auto_awesome_mosaic_outlined, label: 'Requests', ), ), From b16829735094ff93c25816939246e53f8d31a618 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sun, 18 Aug 2024 03:58:53 +0530 Subject: [PATCH 35/71] fix: pass size param --- integration_test/desktop/env_manager_test.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/integration_test/desktop/env_manager_test.dart b/integration_test/desktop/env_manager_test.dart index 28327d6c..047e2d33 100644 --- a/integration_test/desktop/env_manager_test.dart +++ b/integration_test/desktop/env_manager_test.dart @@ -25,7 +25,8 @@ void main() async { const expectedCurlCode = "curl --url 'https://api.apidash.dev/humanize/social?num=8700000'"; - await ApidashTestHelper.initialize(); + await ApidashTestHelper.initialize( + size: Size(kExpandedWindowWidth, kMinWindowSize.height)); apidashWidgetTest("Testing Environment Manager in desktop end-to-end", (WidgetTester tester, helper) async { await tester.pumpUntilFound(find.byType(DashApp)); From ffcd41efaa149ca71acebfe1b2ecc0df660aee66 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sun, 18 Aug 2024 04:24:37 +0530 Subject: [PATCH 36/71] refactor: abstract to helper --- .../desktop/env_manager_test.dart | 79 ++++----------- integration_test/env_helper.dart | 71 ++++++++++++++ integration_test/mobile/env_manager_test.dart | 95 ++++--------------- integration_test/req_helper.dart | 39 ++++++++ integration_test/test_helper.dart | 85 +++++++++++++++-- 5 files changed, 224 insertions(+), 145 deletions(-) create mode 100644 integration_test/env_helper.dart create mode 100644 integration_test/req_helper.dart diff --git a/integration_test/desktop/env_manager_test.dart b/integration_test/desktop/env_manager_test.dart index 28327d6c..a3743501 100644 --- a/integration_test/desktop/env_manager_test.dart +++ b/integration_test/desktop/env_manager_test.dart @@ -6,12 +6,9 @@ import 'package:spot/spot.dart'; import 'package:apidash/app.dart'; import 'package:apidash/consts.dart'; import 'package:apidash/widgets/widgets.dart'; -import 'package:apidash/screens/screens.dart'; import 'package:apidash/screens/common_widgets/env_trigger_options.dart'; -import 'package:apidash/screens/envvar/editor_pane/variables_pane.dart'; import 'package:apidash/screens/home_page/editor_pane/editor_request.dart'; import 'package:apidash/screens/home_page/editor_pane/url_card.dart'; -import 'package:apidash/screens/envvar/environments_pane.dart'; import '../../test/extensions/widget_tester_extensions.dart'; import '../test_helper.dart'; @@ -19,73 +16,42 @@ void main() async { const environmentName = "test-env-name"; const envVarName = "test-env-var"; const envVarValue = "8700000"; - const testEndpoint = "api.apidash.dev/humanize/social?num="; + const testEndpoint = "https://api.apidash.dev/humanize/social?num="; const unknown = "unknown"; - const untitled = "untitled"; - const expectedCurlCode = - "curl --url 'https://api.apidash.dev/humanize/social?num=8700000'"; + const expectedCurlCode = "curl --url '$testEndpoint$envVarValue'"; - await ApidashTestHelper.initialize(); + await ApidashTestHelper.initialize( + size: Size(kExpandedWindowWidth, kMinWindowSize.height)); apidashWidgetTest("Testing Environment Manager in desktop end-to-end", (WidgetTester tester, helper) async { await tester.pumpUntilFound(find.byType(DashApp)); await Future.delayed(const Duration(seconds: 1)); /// Navigate to Environment Manager - await navigateByIcon(tester, Icons.laptop_windows_outlined); + await helper.navigateToEnvironmentManager(); await Future.delayed(const Duration(milliseconds: 500)); /// Create New Environment - final newEnvButton = - spot().spot().spotText(kLabelPlusNew); - newEnvButton.existsOnce(); - await act.tap(newEnvButton); - await tester.pumpAndSettle(); + await helper.envHelper.addNewEnvironment(); await Future.delayed(const Duration(milliseconds: 500)); - /// Open ItemCardMenu of the new environment - Finder envItems = find.byType(EnvironmentItem); - Finder newEnvItem = envItems.at(1); - expect(find.descendant(of: newEnvItem, matching: find.text(untitled)), - findsOneWidget); - Finder itemCardMenu = - find.descendant(of: newEnvItem, matching: find.byType(ItemCardMenu)); - await tester.tap(itemCardMenu); - await tester.pumpAndSettle(); - /// Rename the new environment - await tester.tap(find.text(ItemMenuOption.edit.label).last); - await tester.pump(); - await tester.enterText(newEnvItem, environmentName); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pump(); + await helper.envHelper.renameNewEnvironment(environmentName); await Future.delayed(const Duration(milliseconds: 500)); - /// Edit Environment Variables - final envCells = find.descendant( - of: find.byType(EditEnvironmentVariables), - matching: find.byType(CellField)); - await tester.enterText(envCells.at(0), envVarName); - await tester.enterText(envCells.at(1), envVarValue); + /// Add Environment Variables + await helper.envHelper.addEnvironmentVariables([(envVarName, envVarValue)]); await Future.delayed(const Duration(milliseconds: 500)); /// Navigate to Request Editor - await navigateByIcon(tester, Icons.auto_awesome_mosaic_outlined); + await helper.navigateToRequestEditor(); await Future.delayed(const Duration(milliseconds: 200)); /// Create a new request - await act.tap( - spot().spot().spotText(kLabelPlusNew)); - await tester.pumpAndSettle(); + await helper.reqHelper.addNewRequest(); /// Set active environment - await tester.tap(find.descendant( - of: find.byType(EnvironmentPopupMenu), - matching: find.byIcon(Icons.unfold_more))); - await tester.pumpAndSettle(); - - await tester.tap(find.text(environmentName).last); - await tester.pumpAndSettle(); + await helper.envHelper.setActiveEnvironment(environmentName); /// Check if environment suggestions are working await act.tap(spot().spot()); @@ -110,16 +76,12 @@ void main() async { expect(find.text(envVarValue), findsOneWidget); await Future.delayed(const Duration(milliseconds: 500)); + await helper.navigateToSettings(); + /// Change codegen language to curl - await navigateByIcon(tester, Icons.settings_outlined); - await tester.tap(find.descendant( - of: find.byType(CodegenPopupMenu), - matching: find.byIcon(Icons.unfold_more))); - await tester.pumpAndSettle(); + await helper.changeCodegenLanguage(CodegenLanguage.curl); - await tester.tap(find.text(CodegenLanguage.curl.label).last); - await tester.pumpAndSettle(); - await navigateByIcon(tester, Icons.auto_awesome_mosaic_outlined); + await helper.navigateToRequestEditor(); await Future.delayed(const Duration(milliseconds: 200)); /// Check variable substitution in request @@ -132,18 +94,15 @@ void main() async { findsOneWidget); /// Navigate to Environment Manager - await navigateByIcon(tester, Icons.laptop_windows_outlined); + await helper.navigateToEnvironmentManager(); await Future.delayed(const Duration(milliseconds: 200)); /// Delete the environment variable - final delButtons = find.descendant( - of: find.byType(EditEnvironmentVariables), - matching: find.byIcon(Icons.remove_circle)); - await tester.tap(delButtons.at(0)); + await helper.envHelper.deleteFirstEnvironmentVariable(); await Future.delayed(const Duration(milliseconds: 500)); /// Navigate back to Request Editor - await navigateByIcon(tester, Icons.auto_awesome_mosaic_outlined); + await helper.navigateToRequestEditor(); await Future.delayed(const Duration(milliseconds: 200)); /// Check if environment variable is now shown on hover diff --git a/integration_test/env_helper.dart b/integration_test/env_helper.dart new file mode 100644 index 00000000..a2acfb23 --- /dev/null +++ b/integration_test/env_helper.dart @@ -0,0 +1,71 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/material.dart'; +import 'package:spot/spot.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/screens/envvar/environments_pane.dart'; +import 'package:apidash/screens/envvar/editor_pane/variables_pane.dart'; + +class ApidashTestEnvHelper { + final WidgetTester tester; + + ApidashTestEnvHelper(this.tester); + + Future addNewEnvironment({bool isMobile = false}) async { + if (isMobile) { + kEnvScaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + } + final newEnvButton = + spot().spot().spotText(kLabelPlusNew); + newEnvButton.existsOnce(); + await act.tap(newEnvButton); + await tester.pumpAndSettle(); + } + + Future renameNewEnvironment(String newEnvName) async { + Finder envItems = find.byType(EnvironmentItem); + Finder newEnvItem = envItems.at(1); + expect(find.descendant(of: newEnvItem, matching: find.text("untitled")), + findsOneWidget); + Finder itemCardMenu = + find.descendant(of: newEnvItem, matching: find.byType(ItemCardMenu)); + await tester.tap(itemCardMenu); + await tester.pumpAndSettle(); + + await tester.tap(find.text(ItemMenuOption.edit.label).last); + await tester.pump(); + await tester.enterText(newEnvItem, newEnvName); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pump(); + } + + Future addEnvironmentVariables( + List<(String, String)> keyValuePairs) async { + final envCells = find.descendant( + of: find.byType(EditEnvironmentVariables), + matching: find.byType(CellField)); + for (var i = 0; i < keyValuePairs.length; i++) { + await tester.enterText(envCells.at(i), keyValuePairs[i].$1); + await tester.enterText(envCells.at(i + 1), keyValuePairs[i].$2); + } + } + + Future deleteFirstEnvironmentVariable() async { + final delButtons = find.descendant( + of: find.byType(EditEnvironmentVariables), + matching: find.byIcon(Icons.remove_circle)); + await tester.tap(delButtons.at(0)); + await tester.pump(); + } + + Future setActiveEnvironment(String envName) async { + await tester.tap(find.descendant( + of: find.byType(EnvironmentPopupMenu), + matching: find.byIcon(Icons.unfold_more))); + await tester.pumpAndSettle(); + + await tester.tap(find.text(envName).last); + await tester.pumpAndSettle(); + } +} diff --git a/integration_test/mobile/env_manager_test.dart b/integration_test/mobile/env_manager_test.dart index ea4632e4..491e4653 100644 --- a/integration_test/mobile/env_manager_test.dart +++ b/integration_test/mobile/env_manager_test.dart @@ -6,11 +6,8 @@ import 'package:spot/spot.dart'; import 'package:apidash/app.dart'; import 'package:apidash/consts.dart'; import 'package:apidash/widgets/widgets.dart'; -import 'package:apidash/screens/screens.dart'; import 'package:apidash/screens/common_widgets/env_trigger_options.dart'; -import 'package:apidash/screens/envvar/editor_pane/variables_pane.dart'; import 'package:apidash/screens/home_page/editor_pane/url_card.dart'; -import 'package:apidash/screens/envvar/environments_pane.dart'; import '../../test/extensions/widget_tester_extensions.dart'; import '../test_helper.dart'; @@ -18,11 +15,9 @@ void main() async { const environmentName = "test-env-name"; const envVarName = "test-env-var"; const envVarValue = "8700000"; - const testEndpoint = "api.apidash.dev/humanize/social?num="; + const testEndpoint = "https://api.apidash.dev/humanize/social?num="; const unknown = "unknown"; - const untitled = "untitled"; - const expectedCurlCode = - "curl --url 'https://api.apidash.dev/humanize/social?num=8700000'"; + const expectedCurlCode = "curl --url '$testEndpoint$envVarValue'"; await ApidashTestHelper.initialize( size: Size(kCompactWindowWidth, kMinWindowSize.height)); @@ -31,79 +26,37 @@ void main() async { await tester.pumpUntilFound(find.byType(DashApp)); await Future.delayed(const Duration(seconds: 1)); - kHomeScaffoldKey.currentState!.openDrawer(); - await tester.pumpAndSettle(); - /// Navigate to Environment Manager - await navigateByIcon(tester, Icons.laptop_windows_outlined); + await helper.navigateToEnvironmentManager(scaffoldKey: kHomeScaffoldKey); await Future.delayed(const Duration(milliseconds: 500)); - kEnvScaffoldKey.currentState!.openDrawer(); - await tester.pumpAndSettle(); - /// Create New Environment - final newEnvButton = - spot().spot().spotText(kLabelPlusNew); - newEnvButton.existsOnce(); - await act.tap(newEnvButton); - await tester.pumpAndSettle(); + await helper.envHelper.addNewEnvironment(isMobile: true); await Future.delayed(const Duration(milliseconds: 500)); - /// Open ItemCardMenu of the new environment - Finder envItems = find.byType(EnvironmentItem); - Finder newEnvItem = envItems.at(1); - expect(find.descendant(of: newEnvItem, matching: find.text(untitled)), - findsOneWidget); - Finder itemCardMenu = - find.descendant(of: newEnvItem, matching: find.byType(ItemCardMenu)); - await tester.tap(itemCardMenu); - await tester.pumpAndSettle(); - /// Rename the new environment - await tester.tap(find.text(ItemMenuOption.edit.label).last); - await tester.pump(); - await tester.enterText(newEnvItem, environmentName); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pump(); + await helper.envHelper.renameNewEnvironment(environmentName); await Future.delayed(const Duration(milliseconds: 500)); kEnvScaffoldKey.currentState!.closeDrawer(); await tester.pumpAndSettle(); - /// Edit Environment Variables - final envCells = find.descendant( - of: find.byType(EditEnvironmentVariables), - matching: find.byType(CellField)); - await tester.enterText(envCells.at(0), envVarName); - await tester.enterText(envCells.at(1), envVarValue); + /// Add Environment Variables + await helper.envHelper.addEnvironmentVariables([(envVarName, envVarValue)]); await Future.delayed(const Duration(milliseconds: 500)); - kEnvScaffoldKey.currentState!.openDrawer(); - await tester.pumpAndSettle(); - /// Navigate to Request Editor - await navigateByIcon(tester, Icons.auto_awesome_mosaic_outlined); + await helper.navigateToRequestEditor(scaffoldKey: kEnvScaffoldKey); await Future.delayed(const Duration(milliseconds: 200)); - kHomeScaffoldKey.currentState!.openDrawer(); - await tester.pumpAndSettle(); - /// Create a new request - await act.tap( - spot().spot().spotText(kLabelPlusNew)); - await tester.pumpAndSettle(); + await helper.reqHelper.addNewRequest(isMobile: true); kHomeScaffoldKey.currentState!.closeDrawer(); await tester.pumpAndSettle(); /// Set active environment - await tester.tap(find.descendant( - of: find.byType(EnvironmentPopupMenu), - matching: find.byIcon(Icons.unfold_more))); - await tester.pumpAndSettle(); - - await tester.tap(find.text(environmentName).last); - await tester.pumpAndSettle(); + await helper.envHelper.setActiveEnvironment(environmentName); /// Check if environment suggestions are working await act.tap(spot()); @@ -129,19 +82,12 @@ void main() async { await gesture.moveBy(const Offset(0, 100)); await Future.delayed(const Duration(milliseconds: 500)); - kHomeScaffoldKey.currentState!.openDrawer(); - await tester.pumpAndSettle(); + await helper.navigateToSettings(scaffoldKey: kHomeScaffoldKey); /// Change codegen language to curl - await navigateByIcon(tester, Icons.settings_outlined); - await tester.tap(find.descendant( - of: find.byType(CodegenPopupMenu), - matching: find.byIcon(Icons.unfold_more))); - await tester.pumpAndSettle(); + await helper.changeCodegenLanguage(CodegenLanguage.curl); - await tester.tap(find.text(CodegenLanguage.curl.label).last); - await tester.pumpAndSettle(); - await navigateByIcon(tester, Icons.auto_awesome_mosaic_outlined); + await helper.navigateToRequestEditor(); await Future.delayed(const Duration(milliseconds: 200)); /// Check variable substitution in request @@ -153,25 +99,16 @@ void main() async { matching: find.text(expectedCurlCode)), findsOneWidget); - kHomeScaffoldKey.currentState!.openDrawer(); - await tester.pumpAndSettle(); - /// Navigate to Environment Manager - await navigateByIcon(tester, Icons.laptop_windows_outlined); + await helper.navigateToEnvironmentManager(scaffoldKey: kHomeScaffoldKey); await Future.delayed(const Duration(milliseconds: 200)); /// Delete the environment variable - final delButtons = find.descendant( - of: find.byType(EditEnvironmentVariables), - matching: find.byIcon(Icons.remove_circle)); - await tester.tap(delButtons.at(0)); + await helper.envHelper.deleteFirstEnvironmentVariable(); await Future.delayed(const Duration(milliseconds: 500)); - kEnvScaffoldKey.currentState!.openDrawer(); - await tester.pumpAndSettle(); - /// Navigate back to Request Editor - await navigateByIcon(tester, Icons.auto_awesome_mosaic_outlined); + await helper.navigateToRequestEditor(scaffoldKey: kEnvScaffoldKey); await Future.delayed(const Duration(milliseconds: 200)); /// Check if environment variable is now shown on hover diff --git a/integration_test/req_helper.dart b/integration_test/req_helper.dart new file mode 100644 index 00000000..d32680ce --- /dev/null +++ b/integration_test/req_helper.dart @@ -0,0 +1,39 @@ +import 'package:apidash/consts.dart'; +import 'package:apidash/screens/home_page/collection_pane.dart'; +import 'package:apidash/widgets/menu_item_card.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; + +class ApidashTestRequestHelper { + final WidgetTester tester; + + ApidashTestRequestHelper(this.tester); + + Future addNewRequest({bool isMobile = false}) async { + if (isMobile) { + kHomeScaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + } + await act.tap( + spot().spot().spotText(kLabelPlusNew)); + await tester.pumpAndSettle(); + } + + Future renameNewRequest(String newReqName) async { + Finder reqItems = find.byType(RequestItem); + Finder newReqItem = reqItems.at(0); + expect(find.descendant(of: newReqItem, matching: find.text("untitled")), + findsOneWidget); + Finder itemCardMenu = + find.descendant(of: newReqItem, matching: find.byType(ItemCardMenu)); + await tester.tap(itemCardMenu); + await tester.pumpAndSettle(); + + await tester.tap(find.text(ItemMenuOption.edit.label).last); + await tester.pump(); + await tester.enterText(newReqItem, newReqName); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pump(); + } +} diff --git a/integration_test/test_helper.dart b/integration_test/test_helper.dart index f92a106c..24453d91 100644 --- a/integration_test/test_helper.dart +++ b/integration_test/test_helper.dart @@ -1,16 +1,34 @@ import 'package:apidash/app.dart'; -import 'package:flutter/widgets.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:integration_test/integration_test.dart'; import 'package:apidash/main.dart' as app; +import 'env_helper.dart'; +import 'req_helper.dart'; + class ApidashTestHelper { final WidgetTester tester; ApidashTestHelper(this.tester); + ApidashTestRequestHelper? _reqHelper; + ApidashTestEnvHelper? _envHelper; + + ApidashTestRequestHelper get reqHelper { + _reqHelper ??= ApidashTestRequestHelper(tester); + return _reqHelper!; + } + + ApidashTestEnvHelper get envHelper { + _envHelper ??= ApidashTestEnvHelper(tester); + return _envHelper!; + } + static Future initialize( {Size? size}) async { final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -30,6 +48,66 @@ class ApidashTestHelper { ), ); } + + Future navigateToRequestEditor( + {GlobalKey? scaffoldKey}) async { + if (scaffoldKey != null) { + scaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + } + await tester.tap(find.byIcon(Icons.auto_awesome_mosaic_outlined)); + await tester.pumpAndSettle(); + } + + Future navigateToEnvironmentManager( + {GlobalKey? scaffoldKey}) async { + if (scaffoldKey != null) { + scaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + } + await tester.tap(find.byIcon(Icons.laptop_windows_outlined)); + await tester.pumpAndSettle(); + } + + Future navigateToHistory( + {GlobalKey? scaffoldKey}) async { + if (scaffoldKey != null) { + scaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + } + await tester.tap(find.byIcon(Icons.history_outlined)); + await tester.pumpAndSettle(); + } + + Future navigateToSettings( + {GlobalKey? scaffoldKey}) async { + if (scaffoldKey != null) { + scaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + } + await tester.tap(find.byIcon(Icons.settings_outlined)); + await tester.pumpAndSettle(); + } + + Future changeURIScheme(String scheme) async { + await tester.tap(find.descendant( + of: find.byType(URIPopupMenu), + matching: find.byIcon(Icons.unfold_more))); + await tester.pumpAndSettle(); + + await tester.tap(find.text(scheme).last); + await tester.pumpAndSettle(); + } + + Future changeCodegenLanguage(CodegenLanguage language) async { + await tester.tap(find.descendant( + of: find.byType(CodegenPopupMenu), + matching: find.byIcon(Icons.unfold_more))); + await tester.pumpAndSettle(); + + await tester.tap(find.text(language.label).last); + await tester.pumpAndSettle(); + } } @isTest @@ -46,8 +124,3 @@ void apidashWidgetTest( semanticsEnabled: false, ); } - -Future navigateByIcon(WidgetTester tester, IconData icon) async { - await tester.tap(find.byIcon(icon)); - await tester.pumpAndSettle(); -} From dc8581ea3f4ddc2a521e26867ff6aed1e8e20a2e Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sun, 18 Aug 2024 03:58:53 +0530 Subject: [PATCH 37/71] fix: pass size param From 02b7546cf5673fecebbdcbff2b211931d93cfa6b Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sun, 18 Aug 2024 21:34:11 +0530 Subject: [PATCH 38/71] fix: hover issue --- integration_test/desktop/env_manager_test.dart | 4 +--- integration_test/mobile/env_manager_test.dart | 6 +++--- integration_test/req_helper.dart | 6 ++++++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/integration_test/desktop/env_manager_test.dart b/integration_test/desktop/env_manager_test.dart index a3743501..b68d168c 100644 --- a/integration_test/desktop/env_manager_test.dart +++ b/integration_test/desktop/env_manager_test.dart @@ -7,7 +7,6 @@ import 'package:apidash/app.dart'; import 'package:apidash/consts.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/screens/common_widgets/env_trigger_options.dart'; -import 'package:apidash/screens/home_page/editor_pane/editor_request.dart'; import 'package:apidash/screens/home_page/editor_pane/url_card.dart'; import '../../test/extensions/widget_tester_extensions.dart'; import '../test_helper.dart'; @@ -54,8 +53,7 @@ void main() async { await helper.envHelper.setActiveEnvironment(environmentName); /// Check if environment suggestions are working - await act.tap(spot().spot()); - tester.testTextInput.enterText("$testEndpoint{{$envVarName"); + await helper.reqHelper.addRequestURL("$testEndpoint{{$envVarName"); await tester.pumpAndSettle( const Duration(milliseconds: 500)); // wait for suggestions await act.tap(spot() diff --git a/integration_test/mobile/env_manager_test.dart b/integration_test/mobile/env_manager_test.dart index 491e4653..80ea165b 100644 --- a/integration_test/mobile/env_manager_test.dart +++ b/integration_test/mobile/env_manager_test.dart @@ -15,7 +15,8 @@ void main() async { const environmentName = "test-env-name"; const envVarName = "test-env-var"; const envVarValue = "8700000"; - const testEndpoint = "https://api.apidash.dev/humanize/social?num="; + // TODO: Hover on variable doesn't work in test for long URLs + const testEndpoint = "https://api.apidash.dev?num="; const unknown = "unknown"; const expectedCurlCode = "curl --url '$testEndpoint$envVarValue'"; @@ -59,8 +60,7 @@ void main() async { await helper.envHelper.setActiveEnvironment(environmentName); /// Check if environment suggestions are working - await act.tap(spot()); - tester.testTextInput.enterText("$testEndpoint{{$envVarName"); + await helper.reqHelper.addRequestURL("$testEndpoint{{$envVarName"); await tester.pumpAndSettle( const Duration(milliseconds: 500)); // wait for suggestions await act.tap(spot() diff --git a/integration_test/req_helper.dart b/integration_test/req_helper.dart index d32680ce..c72f0af5 100644 --- a/integration_test/req_helper.dart +++ b/integration_test/req_helper.dart @@ -1,5 +1,6 @@ import 'package:apidash/consts.dart'; import 'package:apidash/screens/home_page/collection_pane.dart'; +import 'package:apidash/screens/home_page/editor_pane/url_card.dart'; import 'package:apidash/widgets/menu_item_card.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -36,4 +37,9 @@ class ApidashTestRequestHelper { await tester.testTextInput.receiveAction(TextInputAction.done); await tester.pump(); } + + Future addRequestURL(String url) async { + await act.tap(spot()); + tester.testTextInput.enterText(url); + } } From a676de6389ce25ac03f73f504e7d6345304e7fcc Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sun, 18 Aug 2024 04:24:37 +0530 Subject: [PATCH 39/71] refactor: abstract to helper --- .../desktop/env_manager_test.dart | 76 ++++----------- integration_test/env_helper.dart | 71 ++++++++++++++ integration_test/mobile/env_manager_test.dart | 95 ++++--------------- integration_test/req_helper.dart | 39 ++++++++ integration_test/test_helper.dart | 85 +++++++++++++++-- 5 files changed, 222 insertions(+), 144 deletions(-) create mode 100644 integration_test/env_helper.dart create mode 100644 integration_test/req_helper.dart diff --git a/integration_test/desktop/env_manager_test.dart b/integration_test/desktop/env_manager_test.dart index 047e2d33..a3743501 100644 --- a/integration_test/desktop/env_manager_test.dart +++ b/integration_test/desktop/env_manager_test.dart @@ -6,12 +6,9 @@ import 'package:spot/spot.dart'; import 'package:apidash/app.dart'; import 'package:apidash/consts.dart'; import 'package:apidash/widgets/widgets.dart'; -import 'package:apidash/screens/screens.dart'; import 'package:apidash/screens/common_widgets/env_trigger_options.dart'; -import 'package:apidash/screens/envvar/editor_pane/variables_pane.dart'; import 'package:apidash/screens/home_page/editor_pane/editor_request.dart'; import 'package:apidash/screens/home_page/editor_pane/url_card.dart'; -import 'package:apidash/screens/envvar/environments_pane.dart'; import '../../test/extensions/widget_tester_extensions.dart'; import '../test_helper.dart'; @@ -19,11 +16,9 @@ void main() async { const environmentName = "test-env-name"; const envVarName = "test-env-var"; const envVarValue = "8700000"; - const testEndpoint = "api.apidash.dev/humanize/social?num="; + const testEndpoint = "https://api.apidash.dev/humanize/social?num="; const unknown = "unknown"; - const untitled = "untitled"; - const expectedCurlCode = - "curl --url 'https://api.apidash.dev/humanize/social?num=8700000'"; + const expectedCurlCode = "curl --url '$testEndpoint$envVarValue'"; await ApidashTestHelper.initialize( size: Size(kExpandedWindowWidth, kMinWindowSize.height)); @@ -33,60 +28,30 @@ void main() async { await Future.delayed(const Duration(seconds: 1)); /// Navigate to Environment Manager - await navigateByIcon(tester, Icons.laptop_windows_outlined); + await helper.navigateToEnvironmentManager(); await Future.delayed(const Duration(milliseconds: 500)); /// Create New Environment - final newEnvButton = - spot().spot().spotText(kLabelPlusNew); - newEnvButton.existsOnce(); - await act.tap(newEnvButton); - await tester.pumpAndSettle(); + await helper.envHelper.addNewEnvironment(); await Future.delayed(const Duration(milliseconds: 500)); - /// Open ItemCardMenu of the new environment - Finder envItems = find.byType(EnvironmentItem); - Finder newEnvItem = envItems.at(1); - expect(find.descendant(of: newEnvItem, matching: find.text(untitled)), - findsOneWidget); - Finder itemCardMenu = - find.descendant(of: newEnvItem, matching: find.byType(ItemCardMenu)); - await tester.tap(itemCardMenu); - await tester.pumpAndSettle(); - /// Rename the new environment - await tester.tap(find.text(ItemMenuOption.edit.label).last); - await tester.pump(); - await tester.enterText(newEnvItem, environmentName); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pump(); + await helper.envHelper.renameNewEnvironment(environmentName); await Future.delayed(const Duration(milliseconds: 500)); - /// Edit Environment Variables - final envCells = find.descendant( - of: find.byType(EditEnvironmentVariables), - matching: find.byType(CellField)); - await tester.enterText(envCells.at(0), envVarName); - await tester.enterText(envCells.at(1), envVarValue); + /// Add Environment Variables + await helper.envHelper.addEnvironmentVariables([(envVarName, envVarValue)]); await Future.delayed(const Duration(milliseconds: 500)); /// Navigate to Request Editor - await navigateByIcon(tester, Icons.auto_awesome_mosaic_outlined); + await helper.navigateToRequestEditor(); await Future.delayed(const Duration(milliseconds: 200)); /// Create a new request - await act.tap( - spot().spot().spotText(kLabelPlusNew)); - await tester.pumpAndSettle(); + await helper.reqHelper.addNewRequest(); /// Set active environment - await tester.tap(find.descendant( - of: find.byType(EnvironmentPopupMenu), - matching: find.byIcon(Icons.unfold_more))); - await tester.pumpAndSettle(); - - await tester.tap(find.text(environmentName).last); - await tester.pumpAndSettle(); + await helper.envHelper.setActiveEnvironment(environmentName); /// Check if environment suggestions are working await act.tap(spot().spot()); @@ -111,16 +76,12 @@ void main() async { expect(find.text(envVarValue), findsOneWidget); await Future.delayed(const Duration(milliseconds: 500)); + await helper.navigateToSettings(); + /// Change codegen language to curl - await navigateByIcon(tester, Icons.settings_outlined); - await tester.tap(find.descendant( - of: find.byType(CodegenPopupMenu), - matching: find.byIcon(Icons.unfold_more))); - await tester.pumpAndSettle(); + await helper.changeCodegenLanguage(CodegenLanguage.curl); - await tester.tap(find.text(CodegenLanguage.curl.label).last); - await tester.pumpAndSettle(); - await navigateByIcon(tester, Icons.auto_awesome_mosaic_outlined); + await helper.navigateToRequestEditor(); await Future.delayed(const Duration(milliseconds: 200)); /// Check variable substitution in request @@ -133,18 +94,15 @@ void main() async { findsOneWidget); /// Navigate to Environment Manager - await navigateByIcon(tester, Icons.laptop_windows_outlined); + await helper.navigateToEnvironmentManager(); await Future.delayed(const Duration(milliseconds: 200)); /// Delete the environment variable - final delButtons = find.descendant( - of: find.byType(EditEnvironmentVariables), - matching: find.byIcon(Icons.remove_circle)); - await tester.tap(delButtons.at(0)); + await helper.envHelper.deleteFirstEnvironmentVariable(); await Future.delayed(const Duration(milliseconds: 500)); /// Navigate back to Request Editor - await navigateByIcon(tester, Icons.auto_awesome_mosaic_outlined); + await helper.navigateToRequestEditor(); await Future.delayed(const Duration(milliseconds: 200)); /// Check if environment variable is now shown on hover diff --git a/integration_test/env_helper.dart b/integration_test/env_helper.dart new file mode 100644 index 00000000..a2acfb23 --- /dev/null +++ b/integration_test/env_helper.dart @@ -0,0 +1,71 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/material.dart'; +import 'package:spot/spot.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/screens/envvar/environments_pane.dart'; +import 'package:apidash/screens/envvar/editor_pane/variables_pane.dart'; + +class ApidashTestEnvHelper { + final WidgetTester tester; + + ApidashTestEnvHelper(this.tester); + + Future addNewEnvironment({bool isMobile = false}) async { + if (isMobile) { + kEnvScaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + } + final newEnvButton = + spot().spot().spotText(kLabelPlusNew); + newEnvButton.existsOnce(); + await act.tap(newEnvButton); + await tester.pumpAndSettle(); + } + + Future renameNewEnvironment(String newEnvName) async { + Finder envItems = find.byType(EnvironmentItem); + Finder newEnvItem = envItems.at(1); + expect(find.descendant(of: newEnvItem, matching: find.text("untitled")), + findsOneWidget); + Finder itemCardMenu = + find.descendant(of: newEnvItem, matching: find.byType(ItemCardMenu)); + await tester.tap(itemCardMenu); + await tester.pumpAndSettle(); + + await tester.tap(find.text(ItemMenuOption.edit.label).last); + await tester.pump(); + await tester.enterText(newEnvItem, newEnvName); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pump(); + } + + Future addEnvironmentVariables( + List<(String, String)> keyValuePairs) async { + final envCells = find.descendant( + of: find.byType(EditEnvironmentVariables), + matching: find.byType(CellField)); + for (var i = 0; i < keyValuePairs.length; i++) { + await tester.enterText(envCells.at(i), keyValuePairs[i].$1); + await tester.enterText(envCells.at(i + 1), keyValuePairs[i].$2); + } + } + + Future deleteFirstEnvironmentVariable() async { + final delButtons = find.descendant( + of: find.byType(EditEnvironmentVariables), + matching: find.byIcon(Icons.remove_circle)); + await tester.tap(delButtons.at(0)); + await tester.pump(); + } + + Future setActiveEnvironment(String envName) async { + await tester.tap(find.descendant( + of: find.byType(EnvironmentPopupMenu), + matching: find.byIcon(Icons.unfold_more))); + await tester.pumpAndSettle(); + + await tester.tap(find.text(envName).last); + await tester.pumpAndSettle(); + } +} diff --git a/integration_test/mobile/env_manager_test.dart b/integration_test/mobile/env_manager_test.dart index ea4632e4..491e4653 100644 --- a/integration_test/mobile/env_manager_test.dart +++ b/integration_test/mobile/env_manager_test.dart @@ -6,11 +6,8 @@ import 'package:spot/spot.dart'; import 'package:apidash/app.dart'; import 'package:apidash/consts.dart'; import 'package:apidash/widgets/widgets.dart'; -import 'package:apidash/screens/screens.dart'; import 'package:apidash/screens/common_widgets/env_trigger_options.dart'; -import 'package:apidash/screens/envvar/editor_pane/variables_pane.dart'; import 'package:apidash/screens/home_page/editor_pane/url_card.dart'; -import 'package:apidash/screens/envvar/environments_pane.dart'; import '../../test/extensions/widget_tester_extensions.dart'; import '../test_helper.dart'; @@ -18,11 +15,9 @@ void main() async { const environmentName = "test-env-name"; const envVarName = "test-env-var"; const envVarValue = "8700000"; - const testEndpoint = "api.apidash.dev/humanize/social?num="; + const testEndpoint = "https://api.apidash.dev/humanize/social?num="; const unknown = "unknown"; - const untitled = "untitled"; - const expectedCurlCode = - "curl --url 'https://api.apidash.dev/humanize/social?num=8700000'"; + const expectedCurlCode = "curl --url '$testEndpoint$envVarValue'"; await ApidashTestHelper.initialize( size: Size(kCompactWindowWidth, kMinWindowSize.height)); @@ -31,79 +26,37 @@ void main() async { await tester.pumpUntilFound(find.byType(DashApp)); await Future.delayed(const Duration(seconds: 1)); - kHomeScaffoldKey.currentState!.openDrawer(); - await tester.pumpAndSettle(); - /// Navigate to Environment Manager - await navigateByIcon(tester, Icons.laptop_windows_outlined); + await helper.navigateToEnvironmentManager(scaffoldKey: kHomeScaffoldKey); await Future.delayed(const Duration(milliseconds: 500)); - kEnvScaffoldKey.currentState!.openDrawer(); - await tester.pumpAndSettle(); - /// Create New Environment - final newEnvButton = - spot().spot().spotText(kLabelPlusNew); - newEnvButton.existsOnce(); - await act.tap(newEnvButton); - await tester.pumpAndSettle(); + await helper.envHelper.addNewEnvironment(isMobile: true); await Future.delayed(const Duration(milliseconds: 500)); - /// Open ItemCardMenu of the new environment - Finder envItems = find.byType(EnvironmentItem); - Finder newEnvItem = envItems.at(1); - expect(find.descendant(of: newEnvItem, matching: find.text(untitled)), - findsOneWidget); - Finder itemCardMenu = - find.descendant(of: newEnvItem, matching: find.byType(ItemCardMenu)); - await tester.tap(itemCardMenu); - await tester.pumpAndSettle(); - /// Rename the new environment - await tester.tap(find.text(ItemMenuOption.edit.label).last); - await tester.pump(); - await tester.enterText(newEnvItem, environmentName); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pump(); + await helper.envHelper.renameNewEnvironment(environmentName); await Future.delayed(const Duration(milliseconds: 500)); kEnvScaffoldKey.currentState!.closeDrawer(); await tester.pumpAndSettle(); - /// Edit Environment Variables - final envCells = find.descendant( - of: find.byType(EditEnvironmentVariables), - matching: find.byType(CellField)); - await tester.enterText(envCells.at(0), envVarName); - await tester.enterText(envCells.at(1), envVarValue); + /// Add Environment Variables + await helper.envHelper.addEnvironmentVariables([(envVarName, envVarValue)]); await Future.delayed(const Duration(milliseconds: 500)); - kEnvScaffoldKey.currentState!.openDrawer(); - await tester.pumpAndSettle(); - /// Navigate to Request Editor - await navigateByIcon(tester, Icons.auto_awesome_mosaic_outlined); + await helper.navigateToRequestEditor(scaffoldKey: kEnvScaffoldKey); await Future.delayed(const Duration(milliseconds: 200)); - kHomeScaffoldKey.currentState!.openDrawer(); - await tester.pumpAndSettle(); - /// Create a new request - await act.tap( - spot().spot().spotText(kLabelPlusNew)); - await tester.pumpAndSettle(); + await helper.reqHelper.addNewRequest(isMobile: true); kHomeScaffoldKey.currentState!.closeDrawer(); await tester.pumpAndSettle(); /// Set active environment - await tester.tap(find.descendant( - of: find.byType(EnvironmentPopupMenu), - matching: find.byIcon(Icons.unfold_more))); - await tester.pumpAndSettle(); - - await tester.tap(find.text(environmentName).last); - await tester.pumpAndSettle(); + await helper.envHelper.setActiveEnvironment(environmentName); /// Check if environment suggestions are working await act.tap(spot()); @@ -129,19 +82,12 @@ void main() async { await gesture.moveBy(const Offset(0, 100)); await Future.delayed(const Duration(milliseconds: 500)); - kHomeScaffoldKey.currentState!.openDrawer(); - await tester.pumpAndSettle(); + await helper.navigateToSettings(scaffoldKey: kHomeScaffoldKey); /// Change codegen language to curl - await navigateByIcon(tester, Icons.settings_outlined); - await tester.tap(find.descendant( - of: find.byType(CodegenPopupMenu), - matching: find.byIcon(Icons.unfold_more))); - await tester.pumpAndSettle(); + await helper.changeCodegenLanguage(CodegenLanguage.curl); - await tester.tap(find.text(CodegenLanguage.curl.label).last); - await tester.pumpAndSettle(); - await navigateByIcon(tester, Icons.auto_awesome_mosaic_outlined); + await helper.navigateToRequestEditor(); await Future.delayed(const Duration(milliseconds: 200)); /// Check variable substitution in request @@ -153,25 +99,16 @@ void main() async { matching: find.text(expectedCurlCode)), findsOneWidget); - kHomeScaffoldKey.currentState!.openDrawer(); - await tester.pumpAndSettle(); - /// Navigate to Environment Manager - await navigateByIcon(tester, Icons.laptop_windows_outlined); + await helper.navigateToEnvironmentManager(scaffoldKey: kHomeScaffoldKey); await Future.delayed(const Duration(milliseconds: 200)); /// Delete the environment variable - final delButtons = find.descendant( - of: find.byType(EditEnvironmentVariables), - matching: find.byIcon(Icons.remove_circle)); - await tester.tap(delButtons.at(0)); + await helper.envHelper.deleteFirstEnvironmentVariable(); await Future.delayed(const Duration(milliseconds: 500)); - kEnvScaffoldKey.currentState!.openDrawer(); - await tester.pumpAndSettle(); - /// Navigate back to Request Editor - await navigateByIcon(tester, Icons.auto_awesome_mosaic_outlined); + await helper.navigateToRequestEditor(scaffoldKey: kEnvScaffoldKey); await Future.delayed(const Duration(milliseconds: 200)); /// Check if environment variable is now shown on hover diff --git a/integration_test/req_helper.dart b/integration_test/req_helper.dart new file mode 100644 index 00000000..d32680ce --- /dev/null +++ b/integration_test/req_helper.dart @@ -0,0 +1,39 @@ +import 'package:apidash/consts.dart'; +import 'package:apidash/screens/home_page/collection_pane.dart'; +import 'package:apidash/widgets/menu_item_card.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; + +class ApidashTestRequestHelper { + final WidgetTester tester; + + ApidashTestRequestHelper(this.tester); + + Future addNewRequest({bool isMobile = false}) async { + if (isMobile) { + kHomeScaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + } + await act.tap( + spot().spot().spotText(kLabelPlusNew)); + await tester.pumpAndSettle(); + } + + Future renameNewRequest(String newReqName) async { + Finder reqItems = find.byType(RequestItem); + Finder newReqItem = reqItems.at(0); + expect(find.descendant(of: newReqItem, matching: find.text("untitled")), + findsOneWidget); + Finder itemCardMenu = + find.descendant(of: newReqItem, matching: find.byType(ItemCardMenu)); + await tester.tap(itemCardMenu); + await tester.pumpAndSettle(); + + await tester.tap(find.text(ItemMenuOption.edit.label).last); + await tester.pump(); + await tester.enterText(newReqItem, newReqName); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pump(); + } +} diff --git a/integration_test/test_helper.dart b/integration_test/test_helper.dart index f92a106c..24453d91 100644 --- a/integration_test/test_helper.dart +++ b/integration_test/test_helper.dart @@ -1,16 +1,34 @@ import 'package:apidash/app.dart'; -import 'package:flutter/widgets.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:integration_test/integration_test.dart'; import 'package:apidash/main.dart' as app; +import 'env_helper.dart'; +import 'req_helper.dart'; + class ApidashTestHelper { final WidgetTester tester; ApidashTestHelper(this.tester); + ApidashTestRequestHelper? _reqHelper; + ApidashTestEnvHelper? _envHelper; + + ApidashTestRequestHelper get reqHelper { + _reqHelper ??= ApidashTestRequestHelper(tester); + return _reqHelper!; + } + + ApidashTestEnvHelper get envHelper { + _envHelper ??= ApidashTestEnvHelper(tester); + return _envHelper!; + } + static Future initialize( {Size? size}) async { final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -30,6 +48,66 @@ class ApidashTestHelper { ), ); } + + Future navigateToRequestEditor( + {GlobalKey? scaffoldKey}) async { + if (scaffoldKey != null) { + scaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + } + await tester.tap(find.byIcon(Icons.auto_awesome_mosaic_outlined)); + await tester.pumpAndSettle(); + } + + Future navigateToEnvironmentManager( + {GlobalKey? scaffoldKey}) async { + if (scaffoldKey != null) { + scaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + } + await tester.tap(find.byIcon(Icons.laptop_windows_outlined)); + await tester.pumpAndSettle(); + } + + Future navigateToHistory( + {GlobalKey? scaffoldKey}) async { + if (scaffoldKey != null) { + scaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + } + await tester.tap(find.byIcon(Icons.history_outlined)); + await tester.pumpAndSettle(); + } + + Future navigateToSettings( + {GlobalKey? scaffoldKey}) async { + if (scaffoldKey != null) { + scaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + } + await tester.tap(find.byIcon(Icons.settings_outlined)); + await tester.pumpAndSettle(); + } + + Future changeURIScheme(String scheme) async { + await tester.tap(find.descendant( + of: find.byType(URIPopupMenu), + matching: find.byIcon(Icons.unfold_more))); + await tester.pumpAndSettle(); + + await tester.tap(find.text(scheme).last); + await tester.pumpAndSettle(); + } + + Future changeCodegenLanguage(CodegenLanguage language) async { + await tester.tap(find.descendant( + of: find.byType(CodegenPopupMenu), + matching: find.byIcon(Icons.unfold_more))); + await tester.pumpAndSettle(); + + await tester.tap(find.text(language.label).last); + await tester.pumpAndSettle(); + } } @isTest @@ -46,8 +124,3 @@ void apidashWidgetTest( semanticsEnabled: false, ); } - -Future navigateByIcon(WidgetTester tester, IconData icon) async { - await tester.tap(find.byIcon(icon)); - await tester.pumpAndSettle(); -} From 087c0b4ab131eb35f842a98dd3ac1e6786b4b407 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sun, 18 Aug 2024 21:34:11 +0530 Subject: [PATCH 40/71] fix: hover issue --- integration_test/desktop/env_manager_test.dart | 4 +--- integration_test/mobile/env_manager_test.dart | 6 +++--- integration_test/req_helper.dart | 6 ++++++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/integration_test/desktop/env_manager_test.dart b/integration_test/desktop/env_manager_test.dart index a3743501..b68d168c 100644 --- a/integration_test/desktop/env_manager_test.dart +++ b/integration_test/desktop/env_manager_test.dart @@ -7,7 +7,6 @@ import 'package:apidash/app.dart'; import 'package:apidash/consts.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/screens/common_widgets/env_trigger_options.dart'; -import 'package:apidash/screens/home_page/editor_pane/editor_request.dart'; import 'package:apidash/screens/home_page/editor_pane/url_card.dart'; import '../../test/extensions/widget_tester_extensions.dart'; import '../test_helper.dart'; @@ -54,8 +53,7 @@ void main() async { await helper.envHelper.setActiveEnvironment(environmentName); /// Check if environment suggestions are working - await act.tap(spot().spot()); - tester.testTextInput.enterText("$testEndpoint{{$envVarName"); + await helper.reqHelper.addRequestURL("$testEndpoint{{$envVarName"); await tester.pumpAndSettle( const Duration(milliseconds: 500)); // wait for suggestions await act.tap(spot() diff --git a/integration_test/mobile/env_manager_test.dart b/integration_test/mobile/env_manager_test.dart index 491e4653..80ea165b 100644 --- a/integration_test/mobile/env_manager_test.dart +++ b/integration_test/mobile/env_manager_test.dart @@ -15,7 +15,8 @@ void main() async { const environmentName = "test-env-name"; const envVarName = "test-env-var"; const envVarValue = "8700000"; - const testEndpoint = "https://api.apidash.dev/humanize/social?num="; + // TODO: Hover on variable doesn't work in test for long URLs + const testEndpoint = "https://api.apidash.dev?num="; const unknown = "unknown"; const expectedCurlCode = "curl --url '$testEndpoint$envVarValue'"; @@ -59,8 +60,7 @@ void main() async { await helper.envHelper.setActiveEnvironment(environmentName); /// Check if environment suggestions are working - await act.tap(spot()); - tester.testTextInput.enterText("$testEndpoint{{$envVarName"); + await helper.reqHelper.addRequestURL("$testEndpoint{{$envVarName"); await tester.pumpAndSettle( const Duration(milliseconds: 500)); // wait for suggestions await act.tap(spot() diff --git a/integration_test/req_helper.dart b/integration_test/req_helper.dart index d32680ce..c72f0af5 100644 --- a/integration_test/req_helper.dart +++ b/integration_test/req_helper.dart @@ -1,5 +1,6 @@ import 'package:apidash/consts.dart'; import 'package:apidash/screens/home_page/collection_pane.dart'; +import 'package:apidash/screens/home_page/editor_pane/url_card.dart'; import 'package:apidash/widgets/menu_item_card.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -36,4 +37,9 @@ class ApidashTestRequestHelper { await tester.testTextInput.receiveAction(TextInputAction.done); await tester.pump(); } + + Future addRequestURL(String url) async { + await act.tap(spot()); + tester.testTextInput.enterText(url); + } } From eb02c435e3b14b7ba50fd5ccf804bdeb798c49de Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Wed, 21 Aug 2024 03:13:16 +0530 Subject: [PATCH 41/71] test: his_request_test desktop --- .../desktop/his_request_test.dart | 63 ++++++++++++ integration_test/env_helper.dart | 9 +- integration_test/req_helper.dart | 95 ++++++++++++++++++- integration_test/test_helper.dart | 6 +- lib/consts.dart | 1 + .../envvar/editor_pane/variables_pane.dart | 2 +- 6 files changed, 165 insertions(+), 11 deletions(-) create mode 100644 integration_test/desktop/his_request_test.dart diff --git a/integration_test/desktop/his_request_test.dart b/integration_test/desktop/his_request_test.dart new file mode 100644 index 00000000..ef62eef3 --- /dev/null +++ b/integration_test/desktop/his_request_test.dart @@ -0,0 +1,63 @@ +import 'dart:ui'; + +import 'package:apidash/screens/history/history_pane.dart'; +import 'package:apidash/screens/history/history_requests.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; +import 'package:apidash/app.dart'; +import 'package:apidash/consts.dart'; +import '../../test/extensions/widget_tester_extensions.dart'; +import '../test_helper.dart'; + +void main() async { + await ApidashTestHelper.initialize( + size: Size(kExpandedWindowWidth, kMinWindowSize.height)); + apidashWidgetTest("Testing Environment Manager in desktop end-to-end", + (WidgetTester tester, helper) async { + await tester.pumpUntilFound(find.byType(DashApp)); + await Future.delayed(const Duration(seconds: 1)); + + /// Create New Request + await helper.reqHelper.addRequest( + "https://api.apidash.dev/humanize/social", + name: "Social", + params: [("num", "870000")], + ); + await Future.delayed(const Duration(milliseconds: 200)); + await helper.reqHelper.sendRequest(); + + /// Navigate to History + await helper.navigateToHistory(); + var sidebarCards = spot().spot().finder; + final initSidebarCardCount = + tester.widgetList(sidebarCards).length; + var historyCards = + spot().spot().finder; + final initHistoryCardCount = + tester.widgetList(historyCards).length; + + /// Send another request with same name + await helper.navigateToRequestEditor(); + await Future.delayed(const Duration(milliseconds: 200)); + await helper.reqHelper.addRequest( + "https://api.apidash.dev/convert/leet", + name: "Social", + params: [("text", "870000")], + ); + await Future.delayed(const Duration(milliseconds: 200)); + await helper.reqHelper.sendRequest(); + + /// Check history Card counts + await helper.navigateToHistory(); + sidebarCards = spot().spot().finder; + final newSidebarCardCount = + tester.widgetList(sidebarCards).length; + historyCards = spot().spot().finder; + final newHistoryCardCount = + tester.widgetList(historyCards).length; + expect(newSidebarCardCount, initSidebarCardCount); + expect(newHistoryCardCount, initHistoryCardCount + 1); + }); +} diff --git a/integration_test/env_helper.dart b/integration_test/env_helper.dart index a2acfb23..c26775a7 100644 --- a/integration_test/env_helper.dart +++ b/integration_test/env_helper.dart @@ -42,12 +42,15 @@ class ApidashTestEnvHelper { Future addEnvironmentVariables( List<(String, String)> keyValuePairs) async { - final envCells = find.descendant( + var envCells = find.descendant( of: find.byType(EditEnvironmentVariables), matching: find.byType(CellField)); for (var i = 0; i < keyValuePairs.length; i++) { - await tester.enterText(envCells.at(i), keyValuePairs[i].$1); - await tester.enterText(envCells.at(i + 1), keyValuePairs[i].$2); + await tester.enterText(envCells.at(i * 2), keyValuePairs[i].$1); + await tester.enterText(envCells.at(i * 2 + 1), keyValuePairs[i].$2); + envCells = find.descendant( + of: find.byType(EditEnvironmentVariables), + matching: find.byType(CellField)); } } diff --git a/integration_test/req_helper.dart b/integration_test/req_helper.dart index c72f0af5..af0b7f3e 100644 --- a/integration_test/req_helper.dart +++ b/integration_test/req_helper.dart @@ -1,10 +1,13 @@ -import 'package:apidash/consts.dart'; -import 'package:apidash/screens/home_page/collection_pane.dart'; -import 'package:apidash/screens/home_page/editor_pane/url_card.dart'; -import 'package:apidash/widgets/menu_item_card.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/screens/common_widgets/common_widgets.dart'; +import 'package:apidash/screens/home_page/collection_pane.dart'; +import 'package:apidash/screens/home_page/editor_pane/url_card.dart'; +import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/request_params.dart'; +import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/request_headers.dart'; class ApidashTestRequestHelper { final WidgetTester tester; @@ -42,4 +45,88 @@ class ApidashTestRequestHelper { await act.tap(spot()); tester.testTextInput.enterText(url); } + + Future setRequestMethod(HTTPVerb method) async { + final methodDropdown = spot(); + await act.tap(methodDropdown); + await tester.pumpAndSettle(); + await tester.tap(find.text(method.name.toUpperCase()).last); + await tester.pumpAndSettle(); + } + + Future addRequestParams(List<(String, String)> keyValuePairs) async { + final paramTabButton = + spot().spot().spotText(kLabelURLParams); + await act.tap(paramTabButton); + await tester.pumpAndSettle(); + + var paramCells = find.descendant( + of: find.byType(EditRequestURLParams), + matching: find.byType(EnvCellField)); + for (var i = 0; i < keyValuePairs.length; i++) { + await tester.tap(paramCells.at(i)); + tester.testTextInput.enterText(keyValuePairs[i].$1); + await tester.tap(paramCells.at(i + 1)); + tester.testTextInput.enterText(keyValuePairs[i].$2); + paramCells = find.descendant( + of: find.byType(EditRequestURLParams), + matching: find.byType(EnvCellField)); + } + } + + Future addRequestHeaders(List<(String, String)> keyValuePairs) async { + final headerTabButton = + spot().spot().spotText(kLabelHeaders); + await act.tap(headerTabButton); + await tester.pumpAndSettle(); + + var headerCells = find.descendant( + of: find.byType(EditRequestHeaders), + matching: find.byType(HeaderField)); + var valueCells = find.descendant( + of: find.byType(EditRequestHeaders), + matching: find.byType(EnvCellField)); + for (var i = 0; i < keyValuePairs.length; i++) { + await tester.tap(headerCells.at(i)); + tester.testTextInput.enterText(keyValuePairs[i].$1); + await tester.tap(valueCells.at(i)); + tester.testTextInput.enterText(keyValuePairs[i].$2); + headerCells = find.descendant( + of: find.byType(EditRequestHeaders), + matching: find.byType(HeaderField)); + valueCells = find.descendant( + of: find.byType(EditRequestHeaders), + matching: find.byType(EnvCellField)); + } + } + + Future addRequest( + String url, { + String? name, + HTTPVerb method = HTTPVerb.get, + List<(String, String)> params = const [], + List<(String, String)> headers = const [], + bool isMobile = false, + }) async { + await addNewRequest(isMobile: isMobile); + if (name != null) { + await renameNewRequest(name); + } + if (isMobile) { + kHomeScaffoldKey.currentState!.closeDrawer(); + await tester.pumpAndSettle(); + } + if (method != HTTPVerb.get) { + await setRequestMethod(method); + } + await addRequestURL(url); + await addRequestParams(params); + await addRequestHeaders(headers); + } + + Future sendRequest( + {Duration stallTime = const Duration(seconds: 3)}) async { + await act.tap(spot()); + await tester.pumpAndSettle(stallTime); + } } diff --git a/integration_test/test_helper.dart b/integration_test/test_helper.dart index 24453d91..a1ce860d 100644 --- a/integration_test/test_helper.dart +++ b/integration_test/test_helper.dart @@ -1,12 +1,12 @@ -import 'package:apidash/app.dart'; -import 'package:apidash/consts.dart'; -import 'package:apidash/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:integration_test/integration_test.dart'; import 'package:apidash/main.dart' as app; +import 'package:apidash/app.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/widgets/widgets.dart'; import 'env_helper.dart'; import 'req_helper.dart'; diff --git a/lib/consts.dart b/lib/consts.dart index 7be964e9..9d72e0e3 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -761,6 +761,7 @@ const kHintAddName = "Add Name"; const kHintAddFieldName = "Add Field Name"; const kLabelAddParam = "Add Param"; const kLabelAddHeader = "Add Header"; +const kLabelAddVariable = "Add Variable"; const kLabelSelectFile = "Select File"; const kLabelAddFormField = "Add Form Field"; // Response Pane diff --git a/lib/screens/envvar/editor_pane/variables_pane.dart b/lib/screens/envvar/editor_pane/variables_pane.dart index 1d6eedb7..8af4ba6e 100644 --- a/lib/screens/envvar/editor_pane/variables_pane.dart +++ b/lib/screens/envvar/editor_pane/variables_pane.dart @@ -217,7 +217,7 @@ class EditEnvironmentVariablesState }, icon: const Icon(Icons.add), label: const Text( - "Add Variable", + kLabelAddVariable, style: kTextStyleButton, ), ), From 6e82d2b5701b149b4dca817ea7d7d473f69f72ac Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Wed, 21 Aug 2024 16:57:01 +0530 Subject: [PATCH 42/71] fix: selected icon test --- test/providers/ui_providers_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/providers/ui_providers_test.dart b/test/providers/ui_providers_test.dart index 82116586..db2f21d1 100644 --- a/test/providers/ui_providers_test.dart +++ b/test/providers/ui_providers_test.dart @@ -217,7 +217,7 @@ void main() { // Verify that the EnvironmentPage is displayed expect(find.byType(EnvironmentPage), findsOneWidget); // Verify that the selected icon is the filled version (selectedIcon) - expect(find.byIcon(Icons.laptop_windows_outlined), findsOneWidget); + expect(find.byIcon(Icons.laptop_windows), findsOneWidget); // Go to HistoryPage container.read(navRailIndexStateProvider.notifier).state = 2; From 063b9258c2b95aa60487c8dc4b13d023c7b8caef Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Wed, 21 Aug 2024 22:09:30 +0530 Subject: [PATCH 43/71] test: his_request_test mobile --- .../desktop/his_request_test.dart | 18 ++--- integration_test/mobile/env_manager_test.dart | 2 +- integration_test/mobile/his_request_test.dart | 78 +++++++++++++++++++ integration_test/req_helper.dart | 4 +- lib/providers/history_providers.dart | 1 + lib/screens/history/history_details.dart | 1 + 6 files changed, 91 insertions(+), 13 deletions(-) create mode 100644 integration_test/mobile/his_request_test.dart diff --git a/integration_test/desktop/his_request_test.dart b/integration_test/desktop/his_request_test.dart index ef62eef3..c371732c 100644 --- a/integration_test/desktop/his_request_test.dart +++ b/integration_test/desktop/his_request_test.dart @@ -1,20 +1,19 @@ import 'dart:ui'; -import 'package:apidash/screens/history/history_pane.dart'; -import 'package:apidash/screens/history/history_requests.dart'; -import 'package:apidash/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:apidash/app.dart'; import 'package:apidash/consts.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/screens/history/history_pane.dart'; import '../../test/extensions/widget_tester_extensions.dart'; import '../test_helper.dart'; void main() async { await ApidashTestHelper.initialize( size: Size(kExpandedWindowWidth, kMinWindowSize.height)); - apidashWidgetTest("Testing Environment Manager in desktop end-to-end", + apidashWidgetTest("Testing History of Requests in desktop end-to-end", (WidgetTester tester, helper) async { await tester.pumpUntilFound(find.byType(DashApp)); await Future.delayed(const Duration(seconds: 1)); @@ -22,7 +21,7 @@ void main() async { /// Create New Request await helper.reqHelper.addRequest( "https://api.apidash.dev/humanize/social", - name: "Social", + name: "test-his-name", params: [("num", "870000")], ); await Future.delayed(const Duration(milliseconds: 200)); @@ -33,8 +32,7 @@ void main() async { var sidebarCards = spot().spot().finder; final initSidebarCardCount = tester.widgetList(sidebarCards).length; - var historyCards = - spot().spot().finder; + var historyCards = find.byType(HistoryRequestCard, skipOffstage: false); final initHistoryCardCount = tester.widgetList(historyCards).length; @@ -43,8 +41,8 @@ void main() async { await Future.delayed(const Duration(milliseconds: 200)); await helper.reqHelper.addRequest( "https://api.apidash.dev/convert/leet", - name: "Social", - params: [("text", "870000")], + name: "test-his-name", + params: [("text", "apidash")], ); await Future.delayed(const Duration(milliseconds: 200)); await helper.reqHelper.sendRequest(); @@ -54,7 +52,7 @@ void main() async { sidebarCards = spot().spot().finder; final newSidebarCardCount = tester.widgetList(sidebarCards).length; - historyCards = spot().spot().finder; + historyCards = find.byType(HistoryRequestCard, skipOffstage: false); final newHistoryCardCount = tester.widgetList(historyCards).length; expect(newSidebarCardCount, initSidebarCardCount); diff --git a/integration_test/mobile/env_manager_test.dart b/integration_test/mobile/env_manager_test.dart index 80ea165b..12d99d4b 100644 --- a/integration_test/mobile/env_manager_test.dart +++ b/integration_test/mobile/env_manager_test.dart @@ -22,7 +22,7 @@ void main() async { await ApidashTestHelper.initialize( size: Size(kCompactWindowWidth, kMinWindowSize.height)); - apidashWidgetTest("Testing Environment Manager in desktop end-to-end", + apidashWidgetTest("Testing Environment Manager in mobile end-to-end", (WidgetTester tester, helper) async { await tester.pumpUntilFound(find.byType(DashApp)); await Future.delayed(const Duration(seconds: 1)); diff --git a/integration_test/mobile/his_request_test.dart b/integration_test/mobile/his_request_test.dart new file mode 100644 index 00000000..9716f62f --- /dev/null +++ b/integration_test/mobile/his_request_test.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; +import 'package:apidash/app.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/screens/history/history_pane.dart'; +import 'package:apidash/screens/history/history_widgets/history_widgets.dart'; +import '../../test/extensions/widget_tester_extensions.dart'; +import '../test_helper.dart'; + +void main() async { + await ApidashTestHelper.initialize( + size: Size(kCompactWindowWidth, kMinWindowSize.height)); + apidashWidgetTest("Testing History of Requests in mobile end-to-end", + (WidgetTester tester, helper) async { + await tester.pumpUntilFound(find.byType(DashApp)); + await Future.delayed(const Duration(seconds: 1)); + + /// Create New Request + await helper.reqHelper.addRequest( + "https://api.apidash.dev/humanize/social", + name: "test-his-name", + params: [("num", "870000")], + isMobile: true, + ); + await Future.delayed(const Duration(milliseconds: 200)); + await helper.reqHelper.sendRequest(); + await helper.reqHelper.sendRequest(); + + /// Navigate to History + await helper.navigateToHistory(scaffoldKey: kHomeScaffoldKey); + kHisScaffoldKey.currentState!.openDrawer(); + var sidebarCards = spot().spot().finder; + final initSidebarCardCount = + tester.widgetList(sidebarCards).length; + kHisScaffoldKey.currentState!.closeDrawer(); + + await act.tap( + spot().spotIcon(Icons.keyboard_arrow_up_rounded)); + await tester.pumpAndSettle(); + var historyCards = find.byType(HistoryRequestCard, skipOffstage: false); + final initHistoryCardCount = + tester.widgetList(historyCards).length; + await tester.tapAt(const Offset(100, 100)); + await tester.pumpAndSettle(); + + /// Send another request with same name + await helper.navigateToRequestEditor(scaffoldKey: kHisScaffoldKey); + await Future.delayed(const Duration(milliseconds: 200)); + await helper.reqHelper.addRequest( + "https://api.apidash.dev/convert/leet", + name: "test-his-name", + params: [("text", "apidash")], + isMobile: true, + ); + await Future.delayed(const Duration(milliseconds: 200)); + await helper.reqHelper.sendRequest(); + + /// Check history Card counts + await helper.navigateToHistory(scaffoldKey: kHomeScaffoldKey); + sidebarCards = spot().spot().finder; + final newSidebarCardCount = + tester.widgetList(sidebarCards).length; + kHisScaffoldKey.currentState!.closeDrawer(); + + await act.tap( + spot().spotIcon(Icons.keyboard_arrow_up_rounded)); + await tester.pumpAndSettle(); + historyCards = find.byType(HistoryRequestCard, skipOffstage: false); + final newHistoryCardCount = + tester.widgetList(historyCards).length; + await tester.tapAt(const Offset(100, 100)); + await tester.pumpAndSettle(); + expect(newSidebarCardCount, initSidebarCardCount); + expect(newHistoryCardCount, initHistoryCardCount + 1); + }); +} diff --git a/integration_test/req_helper.dart b/integration_test/req_helper.dart index af0b7f3e..ecc9c403 100644 --- a/integration_test/req_helper.dart +++ b/integration_test/req_helper.dart @@ -120,8 +120,8 @@ class ApidashTestRequestHelper { await setRequestMethod(method); } await addRequestURL(url); - await addRequestParams(params); - await addRequestHeaders(headers); + if (params.isNotEmpty) await addRequestParams(params); + if (headers.isNotEmpty) await addRequestHeaders(headers); } Future sendRequest( diff --git a/lib/providers/history_providers.dart b/lib/providers/history_providers.dart index 6f5d23df..06d2ab13 100644 --- a/lib/providers/history_providers.dart +++ b/lib/providers/history_providers.dart @@ -87,5 +87,6 @@ class HistoryMetaStateNotifier hiveHandler.setHistoryIds(updatedHistoryKeys); hiveHandler.setHistoryMeta(id, model.metaData.toJson()); await hiveHandler.setHistoryRequest(id, model.toJson()); + await loadHistoryRequest(id); } } diff --git a/lib/screens/history/history_details.dart b/lib/screens/history/history_details.dart index 6e12b422..35d00819 100644 --- a/lib/screens/history/history_details.dart +++ b/lib/screens/history/history_details.dart @@ -18,6 +18,7 @@ class _HistoryDetailsState extends ConsumerState with TickerProviderStateMixin { @override Widget build(BuildContext context) { + ref.watch(historySequenceProvider); final selectedHistoryRequest = ref.watch(selectedHistoryRequestModelProvider); final codePaneVisible = ref.watch(historyCodePaneVisibleStateProvider); From 9b644e0049def6e94572ac7d2401e23bbaabccaf Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Thu, 22 Aug 2024 19:15:36 +0530 Subject: [PATCH 44/71] fix: add note --- integration_test/desktop/his_request_test.dart | 7 +++---- integration_test/mobile/his_request_test.dart | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/integration_test/desktop/his_request_test.dart b/integration_test/desktop/his_request_test.dart index c371732c..41e739ea 100644 --- a/integration_test/desktop/his_request_test.dart +++ b/integration_test/desktop/his_request_test.dart @@ -2,11 +2,9 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:spot/spot.dart'; import 'package:apidash/app.dart'; import 'package:apidash/consts.dart'; import 'package:apidash/widgets/widgets.dart'; -import 'package:apidash/screens/history/history_pane.dart'; import '../../test/extensions/widget_tester_extensions.dart'; import '../test_helper.dart'; @@ -29,7 +27,7 @@ void main() async { /// Navigate to History await helper.navigateToHistory(); - var sidebarCards = spot().spot().finder; + var sidebarCards = find.byType(SidebarHistoryCard, skipOffstage: false); final initSidebarCardCount = tester.widgetList(sidebarCards).length; var historyCards = find.byType(HistoryRequestCard, skipOffstage: false); @@ -48,8 +46,9 @@ void main() async { await helper.reqHelper.sendRequest(); /// Check history Card counts + /// TODO: Having overflowing number of cards causes the test to fail await helper.navigateToHistory(); - sidebarCards = spot().spot().finder; + sidebarCards = find.byType(SidebarHistoryCard, skipOffstage: false); final newSidebarCardCount = tester.widgetList(sidebarCards).length; historyCards = find.byType(HistoryRequestCard, skipOffstage: false); diff --git a/integration_test/mobile/his_request_test.dart b/integration_test/mobile/his_request_test.dart index 9716f62f..7de01ec9 100644 --- a/integration_test/mobile/his_request_test.dart +++ b/integration_test/mobile/his_request_test.dart @@ -4,7 +4,6 @@ import 'package:spot/spot.dart'; import 'package:apidash/app.dart'; import 'package:apidash/consts.dart'; import 'package:apidash/widgets/widgets.dart'; -import 'package:apidash/screens/history/history_pane.dart'; import 'package:apidash/screens/history/history_widgets/history_widgets.dart'; import '../../test/extensions/widget_tester_extensions.dart'; import '../test_helper.dart'; @@ -31,7 +30,7 @@ void main() async { /// Navigate to History await helper.navigateToHistory(scaffoldKey: kHomeScaffoldKey); kHisScaffoldKey.currentState!.openDrawer(); - var sidebarCards = spot().spot().finder; + var sidebarCards = find.byType(SidebarHistoryCard, skipOffstage: false); final initSidebarCardCount = tester.widgetList(sidebarCards).length; kHisScaffoldKey.currentState!.closeDrawer(); @@ -58,8 +57,9 @@ void main() async { await helper.reqHelper.sendRequest(); /// Check history Card counts + /// TODO: Having overflowing number of cards causes the test to fail await helper.navigateToHistory(scaffoldKey: kHomeScaffoldKey); - sidebarCards = spot().spot().finder; + sidebarCards = find.byType(SidebarHistoryCard, skipOffstage: false); final newSidebarCardCount = tester.widgetList(sidebarCards).length; kHisScaffoldKey.currentState!.closeDrawer(); From e7620118f7fad53f4e52e34e0d417ae3f35b855b Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Wed, 21 Aug 2024 16:57:01 +0530 Subject: [PATCH 45/71] fix: selected icon test --- test/providers/ui_providers_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/providers/ui_providers_test.dart b/test/providers/ui_providers_test.dart index 82116586..db2f21d1 100644 --- a/test/providers/ui_providers_test.dart +++ b/test/providers/ui_providers_test.dart @@ -217,7 +217,7 @@ void main() { // Verify that the EnvironmentPage is displayed expect(find.byType(EnvironmentPage), findsOneWidget); // Verify that the selected icon is the filled version (selectedIcon) - expect(find.byIcon(Icons.laptop_windows_outlined), findsOneWidget); + expect(find.byIcon(Icons.laptop_windows), findsOneWidget); // Go to HistoryPage container.read(navRailIndexStateProvider.notifier).state = 2; From 5a83a8673bd34e27e6a1c21ddbb34cc19ff69ed8 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Wed, 21 Aug 2024 16:57:01 +0530 Subject: [PATCH 46/71] fix: selected icon test From d9bf8e6f98a6e8887eeba14a0f4515de5a82adc4 Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Sat, 24 Aug 2024 23:19:56 +0530 Subject: [PATCH 47/71] cleanup --- integration_test/test_helper.dart | 3 --- test/providers/ui_providers_test.dart | 5 ++--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/integration_test/test_helper.dart b/integration_test/test_helper.dart index d8307084..a1ce860d 100644 --- a/integration_test/test_helper.dart +++ b/integration_test/test_helper.dart @@ -11,9 +11,6 @@ import 'package:apidash/widgets/widgets.dart'; import 'env_helper.dart'; import 'req_helper.dart'; -import 'env_helper.dart'; -import 'req_helper.dart'; - class ApidashTestHelper { final WidgetTester tester; diff --git a/test/providers/ui_providers_test.dart b/test/providers/ui_providers_test.dart index db2f21d1..39ef1ff6 100644 --- a/test/providers/ui_providers_test.dart +++ b/test/providers/ui_providers_test.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:spot/spot.dart'; -import 'package:apidash/consts.dart'; +//import 'package:spot/spot.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/screens/common_widgets/common_widgets.dart'; import 'package:apidash/screens/dashboard.dart'; @@ -327,7 +326,7 @@ void main() { await tester.tap(byType); await tester.pumpAndSettle(); - // Screenshot + // Screenshot using spot // await takeScreenshot(); var dupId = container.read(selectedIdStateProvider); From 0db18a1ca62105a8725c89862f4f5b5557410da6 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sun, 25 Aug 2024 05:03:42 +0530 Subject: [PATCH 48/71] test: req_editor_test desktop --- integration_test/desktop/req_editor_test.dart | 60 +++++++++++++++++++ integration_test/req_helper.dart | 7 +++ 2 files changed, 67 insertions(+) create mode 100644 integration_test/desktop/req_editor_test.dart diff --git a/integration_test/desktop/req_editor_test.dart b/integration_test/desktop/req_editor_test.dart new file mode 100644 index 00000000..3c37824a --- /dev/null +++ b/integration_test/desktop/req_editor_test.dart @@ -0,0 +1,60 @@ +import 'dart:ui'; + +import 'package:apidash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; +import 'package:apidash/app.dart'; +import 'package:apidash/consts.dart'; +import '../../test/extensions/widget_tester_extensions.dart'; +import '../test_helper.dart'; + +void main() async { + const reqName = "test-req-name"; + const testEndpoint = "https://api.apidash.dev/humanize/social"; + const paramKey = "num"; + const paramValue = "870000"; + const headerKey = "Content-Type"; + const headerValue = "application/json"; + const expectedCurlCode = "curl --url '$testEndpoint?$paramKey=$paramValue'"; + const expectedRawCode = '''{ + "data": "870K" +}'''; + + await ApidashTestHelper.initialize( + size: Size(kExpandedWindowWidth, kMinWindowSize.height)); + apidashWidgetTest("Testing Request Editor in desktop end-to-end", + (WidgetTester tester, helper) async { + await tester.pumpUntilFound(find.byType(DashApp)); + await Future.delayed(const Duration(seconds: 1)); + + /// Create New Request + await helper.reqHelper.addRequest(testEndpoint, + name: reqName, + params: [(paramKey, paramValue)], + headers: [(headerKey, headerValue)]); + await Future.delayed(const Duration(milliseconds: 200)); + await helper.reqHelper.sendRequest(); + + /// Check if response is received + await act.tap(spotText(ResponseBodyView.raw.label)); + await tester.pumpAndSettle(); + spotText(expectedRawCode).existsOnce(); + + /// Check Response Headers + await act.tap(spot().spotText(kLabelHeaders)); + await tester.pumpAndSettle(); + spotText("$kLabelRequestHeaders (1 $kLabelItems)").existsOnce(); + + /// Change codegen language to curl + await helper.navigateToSettings(); + await helper.changeCodegenLanguage(CodegenLanguage.curl); + await helper.navigateToRequestEditor(); + + /// Check generated code + await helper.reqHelper.unCheckFirstHeader(); + await act.tap(spot().spotText(kLabelViewCode)); + await tester.pumpAndSettle(); + spot().spotText(expectedCurlCode).existsOnce(); + }); +} diff --git a/integration_test/req_helper.dart b/integration_test/req_helper.dart index ecc9c403..4d2fd68c 100644 --- a/integration_test/req_helper.dart +++ b/integration_test/req_helper.dart @@ -100,6 +100,13 @@ class ApidashTestRequestHelper { } } + Future unCheckFirstHeader() async { + final headerCells = find.descendant( + of: find.byType(EditRequestHeaders), matching: find.byType(CheckBox)); + await tester.tap(headerCells.at(0)); + await tester.pumpAndSettle(); + } + Future addRequest( String url, { String? name, From feef7d2b75383cada73d3940245a281f3a442532 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sun, 25 Aug 2024 05:12:53 +0530 Subject: [PATCH 49/71] test: req_editor_test mobile --- integration_test/desktop/req_editor_test.dart | 4 +- integration_test/mobile/req_editor_test.dart | 67 +++++++++++++++++++ 2 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 integration_test/mobile/req_editor_test.dart diff --git a/integration_test/desktop/req_editor_test.dart b/integration_test/desktop/req_editor_test.dart index 3c37824a..146732da 100644 --- a/integration_test/desktop/req_editor_test.dart +++ b/integration_test/desktop/req_editor_test.dart @@ -1,11 +1,9 @@ -import 'dart:ui'; - -import 'package:apidash/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:apidash/app.dart'; import 'package:apidash/consts.dart'; +import 'package:apidash/widgets/widgets.dart'; import '../../test/extensions/widget_tester_extensions.dart'; import '../test_helper.dart'; diff --git a/integration_test/mobile/req_editor_test.dart b/integration_test/mobile/req_editor_test.dart new file mode 100644 index 00000000..032c1818 --- /dev/null +++ b/integration_test/mobile/req_editor_test.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; +import 'package:apidash/app.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/widgets/widgets.dart'; +import '../../test/extensions/widget_tester_extensions.dart'; +import '../test_helper.dart'; + +void main() async { + const reqName = "test-req-name"; + const testEndpoint = "https://api.apidash.dev/humanize/social"; + const paramKey = "num"; + const paramValue = "870000"; + const headerKey = "Content-Type"; + const headerValue = "application/json"; + const expectedCurlCode = "curl --url '$testEndpoint?$paramKey=$paramValue'"; + const expectedRawCode = '''{ + "data": "870K" +}'''; + + await ApidashTestHelper.initialize( + size: Size(kCompactWindowWidth, kMinWindowSize.height)); + apidashWidgetTest("Testing Request Editor in mobile end-to-end", + (WidgetTester tester, helper) async { + await tester.pumpUntilFound(find.byType(DashApp)); + await Future.delayed(const Duration(seconds: 1)); + + /// Create New Request + await helper.reqHelper.addRequest(testEndpoint, + name: reqName, + params: [(paramKey, paramValue)], + headers: [(headerKey, headerValue)], + isMobile: true); + await Future.delayed(const Duration(milliseconds: 200)); + await helper.reqHelper.sendRequest(); + + /// Check if response is received + await act.tap(spot().spotText(kLabelResponse)); + await tester.pumpAndSettle(); + await act.tap(spotText(ResponseBodyView.raw.label)); + await tester.pumpAndSettle(); + spotText(expectedRawCode).existsOnce(); + + /// Check Response Headers + await act.tap(spot().spotText(kLabelHeaders)); + await tester.pumpAndSettle(); + spotText("$kLabelRequestHeaders (1 $kLabelItems)").existsOnce(); + + /// Change codegen language to curl + await helper.navigateToSettings(scaffoldKey: kHomeScaffoldKey); + await helper.changeCodegenLanguage(CodegenLanguage.curl); + await helper.navigateToRequestEditor(); + + /// Uncheck first header + await act.tap(spot().spotText(kLabelRequest)); + await tester.pumpAndSettle(); + await act.tap(spot().spot().spotText(kLabelHeaders)); + await tester.pumpAndSettle(); + await helper.reqHelper.unCheckFirstHeader(); + + /// Check generated code + await act.tap(spot().spotText(kLabelCode)); + await tester.pumpAndSettle(); + spot().spotText(expectedCurlCode).existsOnce(); + }); +} From ea43a6f8efec5728780afdadfdfc33dd6b4207ba Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sun, 25 Aug 2024 05:39:51 +0530 Subject: [PATCH 50/71] wip: env doc user guide --- CONTRIBUTING.md | 32 +++- README.md | 168 +++++++++--------- doc/user_guide/env_user_guide.md | 29 +++ .../images/env/env-variable-scope.png | Bin 0 -> 21331 bytes 4 files changed, 137 insertions(+), 92 deletions(-) create mode 100644 doc/user_guide/env_user_guide.md create mode 100644 doc/user_guide/images/env/env-variable-scope.png diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 32f79e5c..7b05a160 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ We value your participation in this open source project. This page will give you a quick overview of how to get involved. -You can contribute to the project in any or all of the following ways: +You can contribute to the project in any or all of the following ways: - [Ask a question](https://github.com/foss42/apidash/discussions) - [Submit a bug report](https://github.com/foss42/apidash/issues/new/choose) @@ -22,12 +22,13 @@ In case you are new to the open source ecosystem, we would be more than happy to > PRs with precise changes (like adding a new test, resolving a bug/issue, adding a new feature) are always preferred over a single PR with a ton of file changes as they are easier to review and merge. We currently do not accept PRs that involve: + - Code refactoring without any new feature addition/existing issue resolution. - Bumping of dependency versions (SDKs, Packages). ### Resolving an existing issue / Adding a requested feature -You can find all existing issues [here](https://github.com/foss42/apidash/issues). A good place to start is to take a look at ["good first issues"](https://github.com/foss42/apidash/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). +You can find all existing issues [here](https://github.com/foss42/apidash/issues). A good place to start is to take a look at ["good first issues"](https://github.com/foss42/apidash/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). **Step 1** - Identify the issue you want to work on. **Step 2** - Comment on the issue so that we can discuss how to approach and solve the problem. @@ -43,7 +44,7 @@ You can find all existing issues [here](https://github.com/foss42/apidash/issues **Step 1** - Open an [issue](https://github.com/foss42/apidash/issues/new/choose) so that we can discuss on the new feature. **Step 2** - Fork the [`foss42/apidash`](https://github.com/foss42/apidash) repo to your account. -**Step 3** - Create a new branch in your fork and name it `add-feature-xyz`. +**Step 3** - Create a new branch in your fork and name it `add-feature-xyz`. **Step 4** - Run API Dash locally (More details [here](#how-to-run-api-dash-locally)). **Step 5** - Make the necessary code changes required to implement the feature in the branch. **Step 6** - Once the feature is implemented. Make sure you add the relevant tests in the `test` folder and run tests (More details [here](#how-to-run-tests)). @@ -53,11 +54,12 @@ You can find all existing issues [here](https://github.com/foss42/apidash/issues ### Adding a new test You can contribute by adding missing/new tests for: + - Widgets (`lib/widgets/`) - Models (`lib/models/`) - Utilities (`lib/utils/`) - Riverpod providers (`lib/providers/`) -- Code generation (`lib/codegen/`) +- Code generation (`lib/codegen/`) - Services (`lib/services/`). **Step 1** - Identify the test you want to add or improve. @@ -68,11 +70,11 @@ You can contribute by adding missing/new tests for: **Step 6** - [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) with a clear title and description of the tests you are adding. **Step 7** - Wait for feedback and review. We will closely work with you on the Pull Request. -## General Instructions +## General Instructions ### What is the supported Flutter/Dart version? -This project supports the latest Dart 3 & Flutter version. If you are using older Flutter version that does not support Dart 3, you might get errors. +This project supports the latest Dart 3 & Flutter version. If you are using older Flutter version that does not support Dart 3, you might get errors. In case you are setting up Flutter for the first time, just go ahead and download the latest (Stable) SDK from the [Flutter SDK Archive](https://docs.flutter.dev/release/archive). Then proceed with the Flutter installation. @@ -100,12 +102,12 @@ flutter test --coverage To generate coverage report as html execute: ``` -genhtml coverage/lcov.info -o coverage/html +genhtml coverage/lcov.info -o coverage/html ``` **Note**: On macOS you need to have `lcov` installed on your system (`brew install lcov`) to run the above command. -To view the coverage report in the browser for further analysis, execute: +To view the coverage report in the browser for further analysis, execute: ``` open coverage/html/index.html @@ -125,6 +127,20 @@ Example: flutter test test/widgets/codegen_previewer_test.dart ``` +#### Running an Integration test + +To run an integration test, execute the following command: + +``` +flutter test integration_test/.dart +``` + +Example: + +``` +flutter test integration_test/desktop/env_manager_test.dart +``` + ### How to add a new package to pubspec.yaml? Instead of copy pasting from pub.dev, it is recommended that you use `flutter pub add package_name` to add a new package to `pubspec.yaml`. You can read more [here](https://docs.flutter.dev/packages-and-plugins/using-packages#adding-a-package-dependency-to-an-app-using-flutter-pub-add). diff --git a/README.md b/README.md index 2da72ff9..431578f8 100644 --- a/README.md +++ b/README.md @@ -111,36 +111,36 @@ API Dash can be downloaded from the links below: API Dash currently supports API integration code generation for the following languages/libraries. -| Language | Library | Comment/Issues | -| ---------------------- | ------------- | ------- | -| cURL | | | -| HAR | | | -| C | `libcurl` | | -| C# | `HttpClient` | | -| C# | `RestSharp` | | -| Dart | `http` | | -| Dart | `dio` | | -| Go | `net/http` | | -| JavaScript | `axios` | | -| JavaScript | `fetch` | | -| JavaScript (`node.js`) | `axios` | | -| JavaScript (`node.js`) | `fetch` | | -| Python | `requests` | | -| Python | `http.client` | | -| Kotlin | `okhttp3` | | -| Ruby | `faraday` | | -| Ruby | `net/http` | | -| Rust | `reqwest` | | -| Rust | `ureq` | | -| Rust | `Actix Client` | | -| Java | `asynchttpclient` | | -| Java | `HttpClient` | | -| Java | `okhttp3` | | -| Java | `Unirest` | | -| Julia | `HTTP` | | -| PHP | `curl` | | -| PHP | `guzzle` | | -| PHP | `HTTPlug` | | +| Language | Library | Comment/Issues | +| ---------------------- | ----------------- | -------------- | +| cURL | | | +| HAR | | | +| C | `libcurl` | | +| C# | `HttpClient` | | +| C# | `RestSharp` | | +| Dart | `http` | | +| Dart | `dio` | | +| Go | `net/http` | | +| JavaScript | `axios` | | +| JavaScript | `fetch` | | +| JavaScript (`node.js`) | `axios` | | +| JavaScript (`node.js`) | `fetch` | | +| Python | `requests` | | +| Python | `http.client` | | +| Kotlin | `okhttp3` | | +| Ruby | `faraday` | | +| Ruby | `net/http` | | +| Rust | `reqwest` | | +| Rust | `ureq` | | +| Rust | `Actix Client` | | +| Java | `asynchttpclient` | | +| Java | `HttpClient` | | +| Java | `okhttp3` | | +| Java | `Unirest` | | +| Julia | `HTTP` | | +| PHP | `curl` | | +| PHP | `guzzle` | | +| PHP | `HTTPlug` | | We welcome contributions to support other programming languages/libraries/frameworks. Please check out more details [here](https://github.com/foss42/apidash/discussions/80). @@ -150,49 +150,49 @@ API Dash is a next-gen API client that supports exploring, testing & previewing Here is the complete list of MIME types that can be directly previewed in API Dash: -| File Type | Mimetype | Extension | Comment | -| --------- | -------------------------- | ----------------- | -------- | -| PDF | `application/pdf` | `.pdf` | | -| Video | `video/mp4` | `.mp4` | | -| Video | `video/webm` | `.webm` | | -| Video | `video/x-ms-wmv` | `.wmv` | | -| Video | `video/x-ms-asf` | `.wmv` | | -| Video | `video/avi` | `.avi` | | -| Video | `video/msvideo` | `.avi` | | -| Video | `video/x-msvideo` | `.avi` | | -| Video | `video/quicktime` | `.mov` | | -| Video | `video/x-quicktime` | `.mov` | | -| Video | `video/x-matroska` | `.mkv` | | -| Image | `image/apng` | `.apng` | Animated | -| Image | `image/avif` | `.avif` | | -| Image | `image/bmp` | `.bmp` | | -| Image | `image/gif` | `.gif` | Animated | -| Image | `image/jpeg` | `.jpeg` or `.jpg` | | -| Image | `image/jp2` | `.jp2` | | -| Image | `image/jpx` | `.jpf` or `.jpx` | | -| Image | `image/pict` | `.pct` | | -| Image | `image/portable-anymap` | `.pnm` | | -| Image | `image/png` | `.png` | | -| Image | `image/sgi` | `.sgi` | | -| Image | `image/svg+xml` | `.svg` | | -| Image | `image/tiff` | `.tiff` | | -| Image | `image/targa` | `.tga` | | -| Image | `image/vnd.wap.wbmp` | `.wbmp` | | -| Image | `image/webp` | `.webp` | | -| Image | `image/xwindowdump` | `.xwd` | | -| Image | `image/x-icon` | `.ico` | | -| Image | `image/x-portable-anymap` | `.pnm` | | -| Image | `image/x-portable-bitmap` | `.pbm` | | -| Image | `image/x-portable-graymap` | `.pgm` | | -| Image | `image/x-portable-pixmap` | `.ppm` | | -| Image | `image/x-tga` | `.tga` | | -| Image | `image/x-xwindowdump` | `.xwd` | | -| Audio | `audio/flac` | `.flac` | | -| Audio | `audio/mpeg` | `.mp3` | | -| Audio | `audio/mp4` | `.m4a` or `.mp4a` | | -| Audio | `audio/x-m4a` | `.m4a` | | -| Audio | `audio/wav` | `.wav` | | -| Audio | `audio/wave` | `.wav` | | +| File Type | Mimetype | Extension | Comment | +| --------- | -------------------------- | ----------------- | --------------- | +| PDF | `application/pdf` | `.pdf` | | +| Video | `video/mp4` | `.mp4` | | +| Video | `video/webm` | `.webm` | | +| Video | `video/x-ms-wmv` | `.wmv` | | +| Video | `video/x-ms-asf` | `.wmv` | | +| Video | `video/avi` | `.avi` | | +| Video | `video/msvideo` | `.avi` | | +| Video | `video/x-msvideo` | `.avi` | | +| Video | `video/quicktime` | `.mov` | | +| Video | `video/x-quicktime` | `.mov` | | +| Video | `video/x-matroska` | `.mkv` | | +| Image | `image/apng` | `.apng` | Animated | +| Image | `image/avif` | `.avif` | | +| Image | `image/bmp` | `.bmp` | | +| Image | `image/gif` | `.gif` | Animated | +| Image | `image/jpeg` | `.jpeg` or `.jpg` | | +| Image | `image/jp2` | `.jp2` | | +| Image | `image/jpx` | `.jpf` or `.jpx` | | +| Image | `image/pict` | `.pct` | | +| Image | `image/portable-anymap` | `.pnm` | | +| Image | `image/png` | `.png` | | +| Image | `image/sgi` | `.sgi` | | +| Image | `image/svg+xml` | `.svg` | | +| Image | `image/tiff` | `.tiff` | | +| Image | `image/targa` | `.tga` | | +| Image | `image/vnd.wap.wbmp` | `.wbmp` | | +| Image | `image/webp` | `.webp` | | +| Image | `image/xwindowdump` | `.xwd` | | +| Image | `image/x-icon` | `.ico` | | +| Image | `image/x-portable-anymap` | `.pnm` | | +| Image | `image/x-portable-bitmap` | `.pbm` | | +| Image | `image/x-portable-graymap` | `.pgm` | | +| Image | `image/x-portable-pixmap` | `.ppm` | | +| Image | `image/x-tga` | `.tga` | | +| Image | `image/x-xwindowdump` | `.xwd` | | +| Audio | `audio/flac` | `.flac` | | +| Audio | `audio/mpeg` | `.mp3` | | +| Audio | `audio/mp4` | `.m4a` or `.mp4a` | | +| Audio | `audio/x-m4a` | `.m4a` | | +| Audio | `audio/wav` | `.wav` | | +| Audio | `audio/wave` | `.wav` | | | CSV | `text/csv` | `.csv` | Can be improved | We welcome PRs to add support for previewing other multimedia MIME types. Please go ahead and raise an issue so that we can discuss the approach. @@ -200,18 +200,18 @@ We are adding support for other MIME types with each release. But, if you are lo Here is the complete list of MIME types that are syntax highlighted in API Dash: -| Mimetype | Extension | Comment | -| ------------------ | --------- | ------------------------------------------------------------------------------------------------------------------ | +| Mimetype | Extension | Comment | +| ------------------ | --------- | ------------------------------------------------------------------------------------------------------------------- | | `application/json` | `.json` | Other MIME types like `application/geo+json`, `application/vcard+json` that are based on `json` are also supported. | | `application/xml` | `.xml` | Other MIME types like `application/xhtml+xml`, `application/vcard+xml` that are based on `xml` are also supported. | -| `text/xml` | `.xml` | | -| `application/yaml` | `.yaml` | Others - `application/x-yaml` or `application/x-yml` | -| `text/yaml` | `.yaml` | Others - `text/yml` | -| `application/sql` | `.sql` | | -| `text/css` | `.css` | | -| `text/html` | `.html` | Only syntax highlighting, no web preview. | -| `text/javascript` | `.js` | | -| `text/markdown` | `.md` | | +| `text/xml` | `.xml` | | +| `application/yaml` | `.yaml` | Others - `application/x-yaml` or `application/x-yml` | +| `text/yaml` | `.yaml` | Others - `text/yml` | +| `application/sql` | `.sql` | | +| `text/css` | `.css` | | +| `text/html` | `.html` | Only syntax highlighting, no web preview. | +| `text/javascript` | `.js` | | +| `text/markdown` | `.md` | | ## What's new in v0.3.0? @@ -223,7 +223,7 @@ Just click on the [Issue tab](https://github.com/foss42/apidash/issues) to raise ## Roadmap -Please find the Roadmap for API Dash [here](https://github.com/foss42/apidash/blob/main/ROADMAP.md). +Please find the Roadmap for API Dash [here](https://github.com/foss42/apidash/blob/main/ROADMAP.md). ## Contribute to API Dash diff --git a/doc/user_guide/env_user_guide.md b/doc/user_guide/env_user_guide.md new file mode 100644 index 00000000..905cb44e --- /dev/null +++ b/doc/user_guide/env_user_guide.md @@ -0,0 +1,29 @@ +# Environment Variables Manager + +The _Environment Variables Manager_ in API Dash allows you to store and reuse values efficiently. It enables you to manage variables and easily switch between different sets of variables. + +## Variable Scopes + +API Dash provides two scopes of variables: + +- **Global variables :** Variables declared in this scope are available regardless of the current active environment. +- **Environment cariables:** Variables declared in a particular environment are available only when that environment is set as active. + +### Scope Hierarchy + +![Image](./images/env/env-variable-scope.png) + +**Environment Scope takes precedence over Global Scope.** + +If a variable with the same name exists in both the Global and Environment Scopes, and the environment is active, the value from the Environment Scope will be used. + +#### Example + +Suppose you have a variable named `API_URL` defined in both the Global Scope and an Environment Scope (e.g., `Development`). + +- Global Scope: + - `API_UR`L = `https://api.foss42.com` +- Development Environment Scope: + - `API_URL` = `https://api.apidash.dev` + +If the `Development` environment is active, `https://api.apidash.dev.com` will be used as the `API_URL`. If no environment is active, or if a different environment is active, the Global Scope value `https://api.foss42.com` will be used. diff --git a/doc/user_guide/images/env/env-variable-scope.png b/doc/user_guide/images/env/env-variable-scope.png new file mode 100644 index 0000000000000000000000000000000000000000..764f56c9656140af18eb1df651eaa60308f91ba5 GIT binary patch literal 21331 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�$e==;uka|6$L!?soyGd-xk8>Sfc^x&ViRny}_%~ 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 R{1k3%{~zeRMSD)f{{s;`#8m(Q literal 0 HcmV?d00001 From 4241f11b0f05d21e4cb446749ed5726e5cd1421d Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sun, 25 Aug 2024 05:54:39 +0530 Subject: [PATCH 51/71] wip: final report --- doc/gsoc/2024/ragul_raj_m.md | 257 +++++++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 doc/gsoc/2024/ragul_raj_m.md diff --git a/doc/gsoc/2024/ragul_raj_m.md b/doc/gsoc/2024/ragul_raj_m.md new file mode 100644 index 00000000..2b415e3a --- /dev/null +++ b/doc/gsoc/2024/ragul_raj_m.md @@ -0,0 +1,257 @@ +--- +title: "GSoC'24 API Dash" +publishedAt: "2024-08-25" +summary: "Summarizing my GSoC'24 contributions to API Dash." +--- + +# GSoC'24 - Android/iOS support for API Dash + +> Final report summarizing my contributions to the project as part of GSoC'24 + +## Project Details + +1. **Contributor** : Ragul Raj M +2. **Mentors** : Ashita P, Ankit M +3. **Organization**: API Dash +4. **Project**: Android/iOS support for API Dash + +#### Quick Links + +- [GSoC Project Page](https://summerofcode.withgoogle.com/programs/2024/projects/exbL7COY) +- [Code Repository](https://github.com/foss42/apidash) +- [Discussion Logs](https://github.com/foss42/apidash/discussions/406) + +## Proposed Objectives + +1. Extend application support to Android & iOS. +2. Add Environment Manager feature with unit testing & widget testing. +3. Add History of Requests feature with unit testing & widget testing. +4. Support for Integration testing. + +## Objectives Summary + +The primary goal was to adapt existing desktop features to work with limited screen space ensuring that API Dash remains intuitive, user-friendly, and accesibile on both Android and iOS platforms. Thus the application should auto adjust to narrow layouts on resizing even on desktop platforms. +In addition, we aimed to add some important core features such as environment variables management, and history of requests which are essential for an API Client and are currently missing. + +## Objectives Completed + +### Responsive & Adaptive Layout (Android & iOS) + +Restructured the app layout with responsive breakpoints to automatically adjust based on available width. Adapted existing components to work with touch inputs, ensuring proper accessibility in Android & iOS. The UI compoents for all new features added are inherently written to be responsive. + +### Environment Manager + +Added a way for users to manage variables with multiple environment support. Made existing fields support environment variables with custom highlighting indicating the status (availability) of the variable and popover to show current value and environment and trigger suggestions of available variables as user types a variable name. Added code to extend the feature to hold secret variables. + +Extended the package [multi_trigger_autocomplete](https://pub.dev/packages/multi_trigger_autocomplete) to support custom `triggerEnd` values and handling triggers that could be a substring of another like `{` and `{{`. + +### History of Requests + +Implemented a comprehensive request history feature that enables users to review all the requests and responses they've sent and received in the past. The history is organized with proper grouping of similar requests and sorted by timestamps for easy navigation. Users have the ability to navigate to the original request or duplicate any request directly from the history, carrying over all specific configurations, allowing them to edit the request. + +Additionally, added an option for users to customize their request history retention period. They can choose from predefined periods—one week, one month, three months, or keep the history indefinitely. This setting ensures that the request history is automatically cleared according to the user's preference, maintaining a clean and efficient history log. + +### Integration Tests + +In addition to testing each model, utility and widget added with the new features, I implemented end-to-end integration tests for both existing and new functionalities. These integration tests are written in a modular way, allowing for easy reuse of user actions across different tests. + +## Pull Request Report + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FeaturePull Requests
Android & iOS Support
+ feat: mobile support +
+ feat: desktop responsiveness +
Environment Manager
+ feat: environment manager +
+ fix: environment field issues +
+ test: environment manager tests +
History of Requests
+ feat: history of requests +
+ test: history of requests tests +
Integration Tests
+ test: integration tests for env manager +
+ test: integration tests for history of requests +
+ test: integration tests for request editor +
UI fixes
+ fix: color, deprecations and tests +
+ feat: about dialog +
+ fix: renderflex overflow issues in pane +
+ fix: resizing split views +
+ fix: refactored ui +
+ +## Challenges Faced + +### 1. Implementing Environment Suggestion field + +#### Task + +1. Implement a field that displays suitable variable suggestions when the user types the trigger character `{` or `{{`. +2. Selecting the suggestion should replace the characters in the field with variable. +3. The variable in the field should have appropriate styling (highlighted) according to the variable's availability in the active environment. +4. Hovering the variable should show a Popover with the value the variable will get replaced with and which environment it belongs to. + +#### Attempts, Problems and Solution + +**1. Attempt:** + +I initially found [mention_tag_text_field](https://pub.dev/packages/mention_tag_text_field) which handled both the triggering and allowed custom styling. I configured the field provided by the package and used [flutter_portal](https://pub.dev/packages/flutter_portal) to display suggestions and popover on hovering. + +**Problems:** + +- I couldn't make the field work for both triggers `{` and `{{` since the former is a substring of latter so, it always matches the trigger to `{`. +- The field didn't support copying and pasting of its content with the variable. + +**2. Attempt:** + +Used [multi_trigger_autocomplete](https://pub.dev/packages/multi_trigger_autocomplete) for handling triggers, and [extended_text_field](https://pub.dev/packages/extended_text_field) for custom styling the variable. Using separate packages gave me more control and helped in the separation of concerns. + +**Problems:** + +- [multi_trigger_autocomplete](https://pub.dev/packages/multi_trigger_autocomplete) implicitly added a `" "` and didn't allow customizing it. +- It too couldn't choose the correct trigger among `{` and `{{`. + +**Final Solution:** + +I had to extend the [multi_trigger_autocomplete](https://pub.dev/packages/multi_trigger_autocomplete) package to allow having custom end Character (`triggerEnd`) instead of `" "` and also choose the right trigger when multiple triggers match the user typed string. + +> Link to forked repository: [foss42/multi_trigger_autocomplete](https://github.com/foss42/multi_trigger_autocomplete) + +### 2. Loading History of Requests Optimally + +#### Issue + +The history of requests stores all instances of requests and responses sent by the user, so loading all instances into memory can cause significant overhead. + +#### Solution + +Use a normal [Box](https://pub.dev/documentation/hive/latest/hive/Box-class.html) to store metadata of all request history, while using a [LazyBox](https://pub.dev/documentation/hive/latest/hive/LazyBox-class.html) to load the full request and response data. Ensure proper concurrency between the history requests stored in both boxes. + +Implement auto-clearing to automatically remove old history requests on app start, and allow users to select their preferred request history retention period. + +### 3. Writing Integration Tests + +#### Issue + +I encountered an issue where the integration test preview was hanging at the 'Test starting' splash screen. Initially, I assumed it was related to Hive setup, so I created a simple counter app using Hive and Riverpod to test it out. However, the integration tests worked fine in that app. Running the test on Android also resulted in the same issue, confirming that it wasn't caused by any window-related packages specific to Desktop. + +Runing the test with `flutter drive` gave the following logs. + +``` +... +VMServiceFlutterDriver: Isolate found with number: 986834591817471 +VMServiceFlutterDriver: Isolate is paused at start. +VMServiceFlutterDriver: Attempting to resume isolate +... +``` + +Investigating these logs online, I found related issues. + +- [integration_test package pauses isolates and calling pumpAndSettle() never finishes.](https://github.com/flutter/flutter/issues/73355) +- [flutter_driver pauses all isolates at their startup (even ones started from compute()), rather than only pausing initial isolate](https://github.com/flutter/flutter/issues/24703) +- [Integration test example boilerplate to support async main](https://github.com/flutter/flutter/issues/88549) +- [Can't get past splash screen after integration_test migration](https://stackoverflow.com/questions/67762969/cant-get-past-splash-screen-after-integration-test-migration) +- [Flutter Driver hangs at splash screen](https://stackoverflow.com/questions/64797858/flutter-driver-hangs-at-splash-screen) + +#### Solution + +With the help of mentors gathered a list of popular open source flutter apps which have migrated to `integration_test`. Looked into how [immich](https://github.com/immich-app/immich/tree/main/mobile/integration_test) are writing their integration tests and mimicked their setup process. + +### 4. Package and SDK upgrades + +The [multi_split_view](https://pub.dev/packages/multi_split_view) introduced breaking changes that disrupted the split views in API Dash's application. To address this, we had to carefully evaluate and choose the most appropriate solution from three different options. + +While implementing the new features, a Flutter SDK upgrade introduced deprecations and theme color changes that disrupted the application's appearance. As a result, I had to address the deprecations and adjust some widget stylings to maintain the original look and feel. + +## Future Work + +### Environment secrets + +The environment manager feature can be extended to support secret variables whose values are obscured in the UI. + +### Isolate auto-clearing + +The auto-clearing of requests currently happens at app start and can potentially delay the loading of the app. Optimizing this process by running the auto-clear operation asynchronously after the UI has loaded or moving it to a isolate could improve startup performance. + +## Design and Prototypes Link + +1. [Initial Mobile App Design](https://www.figma.com/file/6gsFPwN1NRv7XuVpXZ9FrP/apidash_mobile-DenserMeerkat?type=design&node-id=0%3A1&mode=design&t=uumUAO9f7BfcWAag-1) +2. [History of Request Hive box Schema](https://www.tldraw.com/s/v2_c_k5WBraUFDTsARPpQxNAVa?v=0,400,1536,730&p=kdBoBzSnWE6NZ5xjvaLmB) +3. [History of Request UI Design](https://www.tldraw.com/s/v2_c_k5WBraUFDTsARPpQxNAVa?v=0,100,1537,730&p=page) From a27950adff897f3b7582a7b5d4ed0df88f18bc40 Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Sun, 25 Aug 2024 13:05:04 +0530 Subject: [PATCH 52/71] sort alphabetically --- pubspec.lock | 36 ++++++++--------- pubspec.yaml | 108 +++++++++++++++++++++++++-------------------------- 2 files changed, 72 insertions(+), 72 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 80c375d3..f889255f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -813,18 +813,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -877,18 +877,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: transitive description: @@ -1070,10 +1070,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -1363,26 +1363,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" + sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" url: "https://pub.dev" source: hosted - version: "1.25.2" + version: "1.25.7" test_api: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" test_core: dependency: transitive description: name: test_core - sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" + sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.4" textwrap: dependency: transitive description: @@ -1563,10 +1563,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.5" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5563422f..fc7e68af 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,87 +10,87 @@ environment: dependencies: flutter: sdk: flutter - multi_split_view: ^3.2.2 - url_launcher: ^6.2.5 - flutter_riverpod: ^2.5.1 - riverpod: ^2.5.1 - uuid: ^4.3.3 - http: ^1.2.1 - http_parser: ^4.0.2 + code_builder: ^4.10.0 collection: ^1.18.0 - google_fonts: ^6.2.1 - highlighter: ^0.1.1 - xml: ^6.3.0 - jinja: ^0.6.0 - window_size: + csv: ^6.0.0 + curl_converter: git: - url: https://github.com/google/flutter-desktop-embedding.git - path: plugins/window_size - hive_flutter: ^1.1.0 - lottie: ^3.1.0 - mime_dart: ^3.0.0 - path_provider: ^2.1.2 - window_manager: ^0.3.8 - path: ^1.8.3 + url: https://github.com/foss42/curl_converter.git + ref: 726e8cd04aeb326211af27f75920be5b21c90bb4 + data_table_2: ^2.5.15 + dart_style: ^2.3.6 + desktop_drop: ^0.4.4 + extended_text_field: ^15.0.0 + file_selector: ^1.0.3 + flex_color_scheme: ^7.3.1 + flutter_hooks: ^0.20.5 flutter_markdown: ^0.7.2+1 - markdown: ^7.2.2 - just_audio: ^0.9.34 - just_audio_mpv: ^0.1.7 - just_audio_windows: ^0.2.0 - freezed_annotation: ^2.4.1 - json_annotation: ^4.9.0 - printing: ^5.12.0 - package_info_plus: ^8.0.0 + flutter_portal: ^1.1.4 + flutter_riverpod: ^2.5.1 + flutter_svg: ^2.0.10+1 flutter_typeahead: ^5.2.0 - provider: ^6.1.2 + freezed_annotation: ^2.4.1 fvp: ^0.17.0 - video_player: ^2.8.7 - video_player_platform_interface: ^6.2.2 + google_fonts: ^6.2.1 + highlighter: ^0.1.1 + hive_flutter: ^1.1.0 + hooks_riverpod: ^2.5.1 + http: ^1.2.1 + http_parser: ^4.0.2 + intl: ^0.19.0 + jinja: ^0.6.0 + json_annotation: ^4.9.0 json_data_explorer: git: url: https://github.com/foss42/json_data_explorer.git ref: b7dde2f85dff4f482eed7eda4ef2a71344ef8b3a - scrollable_positioned_list: ^0.3.8 - flutter_svg: ^2.0.10+1 - vector_graphics_compiler: ^1.1.9+1 - code_builder: ^4.10.0 - dart_style: ^2.3.6 json_text_field: ^1.2.0 - csv: ^6.0.0 - flex_color_scheme: ^7.3.1 - data_table_2: ^2.5.15 - file_selector: ^1.0.3 - hooks_riverpod: ^2.5.1 - flutter_hooks: ^0.20.5 - flutter_portal: ^1.1.4 - intl: ^0.19.0 + just_audio: ^0.9.34 + just_audio_mpv: ^0.1.7 + just_audio_windows: ^0.2.0 + lottie: ^3.1.0 + markdown: ^7.2.2 + mime_dart: ^3.0.0 + multi_split_view: ^3.2.2 multi_trigger_autocomplete: git: url: https://github.com/foss42/multi_trigger_autocomplete.git ref: cb22bab30dd14452d184bc6ad3bb41b612b22c70 - extended_text_field: ^15.0.0 - desktop_drop: ^0.4.4 - curl_converter: + package_info_plus: ^8.0.0 + path: ^1.8.3 + path_provider: ^2.1.2 + printing: ^5.12.0 + provider: ^6.1.2 + riverpod: ^2.5.1 + scrollable_positioned_list: ^0.3.8 + url_launcher: ^6.2.5 + uuid: ^4.3.3 + vector_graphics_compiler: ^1.1.9+1 + video_player: ^2.8.7 + video_player_platform_interface: ^6.2.2 + window_manager: ^0.3.8 + window_size: git: - url: https://github.com/foss42/curl_converter.git - ref: 726e8cd04aeb326211af27f75920be5b21c90bb4 + url: https://github.com/google/flutter-desktop-embedding.git + path: plugins/window_size + xml: ^6.3.0 dependency_overrides: - web: ^0.5.0 pdf_widget_wrapper: ^1.0.4 + web: ^0.5.0 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^4.0.0 - flutter_launcher_icons: ^0.13.1 - test: ^1.25.2 build_runner: ^2.4.11 + flutter_launcher_icons: ^0.13.1 + flutter_lints: ^4.0.0 freezed: ^2.5.2 json_serializable: ^6.7.1 - spot: ^0.13.0 integration_test: sdk: flutter + spot: ^0.13.0 + test: ^1.25.2 flutter: uses-material-design: true From 3e3ba2a26401654908176032bd75b5dbc65f04a0 Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Sun, 25 Aug 2024 15:26:04 +0530 Subject: [PATCH 53/71] Upgrade to work with Flutter 3.24.1 --- pubspec.lock | 59 ++++++++++++++++++++++++++++++++-------------------- pubspec.yaml | 19 +++++++++-------- 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index f889255f..f4140996 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 url: "https://pub.dev" source: hosted - version: "67.0.0" + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" analyzer: dependency: transitive description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.7.0" archive: dependency: transitive description: @@ -109,10 +114,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" + sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04 url: "https://pub.dev" source: hosted - version: "2.4.11" + version: "2.4.12" build_runner_core: dependency: transitive description: @@ -302,10 +307,10 @@ packages: dependency: "direct main" description: name: extended_text_field - sha256: "954c7eea1e82728a742f7ddf09b9a51cef087d4f52b716ba88cb3eb78ccd7c6e" + sha256: d3f3f8d37e516f0e805b4a41283833f86fc10fecc83abe51d7223ec9a64e136a url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "16.0.0" extended_text_library: dependency: transitive description: @@ -512,10 +517,10 @@ packages: dependency: "direct main" description: name: flutter_markdown - sha256: "2e8a801b1ded5ea001a4529c97b1f213dcb11c6b20668e081cafb23468593514" + sha256: a23c41ee57573e62fc2190a1f36a0480c4d90bde3a8a8d7126e5d5992fb53fb7 url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.3+1" flutter_portal: dependency: "direct main" description: @@ -562,10 +567,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1 + sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.7" freezed_annotation: dependency: "direct main" description: @@ -591,10 +596,10 @@ packages: dependency: "direct main" description: name: fvp - sha256: "398f8c342f40005f2f881b926b96a7bbf0b9c0c59267ce5c47377a1d4c324088" + sha256: "6462fd078de4478a0990d437463897036cff98aff3f0ac9efbbf817c99654c87" url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.24.1" glob: dependency: transitive description: @@ -647,10 +652,10 @@ packages: dependency: "direct main" description: name: hooks_riverpod - sha256: "45b2030a18bcd6dbd680c2c91bc3b33e3fe7c323e3acb5ecec93a613e2fbaa8a" + sha256: "97266a91c994951a06ef0ff3a1c7fb261e52ec7f74e87f0614ea0b7411b859b2" url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "2.5.2" html: dependency: transitive description: @@ -857,6 +862,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" markdown: dependency: "direct main" description: @@ -966,10 +979,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "4de6c36df77ffbcef0a5aefe04669d33f2d18397fea228277b852a2d4e58e860" + sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918 url: "https://pub.dev" source: hosted - version: "8.0.1" + version: "8.0.2" package_info_plus_platform_interface: dependency: transitive description: @@ -1126,10 +1139,10 @@ packages: dependency: "direct main" description: name: printing - sha256: cc4b256a5a89d5345488e3318897b595867f5181b8c5ed6fc63bfa5f2044aec3 + sha256: de1889f30b34029fc46e5de6a9841498850b23d32942a9ee810ca36b0cb1b234 url: "https://pub.dev" source: hosted - version: "5.13.1" + version: "5.13.2" process: dependency: transitive description: @@ -1619,10 +1632,10 @@ packages: dependency: "direct main" description: name: window_manager - sha256: "8699323b30da4cdbe2aa2e7c9de567a6abd8a97d9a5c850a3c86dcd0b34bbfbf" + sha256: ab8b2a7f97543d3db2b506c9d875e637149d48ee0c6a5cb5f5fd6e0dac463792 url: "https://pub.dev" source: hosted - version: "0.3.9" + version: "0.4.2" window_size: dependency: "direct main" description: @@ -1657,5 +1670,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0 <3.999.0" + dart: ">=3.5.0-259.0.dev <3.999.0" flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index fc7e68af..0eadf529 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,21 +20,21 @@ dependencies: data_table_2: ^2.5.15 dart_style: ^2.3.6 desktop_drop: ^0.4.4 - extended_text_field: ^15.0.0 + extended_text_field: ^16.0.0 file_selector: ^1.0.3 flex_color_scheme: ^7.3.1 flutter_hooks: ^0.20.5 - flutter_markdown: ^0.7.2+1 + flutter_markdown: ^0.7.3+1 flutter_portal: ^1.1.4 flutter_riverpod: ^2.5.1 flutter_svg: ^2.0.10+1 flutter_typeahead: ^5.2.0 freezed_annotation: ^2.4.1 - fvp: ^0.17.0 + fvp: ^0.24.1 google_fonts: ^6.2.1 highlighter: ^0.1.1 hive_flutter: ^1.1.0 - hooks_riverpod: ^2.5.1 + hooks_riverpod: ^2.5.2 http: ^1.2.1 http_parser: ^4.0.2 intl: ^0.19.0 @@ -56,10 +56,10 @@ dependencies: git: url: https://github.com/foss42/multi_trigger_autocomplete.git ref: cb22bab30dd14452d184bc6ad3bb41b612b22c70 - package_info_plus: ^8.0.0 + package_info_plus: ^8.0.2 path: ^1.8.3 path_provider: ^2.1.2 - printing: ^5.12.0 + printing: ^5.13.2 provider: ^6.1.2 riverpod: ^2.5.1 scrollable_positioned_list: ^0.3.8 @@ -68,7 +68,7 @@ dependencies: vector_graphics_compiler: ^1.1.9+1 video_player: ^2.8.7 video_player_platform_interface: ^6.2.2 - window_manager: ^0.3.8 + window_manager: ^0.4.2 window_size: git: url: https://github.com/google/flutter-desktop-embedding.git @@ -76,16 +76,17 @@ dependencies: xml: ^6.3.0 dependency_overrides: + extended_text_field: ^16.0.0 pdf_widget_wrapper: ^1.0.4 web: ^0.5.0 dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.4.11 + build_runner: ^2.4.12 flutter_launcher_icons: ^0.13.1 flutter_lints: ^4.0.0 - freezed: ^2.5.2 + freezed: ^2.5.7 json_serializable: ^6.7.1 integration_test: sdk: flutter From 78d8cac6fb6f4835cb3771edb62de0f29f90712a Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sun, 25 Aug 2024 19:30:01 +0530 Subject: [PATCH 54/71] fix: ui corrections --- lib/screens/common_widgets/button_navbar.dart | 29 ++++++------ .../common_widgets/env_trigger_field.dart | 3 ++ lib/screens/history/history_requests.dart | 45 ++++++++++--------- .../history/history_widgets/his_url_card.dart | 2 +- lib/widgets/button_group_filled.dart | 2 +- 5 files changed, 42 insertions(+), 39 deletions(-) diff --git a/lib/screens/common_widgets/button_navbar.dart b/lib/screens/common_widgets/button_navbar.dart index b9ea8e61..39b7938a 100644 --- a/lib/screens/common_widgets/button_navbar.dart +++ b/lib/screens/common_widgets/button_navbar.dart @@ -29,7 +29,6 @@ class NavbarButton extends ConsumerWidget { final mobileScaffoldKeyNotifier = ref.watch(mobileScaffoldKeyStateProvider.notifier); final bool isSelected = railIdx == buttonIdx; - final Size size = isCompact ? const Size(56, 32) : const Size(65, 32); var onPress = isSelected ? null : () { @@ -49,20 +48,20 @@ class NavbarButton extends ConsumerWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - TextButton( - style: isSelected - ? TextButton.styleFrom( - fixedSize: size, - backgroundColor: - Theme.of(context).colorScheme.secondaryContainer, - ) - : TextButton.styleFrom( - fixedSize: size, - ), - onPressed: onPress, - child: Icon( - isSelected ? selectedIcon : icon, - color: Theme.of(context).colorScheme.onSurfaceVariant, + SizedBox( + height: isCompact ? 36 : 36, + child: TextButton( + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + backgroundColor: isSelected + ? Theme.of(context).colorScheme.secondaryContainer + : null, + ), + onPressed: onPress, + child: Icon( + isSelected ? selectedIcon : icon, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ), showLabel ? const SizedBox(height: 4) : const SizedBox.shrink(), diff --git a/lib/screens/common_widgets/env_trigger_field.dart b/lib/screens/common_widgets/env_trigger_field.dart index dcdbde3c..23f01d1b 100644 --- a/lib/screens/common_widgets/env_trigger_field.dart +++ b/lib/screens/common_widgets/env_trigger_field.dart @@ -106,6 +106,9 @@ class EnvironmentTriggerFieldState extends State { onChanged: widget.onChanged, onSubmitted: widget.onFieldSubmitted, specialTextSpanBuilder: EnvRegExpSpanBuilder(), + onTapOutside: (event) { + focusNode.unfocus(); + }, ); }, ); diff --git a/lib/screens/history/history_requests.dart b/lib/screens/history/history_requests.dart index 7927cd3c..49f6fc10 100644 --- a/lib/screens/history/history_requests.dart +++ b/lib/screens/history/history_requests.dart @@ -129,33 +129,34 @@ class Grabber extends StatelessWidget { @override Widget build(BuildContext context) { - if (!isOnDesktopAndWeb) { - return const SizedBox.shrink(); - } final ColorScheme colorScheme = Theme.of(context).colorScheme; - return GestureDetector( - onVerticalDragUpdate: onVerticalDragUpdate, - child: Container( - width: double.infinity, - decoration: BoxDecoration( - color: colorScheme.surfaceContainerLow, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(16), topRight: Radius.circular(16)), - ), - child: Align( - alignment: Alignment.topCenter, - child: Container( - margin: kPv10, - width: 80.0, - height: 6.0, - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(8.0), - ), + final handle = Container( + width: double.infinity, + decoration: BoxDecoration( + color: colorScheme.surfaceContainerLow, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), topRight: Radius.circular(16)), + ), + child: Align( + alignment: Alignment.topCenter, + child: Container( + margin: kPv10, + width: 80.0, + height: 6.0, + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8.0), ), ), ), ); + if (!isOnDesktopAndWeb) { + return handle; + } + return GestureDetector( + onVerticalDragUpdate: onVerticalDragUpdate, + child: handle, + ); } } diff --git a/lib/screens/history/history_widgets/his_url_card.dart b/lib/screens/history/history_widgets/his_url_card.dart index f7fd89b3..f0e70a2e 100644 --- a/lib/screens/history/history_widgets/his_url_card.dart +++ b/lib/screens/history/history_widgets/his_url_card.dart @@ -21,7 +21,7 @@ class HistoryURLCard extends StatelessWidget { return LayoutBuilder(builder: (context, constraints) { final isCompact = constraints.maxWidth <= kMinWindowSize.width; - final isExpanded = constraints.maxWidth >= kMediumWindowWidth; + final isExpanded = constraints.maxWidth >= kMediumWindowWidth - 8; return Card( color: kColorTransparent, surfaceTintColor: kColorTransparent, diff --git a/lib/widgets/button_group_filled.dart b/lib/widgets/button_group_filled.dart index c27d96c5..722ed403 100644 --- a/lib/widgets/button_group_filled.dart +++ b/lib/widgets/button_group_filled.dart @@ -37,7 +37,7 @@ class FilledButtonGroup extends StatelessWidget { } } return ClipRRect( - borderRadius: kBorderRadius20, + borderRadius: BorderRadius.circular(88), child: Row( mainAxisSize: MainAxisSize.min, children: buttonsWithSpacers, From 4bb5afecb6ae4b04289238730db9e96c14571684 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sun, 25 Aug 2024 21:04:45 +0530 Subject: [PATCH 55/71] test: common runner file --- .../desktop/env_manager_test.dart | 6 ++- .../desktop/his_request_test.dart | 6 ++- integration_test/desktop/req_editor_test.dart | 6 ++- integration_test/mobile/env_manager_test.dart | 54 ++++++++++++++----- integration_test/mobile/his_request_test.dart | 6 ++- integration_test/mobile/req_editor_test.dart | 6 ++- integration_test/runner.dart | 22 ++++++++ 7 files changed, 87 insertions(+), 19 deletions(-) create mode 100644 integration_test/runner.dart diff --git a/integration_test/desktop/env_manager_test.dart b/integration_test/desktop/env_manager_test.dart index b68d168c..cdd748a2 100644 --- a/integration_test/desktop/env_manager_test.dart +++ b/integration_test/desktop/env_manager_test.dart @@ -11,7 +11,11 @@ import 'package:apidash/screens/home_page/editor_pane/url_card.dart'; import '../../test/extensions/widget_tester_extensions.dart'; import '../test_helper.dart'; -void main() async { +Future main() async { + await runDesktopEnvIntegrationTest(); +} + +Future runDesktopEnvIntegrationTest() async { const environmentName = "test-env-name"; const envVarName = "test-env-var"; const envVarValue = "8700000"; diff --git a/integration_test/desktop/his_request_test.dart b/integration_test/desktop/his_request_test.dart index 41e739ea..b47fe6e7 100644 --- a/integration_test/desktop/his_request_test.dart +++ b/integration_test/desktop/his_request_test.dart @@ -8,7 +8,11 @@ import 'package:apidash/widgets/widgets.dart'; import '../../test/extensions/widget_tester_extensions.dart'; import '../test_helper.dart'; -void main() async { +Future main() async { + await runDesktopHisIntegrationTest(); +} + +Future runDesktopHisIntegrationTest() async { await ApidashTestHelper.initialize( size: Size(kExpandedWindowWidth, kMinWindowSize.height)); apidashWidgetTest("Testing History of Requests in desktop end-to-end", diff --git a/integration_test/desktop/req_editor_test.dart b/integration_test/desktop/req_editor_test.dart index 146732da..abdb24cf 100644 --- a/integration_test/desktop/req_editor_test.dart +++ b/integration_test/desktop/req_editor_test.dart @@ -7,7 +7,11 @@ import 'package:apidash/widgets/widgets.dart'; import '../../test/extensions/widget_tester_extensions.dart'; import '../test_helper.dart'; -void main() async { +Future main() async { + await runDesktopReqIntegrationTest(); +} + +Future runDesktopReqIntegrationTest() async { const reqName = "test-req-name"; const testEndpoint = "https://api.apidash.dev/humanize/social"; const paramKey = "num"; diff --git a/integration_test/mobile/env_manager_test.dart b/integration_test/mobile/env_manager_test.dart index 12d99d4b..d6544331 100644 --- a/integration_test/mobile/env_manager_test.dart +++ b/integration_test/mobile/env_manager_test.dart @@ -11,7 +11,11 @@ import 'package:apidash/screens/home_page/editor_pane/url_card.dart'; import '../../test/extensions/widget_tester_extensions.dart'; import '../test_helper.dart'; -void main() async { +Future main() async { + await runMobileEnvIntegrationTest(); +} + +Future runMobileEnvIntegrationTest() async { const environmentName = "test-env-name"; const envVarName = "test-env-var"; const envVarValue = "8700000"; @@ -73,13 +77,26 @@ void main() async { addTearDown(gesture.removePointer); await tester.pump(); - /// Check if environment variable is shown on hover - await gesture.moveTo(tester.getCenter(find.descendant( - of: find.byType(URLTextField), - matching: find.text('{{$envVarName}}')))); - await tester.pumpAndSettle(); - expect(find.text(envVarValue), findsOneWidget); - await gesture.moveBy(const Offset(0, 100)); + /// Check if environment variable is shown + if (kIsMobile) { + // TODO: Unable to get Popover to show on mobile + // await tester.tapAt(tester.getCenter(find.descendant( + // of: find.byType(URLTextField), + // matching: find.text('{{$envVarName}}')))); + // await tester.pumpAndSettle(); + // expect(find.text(envVarValue), findsOneWidget); + // await tester.tapAt(tester.getCenter(find.descendant( + // of: find.byType(URLTextField), + // matching: find.byType(WidgetSpan))) + + // const Offset(0, 100)); + } else { + await gesture.moveTo(tester.getCenter(find.descendant( + of: find.byType(URLTextField), + matching: find.text('{{$envVarName}}')))); + await tester.pumpAndSettle(); + expect(find.text(envVarValue), findsOneWidget); + await gesture.moveBy(const Offset(0, 100)); + } await Future.delayed(const Duration(milliseconds: 500)); await helper.navigateToSettings(scaffoldKey: kHomeScaffoldKey); @@ -111,12 +128,21 @@ void main() async { await helper.navigateToRequestEditor(scaffoldKey: kEnvScaffoldKey); await Future.delayed(const Duration(milliseconds: 200)); - /// Check if environment variable is now shown on hover - await gesture.moveTo(tester.getCenter(find.descendant( - of: find.byType(URLTextField), - matching: find.text('{{$envVarName}}')))); - await tester.pumpAndSettle(); - expect(find.text(unknown), findsNWidgets(2)); + /// Check if environment variable is now shown as unknown + if (kIsMobile) { + // TODO: Unable to get Popover to show on mobile + // await tester.tapAt(tester.getCenter(find.descendant( + // of: find.byType(URLTextField), + // matching: find.text('{{$envVarName}}')))); + // await tester.pumpAndSettle(); + // expect(find.text(unknown), findsNWidgets(2)); + } else { + await gesture.moveTo(tester.getCenter(find.descendant( + of: find.byType(URLTextField), + matching: find.text('{{$envVarName}}')))); + await tester.pumpAndSettle(); + expect(find.text(unknown), findsNWidgets(2)); + } await Future.delayed(const Duration(milliseconds: 500)); }); diff --git a/integration_test/mobile/his_request_test.dart b/integration_test/mobile/his_request_test.dart index 7de01ec9..c2544bc0 100644 --- a/integration_test/mobile/his_request_test.dart +++ b/integration_test/mobile/his_request_test.dart @@ -8,7 +8,11 @@ import 'package:apidash/screens/history/history_widgets/history_widgets.dart'; import '../../test/extensions/widget_tester_extensions.dart'; import '../test_helper.dart'; -void main() async { +Future main() async { + await runMobileHisIntegrationTest(); +} + +Future runMobileHisIntegrationTest() async { await ApidashTestHelper.initialize( size: Size(kCompactWindowWidth, kMinWindowSize.height)); apidashWidgetTest("Testing History of Requests in mobile end-to-end", diff --git a/integration_test/mobile/req_editor_test.dart b/integration_test/mobile/req_editor_test.dart index 032c1818..a407db3e 100644 --- a/integration_test/mobile/req_editor_test.dart +++ b/integration_test/mobile/req_editor_test.dart @@ -7,7 +7,11 @@ import 'package:apidash/widgets/widgets.dart'; import '../../test/extensions/widget_tester_extensions.dart'; import '../test_helper.dart'; -void main() async { +Future main() async { + await runMobileReqIntegrationTest(); +} + +Future runMobileReqIntegrationTest() async { const reqName = "test-req-name"; const testEndpoint = "https://api.apidash.dev/humanize/social"; const paramKey = "num"; diff --git a/integration_test/runner.dart b/integration_test/runner.dart new file mode 100644 index 00000000..a2b88c0d --- /dev/null +++ b/integration_test/runner.dart @@ -0,0 +1,22 @@ +import 'dart:io'; + +import 'desktop/env_manager_test.dart'; +import 'desktop/his_request_test.dart'; +import 'desktop/req_editor_test.dart'; +import 'mobile/env_manager_test.dart'; +import 'mobile/his_request_test.dart'; +import 'mobile/req_editor_test.dart'; + +Future main() async { + if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { + await runDesktopReqIntegrationTest(); + await runDesktopEnvIntegrationTest(); + await runDesktopHisIntegrationTest(); + } else if (Platform.isAndroid || Platform.isIOS) { + await runMobileReqIntegrationTest(); + await runMobileEnvIntegrationTest(); + await runMobileHisIntegrationTest(); + } else { + throw Exception("Unsupported Platform"); + } +} From 55ad184a494ac0cf8edac985d99336b575212eec Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Sun, 25 Aug 2024 23:05:44 +0530 Subject: [PATCH 56/71] Update for running for responsive widths in desktop --- integration_test/desktop/env_manager_test.dart | 5 ++--- integration_test/desktop/his_request_test.dart | 8 ++------ integration_test/desktop/req_editor_test.dart | 6 ++---- integration_test/mobile/env_manager_test.dart | 5 ++--- integration_test/mobile/his_request_test.dart | 6 ++---- integration_test/mobile/req_editor_test.dart | 5 ++--- integration_test/runner.dart | 10 ++++++---- integration_test/test_helper.dart | 3 +++ 8 files changed, 21 insertions(+), 27 deletions(-) diff --git a/integration_test/desktop/env_manager_test.dart b/integration_test/desktop/env_manager_test.dart index cdd748a2..8b35fe4b 100644 --- a/integration_test/desktop/env_manager_test.dart +++ b/integration_test/desktop/env_manager_test.dart @@ -23,9 +23,8 @@ Future runDesktopEnvIntegrationTest() async { const unknown = "unknown"; const expectedCurlCode = "curl --url '$testEndpoint$envVarValue'"; - await ApidashTestHelper.initialize( - size: Size(kExpandedWindowWidth, kMinWindowSize.height)); - apidashWidgetTest("Testing Environment Manager in desktop end-to-end", + apidashWidgetTest( + "Testing Environment Manager in desktop end-to-end", kExpandedWindowWidth, (WidgetTester tester, helper) async { await tester.pumpUntilFound(find.byType(DashApp)); await Future.delayed(const Duration(seconds: 1)); diff --git a/integration_test/desktop/his_request_test.dart b/integration_test/desktop/his_request_test.dart index b47fe6e7..b643d7e6 100644 --- a/integration_test/desktop/his_request_test.dart +++ b/integration_test/desktop/his_request_test.dart @@ -1,6 +1,3 @@ -import 'dart:ui'; - -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:apidash/app.dart'; import 'package:apidash/consts.dart'; @@ -13,9 +10,8 @@ Future main() async { } Future runDesktopHisIntegrationTest() async { - await ApidashTestHelper.initialize( - size: Size(kExpandedWindowWidth, kMinWindowSize.height)); - apidashWidgetTest("Testing History of Requests in desktop end-to-end", + apidashWidgetTest( + "Testing History of Requests in desktop end-to-end", kExpandedWindowWidth, (WidgetTester tester, helper) async { await tester.pumpUntilFound(find.byType(DashApp)); await Future.delayed(const Duration(seconds: 1)); diff --git a/integration_test/desktop/req_editor_test.dart b/integration_test/desktop/req_editor_test.dart index abdb24cf..a3f81ddd 100644 --- a/integration_test/desktop/req_editor_test.dart +++ b/integration_test/desktop/req_editor_test.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spot/spot.dart'; import 'package:apidash/app.dart'; @@ -23,9 +22,8 @@ Future runDesktopReqIntegrationTest() async { "data": "870K" }'''; - await ApidashTestHelper.initialize( - size: Size(kExpandedWindowWidth, kMinWindowSize.height)); - apidashWidgetTest("Testing Request Editor in desktop end-to-end", + apidashWidgetTest( + "Testing Request Editor in desktop end-to-end", kExpandedWindowWidth, (WidgetTester tester, helper) async { await tester.pumpUntilFound(find.byType(DashApp)); await Future.delayed(const Duration(seconds: 1)); diff --git a/integration_test/mobile/env_manager_test.dart b/integration_test/mobile/env_manager_test.dart index d6544331..0fd810fd 100644 --- a/integration_test/mobile/env_manager_test.dart +++ b/integration_test/mobile/env_manager_test.dart @@ -24,9 +24,8 @@ Future runMobileEnvIntegrationTest() async { const unknown = "unknown"; const expectedCurlCode = "curl --url '$testEndpoint$envVarValue'"; - await ApidashTestHelper.initialize( - size: Size(kCompactWindowWidth, kMinWindowSize.height)); - apidashWidgetTest("Testing Environment Manager in mobile end-to-end", + apidashWidgetTest( + "Testing Environment Manager in mobile end-to-end", kCompactWindowWidth, (WidgetTester tester, helper) async { await tester.pumpUntilFound(find.byType(DashApp)); await Future.delayed(const Duration(seconds: 1)); diff --git a/integration_test/mobile/his_request_test.dart b/integration_test/mobile/his_request_test.dart index c2544bc0..995284e0 100644 --- a/integration_test/mobile/his_request_test.dart +++ b/integration_test/mobile/his_request_test.dart @@ -13,9 +13,8 @@ Future main() async { } Future runMobileHisIntegrationTest() async { - await ApidashTestHelper.initialize( - size: Size(kCompactWindowWidth, kMinWindowSize.height)); - apidashWidgetTest("Testing History of Requests in mobile end-to-end", + apidashWidgetTest( + "Testing History of Requests in mobile end-to-end", kCompactWindowWidth, (WidgetTester tester, helper) async { await tester.pumpUntilFound(find.byType(DashApp)); await Future.delayed(const Duration(seconds: 1)); @@ -29,7 +28,6 @@ Future runMobileHisIntegrationTest() async { ); await Future.delayed(const Duration(milliseconds: 200)); await helper.reqHelper.sendRequest(); - await helper.reqHelper.sendRequest(); /// Navigate to History await helper.navigateToHistory(scaffoldKey: kHomeScaffoldKey); diff --git a/integration_test/mobile/req_editor_test.dart b/integration_test/mobile/req_editor_test.dart index a407db3e..738d2fc4 100644 --- a/integration_test/mobile/req_editor_test.dart +++ b/integration_test/mobile/req_editor_test.dart @@ -23,9 +23,8 @@ Future runMobileReqIntegrationTest() async { "data": "870K" }'''; - await ApidashTestHelper.initialize( - size: Size(kCompactWindowWidth, kMinWindowSize.height)); - apidashWidgetTest("Testing Request Editor in mobile end-to-end", + apidashWidgetTest( + "Testing Request Editor in mobile end-to-end", kCompactWindowWidth, (WidgetTester tester, helper) async { await tester.pumpUntilFound(find.byType(DashApp)); await Future.delayed(const Duration(seconds: 1)); diff --git a/integration_test/runner.dart b/integration_test/runner.dart index a2b88c0d..abd43956 100644 --- a/integration_test/runner.dart +++ b/integration_test/runner.dart @@ -1,5 +1,4 @@ -import 'dart:io'; - +import 'package:apidash/consts.dart'; import 'desktop/env_manager_test.dart'; import 'desktop/his_request_test.dart'; import 'desktop/req_editor_test.dart'; @@ -8,11 +7,14 @@ import 'mobile/his_request_test.dart'; import 'mobile/req_editor_test.dart'; Future main() async { - if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { + if (kIsMobile) { + await runMobileReqIntegrationTest(); + await runMobileEnvIntegrationTest(); + await runMobileHisIntegrationTest(); + } else if (kIsMobile || kIsDesktop) { await runDesktopReqIntegrationTest(); await runDesktopEnvIntegrationTest(); await runDesktopHisIntegrationTest(); - } else if (Platform.isAndroid || Platform.isIOS) { await runMobileReqIntegrationTest(); await runMobileEnvIntegrationTest(); await runMobileHisIntegrationTest(); diff --git a/integration_test/test_helper.dart b/integration_test/test_helper.dart index a1ce860d..70cafa9d 100644 --- a/integration_test/test_helper.dart +++ b/integration_test/test_helper.dart @@ -113,11 +113,14 @@ class ApidashTestHelper { @isTest void apidashWidgetTest( String description, + double? width, Future Function(WidgetTester, ApidashTestHelper) test, ) { testWidgets( description, (widgetTester) async { + await ApidashTestHelper.initialize( + size: width != null ? Size(width, kMinWindowSize.height) : null); await ApidashTestHelper.loadApp(widgetTester); await test(widgetTester, ApidashTestHelper(widgetTester)); }, From 7e1e7b9b1b6d232c43c7324a716b16605efb6157 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Mon, 26 Aug 2024 13:45:48 +0530 Subject: [PATCH 57/71] doc: add gsoc images --- doc/gsoc/2024/images/env_manager.png | Bin 0 -> 118815 bytes doc/gsoc/2024/images/his_model_schema.png | Bin 0 -> 92252 bytes doc/gsoc/2024/images/his_of_requests.png | Bin 0 -> 91395 bytes doc/gsoc/2024/images/responsive.png | Bin 0 -> 45028 bytes doc/gsoc/2024/ragul_raj_m.md | 24 +++++++++++++++++++--- 5 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 doc/gsoc/2024/images/env_manager.png create mode 100644 doc/gsoc/2024/images/his_model_schema.png create mode 100644 doc/gsoc/2024/images/his_of_requests.png create mode 100644 doc/gsoc/2024/images/responsive.png diff --git a/doc/gsoc/2024/images/env_manager.png b/doc/gsoc/2024/images/env_manager.png new file mode 100644 index 0000000000000000000000000000000000000000..bdd0564a1402e9d64c570dbb976b9ebb28cd7003 GIT binary patch literal 118815 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 XkXx76tC0O93rMG|&mAv2>WTedGV5+$ literal 0 HcmV?d00001 diff --git a/doc/gsoc/2024/images/his_model_schema.png b/doc/gsoc/2024/images/his_model_schema.png new file mode 100644 index 0000000000000000000000000000000000000000..0f6ea6401ae0e36b71af5dc65a906af60f267f36 GIT binary patch literal 92252 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/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≦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 literal 0 HcmV?d00001 diff --git a/doc/gsoc/2024/images/responsive.png b/doc/gsoc/2024/images/responsive.png new file mode 100644 index 0000000000000000000000000000000000000000..db4afb926710de63fb0ae83c7f8200c937d26d7d GIT binary patch literal 45028 zcmb@tRa9KTvo{JsgIj>$A=p3|9D+jv!QCAKgIjPXKoTUuWv~Fjf;)o_k`QFj;64QR zfx-O_|MQ)@*7tJmdAJXI_1b&&uI{d`s;>G~b+o3s0wEp^9vT`Np^~DUHX7O!02&&G zFD^Ff%Y`M38`YqDYAd`!s~Vx(LA_ww$*Resq1DFY-&$dz-k-WF8hN6j5&eFA(EHp< zY|zlE{gvcob^Xi_mT)ZT{>)*oXbeNvm+>LB0AcKi9idZ(H^h>k%-?Kdk|{ppk-!&snDafHiaAh^=j=m75Pvnkj#KT%mY<#2c^Wji}J*TOySu+=s<2W>B# znj$0{8%r>?8O@R?E``Nf8qM<8oIrFpJ`7I!xs4F}Nur7K6o zpFHmq_gAscn=s>FMCtQ7PUn?E{EgogP3OVXug~gPV>tHZa;E6S7{DQr5Bn-9-k+xU zK&8bARqF0|{XaQH_S+L17BK4_8lP&TAV-i*|U)i1#aS$hq7HyqtEkO(6xbnPdJl z&%xG%PZjK>B!&|;{J8k~+Eaw;FZ39GZJuy)mM4uIZrZrzo<@MICM=~YhSrBC*P-8X zwy!Adhn>P#4HYPsN%u@0KOCs^#sS_gDD|b@F~yi1^&Ien&xNXj2S!h5H_j>h?jg4H z(KFkCO*JE4Hx1GXxl*d^H)Rrn8BGxsH*#)BtztHG;8QGcMteqPJJ_Laky3w}W?A10 zXqs}+)5x>qE?$}ZV^|YUD`d1gnfRYwzJSrMRJaw}#mhLCKechq;YBsUd>}{1jbzxF zU^+2deMJjEZs0AmVyy;08>6tAo^8CrQy~5(RMp-;_$`uYR_;zX##$0JZLwqe`jLiu z=bILr(>u*K{E8?h6Hd7<2*!ql);v`-0?8N1)c%?@6-GE=GNwsrBC%${k4BdyOo8wc7wz+&g1|h zAS)fsGZ>6(^7&M@;jkcJ?5H>~DI``oY^BG-0@?a&+68~4Nx$`-B+BlTCC8~OME@!Z zR$@nuj%o*l&Xs^vHoJbPIjG#w7<~8QZ{#^>q#s%s01l37n5QX*nLkSw88BB+ff2S< zC?&iPCl_!-mL}n$g>UG89B=NJ4AXGWdgm^sO6Q$l@iOK$gnXu>1g=8+r*on!U& zCWomJ4$9)70s_Q5Kj3#Wx^*yb6C~=iIsH0PE)Z))VkKYs~9&kvQ*k>`GBB2sw(o ztF!8X^u(L^czBbO>TF%S(tEuhe}}5Q_c!;jBqPvGpt0C6yO=|?DqPH-5b+B^1cB6^ZMAD@2_{|j3OPjv&j2=jCF8M6r=NA@)C|lwvS+P z=`UH<%|zI^!AIDKUn=sktEy(HD0P3zQE(?SwZ-8mUR3=%p{->O1euqWRp_30pM@Y) zG!2bNrWf>4%ab?VQ(})hX!aV)xNedEH{PLnD8Rnvoah?5CYJMhI=M8rQl;-36Vvya zgS-l+PMNb33UG?zD1&5S8(5WCg_)!_oFGusC4ccIa>h>}8g%Ya&mAb~(~|MNB@tJ9 zXkc#rAn&(2C&!o$d0x6lXr}3mYNCF%Irmp8?xbbt&1I>Z^v;^v*RLv*km#r=j+3%7 z&LS^RX@zO6ss8)jBH0KWl_u9E-4zl7!`mPal6+(sL#n0B`y>F4jxEyiNoxCeYPqi0 z`zQF~&A2LO)9J~UJj#Y2^8=LIb&Xr!C>x}}-qE@+JH;C-xMW8~MJ)LE&}zAD|=?7hfHIe!m< zc}r!jtMx0ZbpN)BV<|@NeJo318(6|V+MirE0&RD+au}@8w_0k~TTV_c<@>dti@%YP zKP-oW9B1~au2jup%?U|GghPkffO$gy9=)c2+hSJw)rxxN?QR+8&ce^?G7Z#^bEUqy zA==OYZ|y8b)ewS}8Ij|U1J+?E?<}@@f@g9lXuHb@13l&+x6-SoWN5@@Kq{cRvOHBP z_-03l1hoL+-G!!V$B6@BMgTSO>O+ z3yecq{;E?>(9m=K-y?6`I|h~{QLC*o>2S{2ht9axjcor*950BMD745h6Q1+KXhFg{ zw_7HQHFDWZ+!{+4L*bIGEF$aNMy*5mrbz`1sa|q6z7tFV{7v*X{$_od3eb7uQ1{`X z41x8d-=Re!@^W$%@e6JbV2P7c9gjcH40e(xJFa(CT6T(Q*V_<3ca!#yk%K%iBfI-| z>aUM&uGScNriun^hth!x3?E9qQS)gtTwc5!hX&qANSsMO_$$7PrKx1Fj_+H|>RZCu zqCE*ySPgv8T?YOxKvV{~=STujyY5|9US4F;8B#Y4StwG=a^{yUD=RzYsvqpQIb?5I zybkq%LwyHRL!qW%;Y=Z?Vm!@hV<*2hY!3R*vMBLO{7q6~8v0w!z^X=Mq#+menSU<`Q>-&tyFqKQC`{f<|uHiw>$pHGomYm>>wJmn?x z=P51iaOu}j!${dg;%di9AZMH8a^ z*-*v|5w{#ukAd8cbshBym3g>z{GACF;2(N)E*|tE`KjP7NIx$;lns{;Bt~6(;=!QIc`!KEINghGL%*@=! zZQia(bo?a6>TTqJXlkC0%+-0m!{?CGfRTacp>EIuo^3;fjeS} zY+OwOOwOh|_;-&zZjsMY3akr!iZ7V+Dlkyi%XQ~vVs&??~#YGDq?lr|_fFvmVh z7O80T|97K=1@IG&3A4-g!;0oV*-z2EvaHKZ4);e9fra?c#@jUA6+;zT{uahv5bvYZ zn3ls@<9|#0e=+?3=>l*Y7zYuagVFwdMTe@cehu5DrRh{Gsj52u`dM={C$l}Hy0S8L zuHJ4yOM(99qoU6hdu?z6czD35pm_IIeeNX>k48)kUQwV@^y%S}N~`xl8MtAjAYO{e zsajGQ4-b#!F)&nH9$L_G?#%15u-!a4xpl|^3T!xLq$)hY1`z%7l?{;g`F;|rB`2rk z?szmZcy)E)8E4pF6aC7xL2kOj=p{7ZJSAz>H3=akl_KAMq2R3+tD9obzq{+W18=G( zpcgZYY4e_VW?cL>Jmrr{8e2Gpz!~0c`^9KV5uRqk?M5tm%)n@dK$fgB{^X>;bYB4G zm)4I_9N|xRGyy{wKV3e1-koOD+vv@4J{R^XA)pzv5E4sS(oant$p53FJ371Fr~vzT ze|MU+O;Y%1t)PtiA2FZG%GWGnzQENbla0F2P)k(JAW|L@uG1B zI-3mhRbgxZJ%Q{14PyP*%BQG#JXyRE_DUhob7${L|NVfy2LIb9ElnsWNfQf>QJPcQDwXiBNh{oz4uOmRBP0jg zn?W-0JRv4VS#paFHRdNMiZ9FVP3ja*j*jgb({vmQbC&Lc0e?+)hpO)*u zE)R9kdZNO5eCT}vjM1b2g~y%uO!@71UpOoN=BUBdbeAt3-NoKok%hRFK%PhJLPTm5 z$Bj2v4^{h4hV76^ji%(RQv~1M4%sfjk#jcV&s!D~myS}PgSkRbx1~M~fc<+=Qv20( zR)G3?n_EY+sOMI{)XiamiZPGg%V&km?naISt-lFK6aOb@m}LH{HLZV?onl9mRefABSV z6{f5bk)puRB28MDj#gMxqp6l0kz~0{{7x4wJk`FIGc|1rM2k=pvf_gU8vdugv z+JsIPCS+joqnOIVqcoE&u}zq!scPhkA@~+BXOnCn`_>7>tQgjI*sgLu8%&(q#HSb? zNuNjk0h4o%q|oNBYp&M1w=6{sT^ZPVwn@9u$s_S|D~e;=m&&}tlNpD>tA>D4N_!O4 zR6i=A;@WX#Yt-S~0{roM4jp#2mHjf3&MSda`ar~v1^BRDS3#^@vZkk}2Ya}KR|dpI z{mOP<9oQu!9!CJ%<*v^LV~O>De)nn3Tt-%#ObX&HCi-`Ort07JRw(ycixpuB_(M2k zZ3F@pwKjxyh2fO=f0LIZR-;a_7Tx_3QWh|`9u_K{j;rYd4;B&K=^-ZzgOyj`Yp8iX zk4RCsxk7F{`W)p>Yuf=*Yp2Os2Y2aW-gUb#WX%dyfcM{n?!1u)%YJts`mQm)g@*pU zif-N4d*O&@t(oA#DubSFaDQ|IAG{-8WZd~cQr&snN*A=A(06eI`L(F*0}+u{deYBL!dB20Y+uUkklkqH+)u_q zw~)ZJYr&?e>4mbcCt)gMeT=~zIP^!J04|>7z{6loGik(JJ($kB=Z9lP?cAnG$r*ZU z{V@BV1K(M#RMJFZ$vE8Q#G{LJE>2D^o0|h)O$>V;=)U&UNg`g_caL3w#~PtXSsVNC z#`VdFxCQh+%_Qr*_Zyw4Hh0tBY9e_x^uD#~-$m=S6Qb;c{7=P@i~V0R`8 zEo?S~Eclp6&ftO!)As7b6kCpqX*YI$=IS@Sc*&20$zkFQ{Tu@`$?{qBS9o@?MdrD; zb|$j?s_n3~?OHJMaLef-B;t0h)k(KHIxKmIaOS}*#C!jPqbATbwFgalUF!>|1jDIP zNa6$4WLFr5Co-8?i|#()PtopM^l|gR{fYo4j{&3W-!OXL#j0Yfj$4nUOxKn_X^xKK zM;|e!S|q47_gfSu&|9GG#Hx_!T#b|^pCvUc)pHzm>+^jhF@-rcJK%-0M97-$%gYf0 z_TP~VK{6r!v!vRlruizt#eLSuc28A%eloCPp_w>^2dwy zqubeThl_J!U|RQ+UvqZ3l>vhLXx$LM0Wu-N!1lKvqTyU0WqwbGcA;t&5@lNnq7TqcZzG1oju<{&m`8MNnr>CcJWV*O!VW)1Y4s_4R5hAtC zSDS2fJ}hEU3V3BUe~Ck1VbR#CoVp-ONLmyr9`G$zc;4{(LIZwjOPek70kDY(eF*H8 z7R;wl=W}^yFSaccng;;oRuE~E#ID^3vWS<+(@v-)kb+7lfBY|_=&8!)g4t=ytPS<5~#@Pfyt2XT?GPXys41%`3wjg_%|9V2ej zaWDC#U`38n=j?-wUSw2$cai?>5|h=Ue$}r?1%hvXCc5lYbE{N>t$V*|fj6adFI#{P zr;8_}r5{);{l_mx#1B~DK{sk6v87(#INhdx5r7rg?iLudwz zQr}F{N5v)>gBG;6?>a75RVKf`^(&4s;t8j$5YM8&*t0Iu$d&T=ttL_`gB3O~F)CFK z^I1H$Yt=u5T{B_1^>AEl`r0;6VEb-JT=lFax&~+0>=f^2URlRWQS8`=1zq9_ZFGV1 zC*8|CTbu*B{q44b@;v^^Q|*}YFVfr&>5TRVB09m;D|poS&85DF4wX6@=WZ?`Yr`UB zd%>rF6+lNTFIwOE{3<|HLpOKj^4>p58N9z`e_nro{WymV9_k*;%Dw6~^NqF3@b0yc z;Gi}sN9iiWVTM2;zzAF`s~Ui4w2pLG%}XQ=O%9b{#2&vlUT8K zKeD}6YwJ!%+Sn>X2a;HkNR@}w2Z@Xz=yT$L-%I$-%TgW(jYW0&{4e21pi_)tG5TNRZLh-lt07y zIDA&ZG+g5)ucHF;7Q_+lMKF?2D?eNn7`(lb7m`V5B~B*Ek%w&vmJaAMUk>NayEwJp z!t~b7<4dqvND*B~j~1tfT+0P_e~Le@?ol^?6+=qJQ*9E>5;Ux^Xd>Rcs%1>(5R!d$U#jB}mb=^HjVjm4WSLwDRU#c>O@Yey_MB@8L`h+VnoN=i5rw-?d z|B2=wm!<_-N!?l~pW=_r20=yt?H_@lLaM^`Jtj=q$nToANf{R?u}9%gS^sse^-jI@k7@2=1P zvbw%c7!PZuSp6Xs@Fj|aO&*K;fke=6Ovb3)1`wzt_Ee&)+HRC#yv=~eZ1vH(#Qm92 zwcJPF3y`A#zu|c;n=q~&d~=$1e|HV5nDd-W{tfWuMuz_d4WLzVvqWVfYE2H z;1`#)+@wCPy1SG^rPfP@fZQA;2&pkKm8T4o#HZv=oAca+m5{#F)ri@nE_{u)RJRm^ zoO=4D8?^+Akr!srl|ms&4&g;YK_94hQAv$v$IU;5sk~YPR*?!$uH@2*;zy?sbUObM zw9<7S5WRq;1R5{qbr{x|hYsXjt#)CD#?-a8_VSSF4pvAMvssv!?9E&hr}t42Fa+)^ z1?W&b%h6xCyY!CJ!YBRQRoX2Me+M0IE~4Z%V#cT9P84#QE^+(912`=1FpR5|?~9qPzps(>6B;>XnyIwdB;{&|K%|zu7U@W`+h{B)OWk?Xp(>ehk^8 zvL{Jp@%jo;FjQhP_5YuZZbCQ@mhKT&1Y7mg%nj)BmyCe?lc6jM(>Nabp8rD0EKL}v z&)@nPc?4M@)8_}i%+#^}XG;ICX#a2DQkvZVu4138JmZB}+x=hm(V#kCLKB>8rX@<3B=e69cYBbJ}l^W~Z4dZBy-i ztXF2z&IxkfV)(P`Gf;Pdl&LBxoDy0iJDam{ZKEw1T~6icrYNB#phw z-{O21dD<*)Bp(EGh=Qg))OImClQ<)D!!}cKI>>27%y_Phe`l`#rxzD+`aeHW+Uxv< z9g=st2#TfI8KLi%CwqbJcv=drI9*Mg$4m0Ki{X)JPOK4VCuR zf2VCReuk&OV9MSi8mJ_K_fu3v_~)t1t9j#wmMTw9#nQ4eZLOSEZd5KH>V2dzr3?4G z>5M2j;mS2Nv$%xc7u4r@;>XUF^jS%O5ln#4GI3dn3KgKAXY6s9NrtiM{9*ZpJ$~N+ zZAZsKC#}Ssm#B?7|0GQ1QazacRNK*MZ_?TeMZe_k$A3E7g{OS^B6enm_W1k#*k=BL zcg9=Un2|@!_FePxxfuDQ!jR7WPkMtqsD4M3g?)0~ehmXjHlCcPpe9;pVc6!D1RYZ6 z5U7^t_YcJL$uJ_W`JaWK8O4Akal|E9;%t2lb%LSdaOy%$V_fBX|1~&hyf7Mhy;lv> zGcfO!L8;9=eFTML+EY#2(VQX`xioGQ*nfXj22kgHqicb9A(qWHJh?Tmta{(HAny&< z!@yFH`!V?!xyfTV`!TY(q>wRB+rR?sC!-P!eXvYzZ52yZZOLd#=B%#Nn=p103+)aP zJ#5xFEzWGR|I56C0@b~C#^u2cOUM`y@^3@D&2D~a-LJuF5x;NFg3d4JoYyy#z~G-w z(L+X3CQ{kwKXUM!{*4iW*8XMn&v=sbq<`_P?I9>A^(7hrgbrOsGriaB^|! z`OC}8=ijcX0Clcjrx@rBCc6a#JSl--!gqv;zkL=|c}O#w3UzY=FPwxVotuV16MVnV zk|2R=Cj&@un?;h^w1tW*bj96&Cjzlv&uBYvBpbXwW+sv>CHd7`Eivoi1TsH*ra+Js z;hV$s8SAh0-3IEINdl?Z?5EeZeiAb-gAbsb_@eyr&|F&7DV>m%L?)?_vh;{@ap_ki zbVuOqC2dL`b(z&o*EAO9<;fsVPKxqs#&QErx<7d&<9S1GZ`efDK+Xoe*|0cx7jFdp5B;f1Hv;!-n<_d(B-jspJJQRZq|Z++sA&A9b*`+r*-%~bpvhyumYoHQ1N+Id zSJJ0og_70}hIQkJ3|Y6{F9nf`9aQqPgUW+bUNhW%{l*wKB@(7yd-Wb8gc`74pmMKh z#FcNS{>EtgNh*-E;$=NOU5*=f_# z8Qit;-K8%+?O()A*;P>heEe#4kjG~2n0Bx@OW6EZKBOB&bLxZ1Gbooj+?fnv5Z4m! zZ(-V)c%Iu2(D_O>4aj{U(c$wU;YXp4< zzTD(CDur#mGX0@)+g-~czLBZ0{=+~j^$!JVK7)_&f=J_u|qph$LiUGZd$a;(-t1hz^yhx54d) zzlo*CRFbXeH<4%%>rS9EZSqf5{WCs7>uD+o-CpQ9;_9{KB_}+4Uy;@Be zEWeM+c@nh$@kr)(ambLCvRrEvPP>(OIMUZogI%0-swjUQq$~w}>=y0K5!nZtShUD% z2OJfI?iEeuY;tq!O-L;z^DtT+orf2NAPjRETsj-P#?;Ae9_U5%t(Dy!N<}4=Tg!Ix zHwM7Td3LqtkP20`tdnsroyqqM|^hNY8`i%>Ub3HZiEKL3CX@Km?v8Crb0oJG~f?&8v_^$ zAJ_+bqRBUks4ON~2i#Q+;{t>QdqX?+M?DbG3m9dV%ZpKovv2#I5BJD{KF0EcFGAC{ zaQAQadB?^l0@R&<-;G0i7#rwxTYrU)HoQxLE2%VY&JH6;y)~(-yPay|HlM^n=)|?0 z(tRZVp6>}S4`qRT=O<(sowmI{mJ+k&dP)L{^zIp5<6R}ED5P@FTIfO%)yd;3 z7M+FQ;;lP-@3{Hj1rIl-=WQ#(SMWaR{u*>Bs59540vu)PGxN1>Yp$t;$?7I{CNXx# z<<8RPq!=Dh&^RR+1BJ}VOf!sYEN7f1?+1@mdLZx_cUYMf5YW`qqbI3Z452DSt=sDU z59JO4N;YgNnU?wH1^`^m7#;Iq&c=2}8kQi42#e}g=olZ|p_PehSXFZIyK2@-08 z>B!S?cv4L-;`eNJQ%ZMV$4On=&v{L#P5HRCTv0aO{!n1hpWwBxAkAJ$AG-hUyL`nT zA1Mm2{Pn696lXM#-zvg}6g(8bjNaWRt2;kSk{sNf^G<)Vsw?+W6?%9Ijn z`VIq27{aOA-@PUX&=@0`?~SGXSu9{TtgB<43%$^q@P&sOs1eo&*DuOcqtWPN@^r6t zg}lGT36IbnGkp2l0l~+t6~fLvm1!-~d0`zd3K`&A^NoYAvF9G4M@;%}m?n%jwS}_7 zW;{E!Mgzl^`VL;?-aF(gH~)FvfB2q8+^@pP z%4(C@|7@){azoO6u6ko0b6ccpVZKh&Zgg}cukxb{!x>~f}9R{VD}e!h)&*cmT!6#FpV!?wnf;I4!b z>%nPXA@Bl*C@eDe+((t+=ud3rgaS{6RNCPJIse{F8lhHRx54qy!pg-79yCdYYhT&8 zI5lvDY!h~$C_m|r)j;t?X|g<=zPK3y7%El5hyl(tMBbUshj0c zZC8Oc^OqS?aQJfZUVH-K%=1hylZH9)QWFi=%d>-i*=E&njhUUCSnVIQ`R9kS_OZE$ zCX4YRrc-K(yVD7e6?B|3f0LpPdP=Y}n`-(P%Re$KAa{|rDcR`3{&4iQxI^hQU0_Ry znqe@@X~1!d7%IRsO+eP4`KFxV+z?;49&R{+u9GI$Fgb)IC?7_|?^%hZ{4BOFPF70m z4euwDX91BXI|t zh}3y8$`^PeStZqf8a~^JIR7%pLj5%%?m2Ck>jQ6hSb3IPYfI*rslJ2O?VYH)r4rur zKLmL$u3e>1?T>B~Ys8IKZ3E0VhcXI^wNNYiWK_(v^T}bjHVb}3u5qiNW|A+|r=H^C z0aQens5!`4LnObr);i}2y{TI~>I^MGY=>38bK#D3X1JGWA`ZEhS9d@_zExRc>uCX^s(Uj{*`fmp7X(@k1;MufOx@3 zeKAyAbnnP)eaA^7kxK(^;o7oNQ}}^7RP2L=-WMKh0-DYd-m_pC_WSqZW~(Oc|ETcoO*We4@u`I7JDD)+#>s-rlWjF|~7y{{ue8-OYD$3x;jAS^^6O`%N|N6Z4h~ z5er_wUr&FQo)u3pz{A^%ePY{>*JzPs!MNkXb%B*r(H{?B)4qq_jfq`yHk2f@Q-x2=DCor?*e@m$9WeF z72q6m&7-|sx0ee0wV#d8k&*BLp@;ii81dnZ%Dvz1a%~8{2T2Fk733#8Rqt8(6!`s8 z@Bm+QTToT#I-&o-0^_3hC-dw`ptO0b4?uU&*-!r|w#q8yYROpFq9+1c_CsKd#LD?e z>wSdggWHUGmLL;3#v7j7rO#w^@NjI8+~5nX*!S*c!`uSy63vZQt6zDvd{+c<*7Ofs zE*PH4&GF)1G+T(!*EmApnLJ0oAxGc9E29KWN7yq{neb@R^ZH%^wQw-hIAH%H(;6j@ z=2HfYLkh9&yok(OU~C%7)6i7uv^+wkYc(x+?K^ligER6{`zFmPrKT+CqJla?e}X!i z2tdUWfZRb@yhbx#O}JD7G%641kb-|+-jbo^vLbdx3GhlhU? zj~kH+q7t&7?rLHR6>$mMuNe^YI81DVeU!StB%zfWSp8>amm3UeWo<4xil&AShexJ= zUP_|-)Qd+f_nSQgj16Di;hkFE}f~o;ql!Jzf zt3)tTaF6JSyb$$#UO1uba&wWgc=F*g8fsQTDTLztE;t^|Q6t2?p$8}AujhG{Hb92~fxUNTc z23-fczy5#5LLPC;{~IJ+S4~Koxh(2IQA|OvYJw7B0kEQZL|GmIp3nac@ZjO&Pd2fW zqtKh1w^?5yGawX7WEK-s*joH<^s)XR@XddJl{afu9L^NdHW*z-l}Vv6A_%1maNZo; zGLgJ`mxrQLR92@9e#C(SWlX;QH~lKFPzH69*mzGlKHGSlY0%hzP_`^WloRG2Twzz8gQ_IiykioTLSa7~ME`}tc?Z9W z`DdE&g!K>W_|jLTew8unv(#s;j{uZ0h6`X`_Yrri|C}s~ce=f77I3l4x&hkIHa6yL zY-|j8gx}=DAmlC_^V$ui_9pew^|E*0uV>fa4N^7C~=s=dHp zpC^xf=h)m1+pnnKXHh$tz%{EGQoklbyuI(EOov3$uCCZX<_%vY@`YL2UAx1tJbp$0 z+*4~*JIi$Rw(#}#Zd~8mDo-5vjP*GQoZIKd&cd8OLg?Zb95Fe4<-$9^R%OtI~Zz%lFQ5Nh^?h(66e0#$&m+tD>xUMk|2R2orgw5f6DpNhP7V3>F3`9XwKES7Kz zr@&|u*9#xp*)KxBbd;3z)gH1pZ$&G5>dk%>Uxe-*aoYZ|&dY1pKH$;Vcc-&}nJ=StmC8q5#jhjhO88?a(va%f%-aE`o ztv;y?fBuXY*bzfn7%rAiLixS6c?U*EFCcnMpFe+g%-kBzYB#b)a`a86+)M-w3=SrT z2Zb`AD*Z#APj9KI`Rii>int75$c>`9Us><7%wk#<)q*>8rze%w%+{>0O39Cx+vS&O zq0K6Zr@Mdk0#dyu@S~!{7^^cb=HX(crKMVot6Wgx6Z`F(^FwYFqLnDM+)znfi22F{ zVoC`#&kva;{A{+-mNB?q6d#fz&js@$WVRShs;b*)ZUptI&7?{~=4HTSq~7YdhNyC| zQsS$@wCaSU)%VZ&?ek6%aE%#zQEY%QXvz^)JDO8*8q)Pl2n)HgXE87-s&cBGzj=mO zG7D*0u=N;PHrXLVNg+Oz>uWpl?PEG=SuzlI^v}s(R%I|SSDab#gX_xvc+uc`8)Ha{ zo}Li$NL*)Q{Knkw;~VZeXVB-x& zOH0j(&c(Viawu+VHhD~0;3M2%C&myHFj?bLL3Jfv{jvrg7Z5CO~k#OvBpTaKS_R)&O0n~2}C%4`LJ${qZ ziO3uv=6pNSsNfI!{~?TlLzEDPaa|6Xm8YjET=6NHnsQpd?vtKml{-K@dJyAv%909l zlP!dd!m861rCKg^PGYYVyK$F4f2AB)eCBlNE(|NFgE{)I6lt8&p|oA!(2(i(7b1O^ zx|X)yXY`bXukm<-(k$z>;KMEy&gTRk>$*`)!C0C4@tJA3%U4-D_`Nukmly zb^=pTew}vN-tnLD7VCd?dUeCmB4ww$ebT$yiIu{1rJi$$mOXV?=~@_~xaF?lPuhIY zSMEu#zxkgXl~V{qZjTiJPi36{A%GpmQ=&l%uPAAER@T3T;}F**I<$csNdhy-jghxN&K}?7paj6VdLr z*cdEbWJ)}kpA*R}d4J_Yf_2U(Jm-F!wka6AmU5serZd|-XV>;B(#YYLA5}`;pjN#6 z7a2RsMI&yjns2a)s)J1Ij=&yFc=2|915AtI-WErAwl$hny(K5S;4Cm_iSZ!$bbG`9 zWVmTP;Vn@jqT^1u*1~GdA#;I827?Ne+i$ilUs_RO{};i%6gY$%2w%G+qe~_W%Jtsl z;~GR=iK!6v1l6l$nvWv{t+h#JD=PYzGy;gt@43F8U|_x6_jvk*Se#cT!3u}t<4ihQ zQRBD^ExLi>$D}@CS55Wtnp`Y4!od1#y?av=u~yT^yOIv`FX2P@NP@72E_{5vCLkxF5#V-y1t2UwQzjS*L*A&lS4*6 zMr|0U@)|oFgB;EB8$NKjK_4ZLBpg4su-u;m7BCVwDSWxdTlPPFSqgfBncd?um8w zA;(={+IGvL<>4V8*mum+iY8!NE9u^2IIly}Yvg7PP(3bc;Sbt-FwA$R4NF+kc^~j) z`O;v5Prjwl0%5;2G}m?NJR2+agjDd<#|@uMigh)aZAAwlC7NlTF>MM|^5yTV{Hvv> zcZImyJRih-(QISGNJZwp;(p#0h#@O4h7zU>Gzt3lnIm74AmWU{H=EGe3{+)6*?$?7 z6{&q$Xn{8ZM0=W(-pfEJ*_)1;p#t!!!&&_klU8dw%c_iGWm8np2*}8e?Q%*|F1&MY|WRZX&G@#M(k!`ZdN=mh-oC8aP zlGVZUuQM0F@uBj!%tBa!mfYzK;L*_Zg5r;|RhwZBxL%pGubq0kx;~vn?wA&EEWBfS z|AZ6Mn7Zx#>ES<|l=6!1k(ZGyzEgJPwNAv-EsMMXr}k$9V+7Z~S;+}hzu^(017BFt z`xKBu;y&ZwG0=MYlJ?>aWi6cUE!Q$s@c20tvxeV*dU;dQY%hLa_7lsIa$xf4eTlwi z_}w*MbK!h6Zq?*!meoEZE-vvB%cX%!j74$hZ$aDSOjV+*nTLR8cfk=~agU6pWxR&0 zrd97}rVQCi>Km(0q9zgR7RmH!8M7Ots=FD4V*%T_FJ-}(IvDvKzA`sT!Dnq!cQsJ| z8SAQU=lVBF=w+z|bx}&MUs_tY&=D%b~tkq|`LCsb1?|$E}$9S&*aWSml)DW7+ z@wxvbk|JF=Lj&84H8-5=IcP-}LxSh|nSTQV5REu#~(a zvuX_JoE>8FXkY$vq3cfj9UZ$qE!Cg|B_$}od*tL?CBFUaKEp0=MHG-OJaRKCTl;hK zIpD072+5(daV-j9&qbO*OW&O>pR)y@H3mtxy(-U59bfd+MvH9@6Sw6(#E_)wmSMh> zAjcH)LEr`m5iNt(;UhIkjHs{zkTR5)7a{cb3&0ROuGNfIy zM^ElwROck-|12|g>wFtdkuK$#R>4u8a&mtb1N^aRiRNUO@TSst_&E-YD667ii{lx2 zA*S06Oa3Bq;`BMErJjY_au<38l-y7d!>C#vYU2kmo|VtGkL z(z7Biy@mZGB7E)t)VTzlC0RJv=Kx>8vaQdTl~F#Y{l8!GWoqyKtB>XESogSQld<}x zC@leu?whhezLD;RQIdy1{C_-#bW;Zob+Sa$^ISBX^JeN=CE`XogME{V0)Ylb41 z>x)C69b?DWoB|Hs*@QK%C=VS}L^+fruF?3%KG|&Ihb$h-xi3la+GFdKWV#lyBx{^z z)RHvRZ7x3^|wJ5hW4V1TgG;o3_-zj?)Hja$S%AWMi;%TCbcTD8<% zr(oXFfE5eS{A@1w+TU}J5A&y?D_P#+(2NL6=z^6PDz56kMx`u)SZBylf%C-f;J{s0 zIhUFG!gABVOz?9cRn;nELrb29wV{*& z!xD{A8%1LvgEAAKOCi)-dac`-*$A>j;ddC7J(wnd5qjv=R&Ar1`YW`XYS26mBcey# zW_s{)MkfGJJgym;8y7{V)N~x%gd0l#GWl9JduOBW=$2Nrt95SXNm5H(t=gZ8z6B7D zu1!JTWNmHF?9@Uof>2+1eIxLHyEW?ir^1R z_D*gD&>z{v@AOzZ+ClQ_562?vyGpz2S18?aCV_X*sbh%++fS1;jHlenDl1ddDh5fC zH`?zhjs(0nGL0H%7N*d|nba$n)YP}MyQdBUHt;qK@#0zty-sHZdw`!x?Kcm`=~Obe z^O(D*bU$Jpm@QN5>;1|~%lDhsFw8&-Mn+gfWLN2Xn)+y-Vqp*lWnjQQ+QLXFbng|_ zSmmBUOmhvL4!rUcke-<6ci&A0rZOSn55=`PDrtVEkj!-d zVP@n%%9{KqJ|iVoOnH#DCBVuq#bBz+yVMjhWGYL_JsnDI!!(L>1$X1Wf*yZZ?j3}gGczZjCFxD&Rsx8x-(?EBjWIse%xPar~ojK z3xZdMb05}RWH9P_{1c5vPj(xk(Wg-18EPDmPmA|k)(OcKH4Q+YDx`~6QmBP;K=bRy_nNUUVlITlP z2h%BFF@RT-yf*Cc&QB%@ijsXm9~G|woP*qp|IZ=+;{g2qF8PYVR*gD6!r|S+5Za;a zW#T9V?Fafh3)}UT@RkZ{Wsr%7FNX|I%7ruxm5-Y#*H!ugOEn<-^>2PHk}`STDqwiQ z=#JlP1QomB=s6x9nrJod8qK6o7f>?col3AhH3Lfr>?G)wJ9fKAQwP`(k3iN1*V1L0 z&%~YKTVEF_ng<}5V3mlhcGa0Xn)j@^U!{oYep8+8gxbbXWA>7iyk?Kn>bXD$caDzf z*#_=0vlowZB`sKnroT+9*okcHxv1ekqVyd6T~_y~Cs2|Ue(ep$-73ARU-EM4;9R{> z+1jsefBnbfYUl~;Vem^yT7x^c#%oOfeA)+jw|_o8_)2ub)$uIH%fZKFet8jplQANX zR;sfHNK&WJK#VZM_gj|%ANPBS%^HY0(zCrP$A3Dr9w_4sES-n^HIqN`N=5Uor@V7Z zs<9E`%Sy#d4QMD+(7`3JO1WB(U`=^?q~dWk`(7W25ghwXl;$%#_!H_p)Z$C^#zA}= zz18?P_u9minh3M0ZYM$L&FGa#&~BvLJSA4JTe5+pa^mcZD%#98oY;7JmaNR*Hi2oy zE0tB2k86dO`cK?z=11rFPMd#*IPS?q&oXSeO3?POV9yLY>q;pzX5=G$N11z*x7`C9 zm%%!i94gpQtjMZyuXm~F>sO;unfe8WVB6n{4PnQAu&+M=VRJHSw7+1elR&qUqvK;Z3Da7}ch*^{7NNe4OzFFPGdJr#M+SRs=1uGrtdWB;- zh?>g%$F3vSTaQ{`j7S#AeAD)Gs35EKI{0*Bw7&ZgS+=uZQFB_MeY_!XjIgaS5^z|G z12Swt{L9xikDmp3w=RveXOWW!&u4qX@44jdu^D6X$qvB}ciOZ%&VN$@zN&!poiFbv zn?z6MDdGd3+eR6T)|B$Nldt%C$*Y>m6T!1y!(wm^%IFupd-ozTW z+xPxFf4Z5l(_V&kohSiRMO(=~Z(cvVhD&|8g|FTCardqzUmx+r%(@%1L0E9FDZ?|O zxPrwN!{3AiugOgv zd`$b-xnH%Q>nquidZ*k zpX_h&0P2x(nS$dp0@viG&v^51$Bx5aJO?=pkk@^QEO&u0K<8H=5adoNQM9i!DDt*m zjJh2|Rbj)1+W3Ur!i$IgQX+0iO@W-Ls3DO`68o??&QC-j2ka@fOE=ij%N)=6$~zv|ws z^ZH=jbr1Pdwvl1q3wm_w|9Ru9q27LJNx1ej^BE@ zo(0l$B>A#X!8~t8I!fbF-7CuN0t_&o7AWInjjeL7`)kw%f3Q9UbmX31rK%T8)pR6JBYwKrgMAJ-To)j);90vU^6D#4cvM zO8;w&X|7nKOYgh=uEtRzquw(@%$~@tq*r@XTjm3vEvhkqQ_HJdW!6`s>vHes&T~}o z-OzklX)*3)<8GH$HSUE{`XJ}&;^XP3Z|z*8`?jaN$P$Svzi;#ISZI_`Tn z`s6LHbY1EFqTqX=;g;8I6ajAv;az@l99|~;oL1tIOz`Q%0Q+{(D!&yjasSL;Tv>_;rWR%PdS>e@R)USFs{q@ zX>fwI#LvEyU%lUzeX-p6ftL)YlESe!dC=5iHI;}|K;4M@xo`NmfcZ}#8E$MncjX;2 zL!X&?N%M=cSVxyMIC#L&w$IH3n|)}^Q!=w@aj-@XjjmBD2 zBH0NXpE*&^-KQzuy2%(9k<{=e9LV-r5IP6& zdumCvoEr>~GjT0N7YbzucH17bjNg!wWm(CF0Tcx~50{xAAS^RGV^f7Beg81~4e6_G zT>o^CqTl8!_AVRlY+B=uDIMXJcds8<2$Th>r!?56n5pV z=E^!x(lhHVDd!RGUnqK{cMV$U?0X6paCag|oCj-i>G#ZHx|8>Wg*7L`Pg>WEtSj5w zLK8gNho{}(+#ZmGCqOO8a8#+_5+n4c^*Ly}E{;8X@{=1eb$$rL3Aao^1>^3O;eRoa zvzFw%1QuzJYpz6K^puz20{jsY8clOy{twRQ{F6oX|0u$Sr!6ZUB6gt2NWGv<6Ooc! zzjx>ZHih7iA2_6*&KypKc(NR>G}}B|s4zw2{9PpWaC(!u!NEwFD}4$-L+L-HGtLt?CJW{Ph%rFL0XV;$dmHVvZ}jTzY4mcu|9eZU3)yd# zIDp?_WP3y9U$;4PioA_uEe@5v>O}t^L>d1Ubp6i}2oQPz{vo&W4))qTE%KOw&LBKF z!cgb$i1M?w+%{ZM2B9s9>na;~Y+%#J9Si$FVg;!#1CiJN{!$2i;J+$6IWZ{?Fn2V9n*oL`%i|f$`O$rXs3UmsfIMVOfNY7r7U0xMu0tp$ z!Oz|(VBw?t?E221sW3ZhN+0#$kRmN0tP0phrPSlRlug+}e-{YZWL08SIaaW?m^N_qzT?zwnPLnbf3<4R*lO1{tsw!lR!UIgPbbB!O#m1CiAR`NMe00bJgq!cSbw z9{-KWe+IYa9|<=?@YWimrZpXuF6#U`;(~4K2f#f^0(`aJ{&BVm&7@3;zxZ)W{^?KX z$a=i8)BhbuN634+ayED;J5=7v+Bz3}TGAssGQ+p>$<5uJC_k-GEYm2XfddL4X$v{N z)hHumm~wa&bhoQn(pv-IhvYU=9)2PQ-4bJ~mNNQVvLk;Ugj9Cw?2P*RN_q3<>}w|^ zE7f~NUqD$|Swd%W4|@j9ys4gtXE5x?4Pvp_$@@V=FT=);Y*dV-0= z0Xr$rQ^FISTG=v(t0_TEwH|%#lPBF8N+VQ4Ekg}ozgmsSBIV79S3geLhR09lhPF%+5Ag9u zpb=^V5c-}*Ovy%61T%GunkYh@mmaJp)u+>#cCY`I`$v_J3ayMw_k2ObgmrAkmdOV zD42s`-#Jd%3Z_fjH-ue-*JKpIy?@>q1fk+Nu9rGm0K);KUQ^hc7p8@k!%>pJ^j9Xl zfN~zm62dF{!1Mg$DhN+f?;goLy=Qzr#52CT5f>_YXTRJkye@CR`T<8_PUNI0X^JXU zxRk1OhIx`H5t*4KaOV#C#X4PC+5{+(V2vC?Z@pi)VkS=B^ z%Akb(I2VZY^xFwpD|g2LT$K7QO15>@1kRA9&Tre&=uucp}n2^f?FySQPgx|b!^#otX!<`muR4Bo-Q6VjTi&;N|OW>S4 zN%7EZaQRF6Swa^HIMl~ZxXU8%k)d$9_}K4o)&&8C(X+m598=a$%DygpZ;gV9=A58Y ziP<1X_zGZk`$TdhjKf^?j**Qsg8-_h!d>Q@Q(>9%8{7Kc`kp9HQzMg7KU&d_-ETA` zAj99*8ZjuOOJ>HZs)_6Mg_Zt-#MEr>4s(u&XrAFbMN4|6eLI=e*guBnOLFov@I2FF-z|3~smTPAV(1kFC6ZDw#n~r{ z>h@oDnDu4FFZMctZHkNSEpT$N7k)!=qclWhx9ktQ1^=KU>nHl>y+cjI3$=7GCE^Kw z!aw0@%|i>ioAeKDL};oJ9!r31!PReTn1g3T06=+YlqW%!%v~US;ef!11gHQTSpE)l zcsSON4AC1{aAi4!_%u;*^=BdDrA6IX5)?-1xNt*8jr@)&@jy5rxhsBs93h(aUaX^V z_QIP|DYxN;TcKuSwNz~0KmC&wmjUab2M%XWdB{~-jDel!UD5Qgzo==sb=1I-%}%fZ z?GzQ4jfi!fWN-ma4U)&9?e(uG#ybmj6W7hUm=JNWRl^^ zth9RI-ko!y=uyS=Ll^%x|jqF#=p zi5CA5*)353rleL>WvYpN#Bg~BTgUZhnwKI}zYxTbJD_785|qSI3Wx~|6oo-vOOfVw zvJ`^Ps`Xwn@@rq#5y%Vva^Q1<_EPP*c zvHsl#wJXj3q{894gRmLP!$2|_XUZizpO7nEnrbSwuj}NFW;ud8qa^nx;?dX}iP?YB z;3`Njvwp5ducx91FH#v>fXqE2n-th0^k9p{Vy4gt>_{3c2IikcL4~sujFR$q8W128(K{{^#>Xq{;deoBBuOl#ihNuOpezZLKYw@=j8eE zQegfc3kFS%Ah;>de86UIL{mx@@OX`!lok#fY&)K|Tz&_)`dnSH#5lrre%kky2U7st zKuw+8)2m-sy=j-)ckxqM@RZU>hjnTI)*SQd0!*gX(HuK3e@mG$5PDf8%h3GCOTeRu ziDW!IqLVW+?6%_U4&i&zyT2i?=Bpy6e%4V%BaM~IS|ony-E5sp4H(s+K6zdrdFN5* z`1l3BowdTBq5JE%@$-=o01{vT&^wFrIHFRfn-?Nd_Ss|pBtLIFeFwMsw9kGiY%p_w zg&8{vJjew^2$mVYq7h5p!K;}9IVTM4&}5U25t!ENRRG1+TtweC z8kPfwl;llFI$@V(&l7$h-v*w@dC;Tr_vnKdN_qps53RtA=HfV#^FRnvbN#@Zc2uqP zR192!a{^aeUmpc`Q6(_jgsIHS_>RX5@NJtu33=0&+w$H0XDtW2SY9}`r%=psuEn?h zu)i_okOsjOdURW6Y$jrufPvRg&M)bHc8Cn9tlqZ#8vF?gJbTN}04!iZz{gP~!X$DA z-VeCJ#DMrQ-GB6f=}htc*@$oCOE9wt$~tmk9AjB!v@mPmQM`$?r%_!zPa4e zxOT7q6^dQ~b0KDzHtc%a%Bp472d=L8jhmPhHeLSS-X*zoo}~K*^nh(ZH9QRat$|Mb zCSMDuh*m&FQ57HI$+9GLZ4b%~dL%%Z5;uQ`?ZA7evyCfs**;*SJN5tqnD9O;(mhRu zOH^_NaVnmy7ibX3FLPea2Y_l;;G=Je%rK=}(n)?FGP{Zd$y9DoPA};`Qn2OQ3b2pF z9o5F&j zK)Q&dwwXDE*s4=bKKuAe1S#0%TerElAH9qdMeU#lxPznLS!x}=?{`|o6vNJXhZY9k z{^qe@>Q@798Und~Q&b+yoOP-fj9*=1R8ImKUr$G_8Z7M1Kk{U<^mm_k(eDklZ(QTD zFWxS5`F&mX?C^}EP2c{=?X+5HZsQTXNFMbzlOI4*(;6}smX_H7bn5=>4XhL57kgpn z?Z_6b^z`)2)?nK8iN|?oNFjg^Cx|DHVH)H_vn8%cADXR%bba~Y5hiM;_UPkyP2{=i z?xzfkD9w({(2KE|Cl@vRYM&4K4p14A3-CXaPit3B7d(o@D0YEaMr zH;N~*8$sgqs%WFb2y6?Ku#7_nst29q2BClBh?H&&94uEaG;Lvae5Hbj}3^a+!ZEhRG_ctfD4`B>30x)%cUmXqjD0 zUrDpM?8F7^s{i=O8|l*{&pQL-J)s(F$qQp+`3)$B9t08>D5ByNuq$ENCrh1bmuv&!$BDIvNA?hzOJ_aF}7zU@q8~_Kb zIC;L>j@h@`v}-RrWD)Qe>DCx?PMjzdd{)Dk@K`H@C|w3?ddx9;nrKAqDTjLU@tZCE>xMvqQGRLuiVTo|a1|cTw&lucPkO*-7<3 zjT?}$7=`DL1JYnrVRcmKJy&*%Z(AiA=O7|oqq11X{=3urA(|=N z@9Pa^&l0Alr^|`Cvf;6!uIS_WAV;2bm;{Y>n|GJ;~ypE|x_rscv zR_U8!scksjEvBd|F{y#|J^AXW51nD(Oewr z;|Ult;eT&J+&@b?Y%e|!p+jr%>!Pj_NqBLQ>;!S4CQ7xV6-6$c(IWj^R73j&d)Ito zh>zN;8b~IZ{*0B^| z4{anr^@rMV5$q2)s)A6)V6mlF_#L^0*d)s?NsouLgWGsm#oC<`*m*4ahHF)=yE7+jcVj;6j^-3cLNDD+zOO4e1Uq9L!~s-|Qvo`N!Hjw=jGS7Cq9RuFWM-;24-AKa@lt7ATjmCIl z^uE6sdR9}KuMVS!Gh0R1ewOGmdu>o7!6EVdb$Q&{;UfO%ERJi#V)l36fO5Y>l481~ z(_f?+KbZl=)rG&o8U%$<((cEA*e*rIr|_349C8s}qqM9#v~O5_eT}vjpdX(Ir_DE3Qt01 zs^lgpBi+D2q&zSU1n07o7C>eYy-|DQ7`2xD{WAbKqqDm!Drz#S8;=CrM)D8|hbfKI zti{uCH>XUIy|Wi;NfwqfyPti-&SR1afp!K!wI?WmgB3!n7MfsYZyzOM0X36x6qFLr z`%($DN@y^1$^GG$DX-7K$qOW=lL3bERTT}z69Bie_S2AnAxuFKX1TMnu+S`)jzgF&kqvC7j1B&N4nK;+wNJWfD8NF!77cJ_I1J^%p&;6C}${5YkYHP}q8GNbHli-z8n z-Z~HQKchVxRw6(ZEl;M?#@T_If#F(_3wd;}Fm+EZk_Q9^p^^QMza(ZA{9K&4+LpH+<$dJ%y53Gsuv*z+h*BwvTu<|s z-nG}UToAW04iC|yTSqUaKh@ffW86s<7@9P7<>b-Q^h24#$38(8e=E31$!M% zeHT*{^Lx>^x!HB1xFQ_5s#`=|rTvaBPvMd`!UtK5bC;(7l-?@*K192CS47yzI#0cL zQ~7?&=*!Qsxgy>9E?0))2l&ne6wf886Pc+YR^7VKGo!Wc-eQP(?X~`iN3GumREQGD zG5QhURb9vPxyy}yI#cwK#BLet!(H(u10QNZk9$o_rP_S6vn`n#lQc z`uWG}QvU9@dj&IG?q|3UlzBZIqFDER6eN8;RM_Xb)cg>KQ_3FO06o$ZtlpB7BAnlO z5MZU%=k#bU^R*r2XvXFCEhm}?c|D4YS3~{AN*4{!SF=5{f)k%KZLCWOwPNLB_e61P zV$Y7hTPwzNW6&x-?@<~b98~mr5|xoQupl1|;R)^CDEKh|@RQTH+_@VRgV;#5IiqCT zwAx(6>e+CQ4e-$#JbxW~88SDfo=w@$d-{X29T*V0tae1FGwb)m)7Q}{w2Ow8A7{PV z5OP2jck~M77w7I0p)mO#ZjGE>-)u?UdVP@Emv8w9rEm};&t^AdEc*M3BZ z!bF_h^jdf2oEMWBqC>XCLXCFi7!NzT{mAX_E0H6Q2dzTfN-Yz9;n~Q^ z?2adzZUDLVD3>%lJa=r%>Qu)Z{Tzqxk^>>r8A(z1QFax=n733uWU3M~2t$g3_CqH^VY}>aVy=J0d z4O2*DQ)DXBb-Z==Ib{>2A^-1c%1+D$MfDdsW3WVDo)*u>s@)BCI2*03baU<>m+0*` z^V9DjioM@XkCvDkl{5N4)LRbcg^|KaH6DDg`jwG8}Aa=)GtMRujSLUl&(+QbDoo_OC! zu5g6`@x%N#t)Q4#^|P|I)%_yQ#e4liUPyPL9EDR&306TBFL|@fw^%fbwcF$)01cTja<+;_h9#nnskLQ^Z?&;;W05H+gv-M4?=}Eyi zWGHBPw=q^@W6&Ug?e?^?H!3Uux#P^pUuvlo*_V8&(9Tkj6>I) z34z-YD2W1kulKS6CkLA)C_t%AoA>7V`3E1=_st5ZztkZ$;T7@QrX7bjix~c^_F2!DK&BjcbD`W+8t8YhtNJTS`9|xT;3fxJT!x|oL_yta(Q=HsW7|b#Smtu z2ws=|?d*~J8Ry>T3z+QH#099bC06QEgY7V^Mkp(m3!WBR!bKga&K9ai|G)w(yU_GF z^qCiIg>qvC4q;wqE`M}``uN+WNK`71x(esTIE!lBs*Jj`lJWwBw$Wfi^ew3XMfE74 z{Hkdkp1Z#_W!M(zYU!;Bn3aVrjf**Vq+c!21L{e7g9RUj5?}3SHu21f?viK>?DmqO_A;@1JC0TLv%tDnX6 z+5&Tr zevA%l<9?#ENi5S4v;`3EIRS?hq!uP`1ubu@cg-L&?7ow8QyeE;yi(l7-9ygLpPB!C zndS;>3}YpYA>avCgn0tt-klP0QnY+WfYkKkzx$<(yXf<0mEqT2^(VkuP-XyV;RZcn z7Qq4j?rSTQ6V+76(1Xz|{MnjbHyVxQHsrJ#`Ck;m|8ueW->qQ(=QkPvodV&X$8_xx z(N4~&UBm$`=~s}q&r?05tOZl7b&1mO^JKXE-<31}GiP7c{7iu=;^GKAy_CJiV}a z(k1N#1Hx(#&TTksNP9cAoOVm$e^VR(qxSf};ob3}=^Z(Nt<6+AP-ZUaDh_(xc%c5& zA2Cint@d||qW=%s!GHOX|NR|q6fS0e8aCxE(WJh;(k0*ZFJOSJKt-S#d-D;Q1!jIyVul)dDvoNLQ)cPtW!Nq-R7q)P7Jq#KtBsi?wU!|GPU&m2q*8^iwLRoE1C&iSvKQ z@y)*o*_U-V7tC%Q(!EhBP9(~c21PND4Z`$r(e_=&#g)LytCcQ;M%Xi!|LFXz=;TQ@ ze(vb7AsLEZ-OvUa3sE?!^CtRx42OI=Wf2= zKz(68-5^a^GZcRd<*quZ07&-ova(pqo<1`$EV*+OhMBllXQKDZcOb>e{r?Q!z^ zz2TtxS@&4DYbEf*wGWKZxGjy(Ii@S&*H=7yQ>%7SJ)n$IsXv+7eykl@<@u!!VmPbd z%?9E5vn{H&*QHLooV}pT(gvU_-GeH(fV5EZ5Vb66r)o`G-P{VTIg`fXrx8Jj24UkG z*E6Bgg<-RN^!)x25>N?{p}K>`7z1Q!4Fp||XZ9XqT1o+JAs;9wyyEoaU1SN&*`pi$ z`*!|c<$4l3LuTe37E{@hL3N_kK`Y@kyqCE4g{=e5EBr=rTm#GI!|+6p5ccf9ashqk zpn0Q=%y?1h`@GFCo3nM0OfLLoT9~3|$6yfCeoICt6-?7RT*8<$Jp9Y);mLWwGH1x+ znJ=r0un%}?eHWmjb7QCdx^MP%rbdl1+P*V?9Y4ofv5b|pZ#5I2mfK{MpiT^gO=tRTER#|`lZ9#kO~mRC z00t3YmO^!;X)f6G)E~sUSG2tcgAo;wFfp<4BiJdv8?y{hREkC(me6ud3iyKlOn66@ z9R^K~MF1f7iDgI_{x5+y-}qJZjB__4C`b|id2Vlus{E|Y zoo@fIQ5Dv~GYR0!t=nF@`epO$^M{@Opc=2TlndxHkBG?2TdlzJbd0DBuN^yWT(@yp z({YomY1TWbjm0&`U&S9VZi}X>jJT2Jim-Yt6W*(mFavYXSPj8cRDk%S_I_Wp9HM? zrTlR_?Z}k1y%|Dy&H0H#&Rp$q-s`dP*?8PSSy_}yg?DI_R>A_Yr?vBl^0}}x zF{l07v$m%N%qYTBnLj*K_%utg``UQ~f{@=vJV8^VIX&UHE|Rase;rm+0wBYAC`(-d z5*Isc=BD3V1hU^6@L$`U_HAInDR@If-od3rJF)~DFj>%?8Rz*XHMWL^9(=Da;n@n+ z_Sg6=cz5ve8eq1jIlr&7b4t`7FL8-S^JU5h#g{t(Z46JRP?iHJrVw5{al^F!Y&QxO zsCrY>mM?CrzD)QW*pWm&E`pX@pmOdT!|Ta{l^@aP6^*jA3uPVurcXm5lwYlTT?2Xb-zn* z`wd=1R`?;`%XdB?=5sj4G*W5dy{w%|bY?scuj^%)Dxc|dhOHP*8Tx%QW*8WvKoqHI zn0hNFfV=2XA<^uc6m*x4A6k%W+wsN6h24MTnpvFo3c!AW*Q? zJk%a}@SK?iKJrZHFfly0@dS}^Euv;P+Nol4a(%wQECPm^9`DOeyfvv7xZw9enCllD zoD;E2UmkWU-|dLMcZ!{TZ&KD`#DbLBv&+Hl_yxt#Ac{N5UC-Rj-{Tk;8fQ$00J6B3f zL?qaP%4>iUx5bP*yQNf{w0P3?qpCtV(e?YshnC=&*4H*ih;WE8B~7ZW=W z)aY=WfboXZ`mUgRpSG&``pqYIx=(bCqL;~&88-w6mB}t4lZj6!(hGh(#!m{5X5s(VDHD#*=bYD1>6-h4_?3svzZj@QbO1VHFTX7%O94Kq=|49Vot+va zx~KLJMoV~if}`SpQ1HU23cu9XT1>>S& zO%r<6dBnE3SQ&Sz5j9UlsB3B6U5?9qZ={s{TX8=5Re~V$4W-awfL;oN4NVU;{C!Cl z!|5Km?c-FTRU5*{cZFKdlHqB(?V_J9*}iTkJ*f}SiG?s&$o@xe!3UeY0xR1Z%4J%- zc-T>VxeK9Z^z`9t&fRqDgU$+U*NhZtqR9=;(_|MHEwt5(+@kfO>21RhFnxYKFtLjI z(i8mN%-*^?<5X4OXa}w_Cwz$!E&InK4lj*Ds&e#v-CR9oojycv-S1r9p81LB-ksNd zKplAj&-5q24e`=ahEKVFyco3kx{KR*jxNUk+g-?Me(#ILkz3?In|3jig_@YI z;#&pL$N*2vB(@^asZM|^H|P`u7d$5}S_YKpy)TSsi}YVlbc^gbam}-ez-wV^nWYaf zQ`Yl0J)|6rzL^9K*;tkn)wmDwz{Fz-2{kn_sZ0kRh?6qXxIdkmtna(0cQo|pP`ft0 zpte@ZD8ZR!(yIC*exB!dsKDQjk&qO<)R2inY@0<|^XAm_3Bt6>N@y+j97I}WsJyf> zHs)JdSs5?Uv$g&u;p0MWx45;<%=sDX+r2mqSv?*-?>tI3S{1Vv(lSl2unZW!MK`At zgPAtBvZ&s9IqUna4L!X$h<0O94$Zj2`oX(I>{*Pysd05W^(I|P!VC-lf;L2q&$_L2 zNsD4szxNQsYYNcPntc{P-_yx{~g*SGv(4?0+sIs7kY`!XNxLib-nDj%7ipDDn%@lu3 zcisoTL6~YT-2wmOcPryH-UzRh1WTZe?_fB>R0M}Y z5GNh>;^qHRBNu|sJ$^tFNp9W#GR@d&fB`coP5bPLg&5V2=;y>Jyf_LGtj8F@Pg=#^ zX6*ypS5=pf^KmS-Ndaj5AT><2)>{ioj6JT$OEs5zVJ~%JOvc8*;0^*zXfC?$#70_y zuR3v}KLAfX-R=XOoJDuH>C&jLsT+?iYiyqKgE0ga2xk`b&p7G(H{8!0i{0F z2WVYP7-Oi*9*XbpzflsE5)L!~hfNv+Rrmo3;bnHHz#i&5=}VM?le0ieI9)OZ%5RO~ zIj4lfjk|!r>*Es|W^|D%2My-QzkOB7JYD`FrO;#<@1-;pjHDGG=RS~hIFY6=FQShD z{FE`lVt*9l@%&^=cYtQK=$AfNQwJm91YRVg)I2{U7Jd0*RjfzQxG9n$R#9QPnWef- zhLHd`S3roxfKh=m@8x|50X@$@iDCK%46iTD$A~#~2kpELkJG!eu~KdWvvszKDLZG4 zRJ^ODbv&=QEDfj?QB~E|CVB^zrIov^30I75%$^Baw+Ezs5w-cV)L%yD2F2MAXStQx zSXXsmXjRk!Gc?d!I%vbhN`yo*8&$FQ`Af7r2*1BGp$0P@4#H<(RwO10F*_>tIQidR zGTQnKm=G!3XN!^GBw;CA@qU0GEMe9~xM{0xwp1h|gZf(h6kbAvGki&x6tK_v_(O{n zn=l^*>Vsbn4dirDJCOl#Cw=Ea)N!^rwG$A~$xOzZK;CY@mi*%J|H53aP>ldJf&Z>n zXV^WDQ}H8CE)lI00~u@Q<&bj^GiNye6v?O~*q$M)XpH3iK;8MBtV@WuhnaJT3h__O zfRIujPalU7?Ap9VWfz{k4W&cDjmE3ufFxl3`}rm+WXN{_+ak4Kr9Cz$`H%TaEWcjn zuV=QlVt{5QTFAiD(-XjrG(dI1*+!NCUho8HX}1XvbaE}r2|Sq%H3X8hsZ~2IMCXn9 zk|ZW3reGmkz8!9J1Z6FVTXN7t&Q{+^A;jB{$u7v<~Ujf_aw?5cNDFXdTe{A6gLf~cCZo8~Ht12QaO8v#XV@ znmCj-o7kJ1M7M)U^xx9?sp%;M5F-kSh41ESL*Pc>WU60u7lBpAa`Snx zmDlkovi?h@0ob6)%+PX;LWm1d2Uh0hij*@1uPTx3mk)MY0VM0h+2PwT+(el5U$bu0 z-&Xv7+#BsD6AS;M<;O<)gY1ELf3E%m65>_RbPjhKhsDR)O!&8w?4{98#yEPeXW^{LUnSBSu#M9dNwuq=TUt@$zh{bz9~ARr!bLCyH5 zZu&fYU?56@1*JYVNi!XwWALZdbcsQwmid!7f+WYIVBFl<7NOq5_)T8H2)V;YKOLnG zlh)Q66IsLuqkyR;_p18Z03dr!I6f>nb`$3d(1j4iwQLCcYKmZ%7n|GnyS@^;e%{tL zo44B{WeC5p@BQ3l3`mr(==0xF>?HjMs>N$~OvG^t@DJPo*ar5u+{A&^A&^y4O(nBA z=WUoj6(!)E`uhE6az#owp)gU`RhGftb}{HX_;Tt7xwq1x z%yqA3?qyq8F#ZAd*FE^Bz8ZO5QDrAC7R5?9Ti45o~^0b!@5j_j=cOvr8S* z@|u>I*!^2FUm;sxH`*i}&Lpj+9tHqCJ4oD8FZuJUt((a_Mhx#a-8RuHI4#&0RVVrnAg6; zyCy~f?Dl!YsTs;8pyygzQ^U+VM`>hfc^7!n2!E!2l3JteTdh{g)HW-GZ^sKqE)`(<9cscqW6MsT$0aP+&I^=k2>~oX zNsxV+!ojpDb{}b5vGUME(n3@|Fv$EXRC`s-T*&eR+ihZKT0dGwM$*0OO6ztR9yqww z(zf0y_^e*jFQ3AfVmo(k7`rd`WxjVLULz~2!M6!4oN88s)^)5KoPZXyetQ1X!JhM$ z5WO_8(y5Ko*>qLj`)xTJwX;Jc*@2Y;sSjUK5Jc%;{Gb$}7J`83+s(r4S?OSMvZ>c- zLG{3rOv}p8)Rnb0KJ@Z$uJjF0#%ucNS?z^JYm)AH%<7e8WyZ&z;~!=_ThbQJL*-`e zChdeark}Ji-53PY0{tOs+amC@W0P`S-5fYWv4emUC`Bb4RBBhO=4(3pBKNl6 zz#gbPl=4z4&`qU4R4fyrZR!2BX@A4~i~CnGuo%VgW+ym>N>N9L%E26a1O8_D$p2{T%j2PJ-}Xx(Su?a~NLgmco^=q)HeqbZk~M~8 zUq+V7R!L}c_gyCU~La0RVHJUn-9@4kWT_AHDo*JB7C`_vJFQiGFxIgkDwRr%vTWOy&Ma zJu0~%BbT-tUH3ecVFd@ff)uc2HtyH6IqZ{rkM8qxH1%r_bDyv*(RvsWg$qq%b$jX*Q(hl z6Kg{7xy?RF`is6vE9xL8%K<$;87Z?>#rH8zE7nFAGDyXK{|1M@6DOv8E`N0PX`2pl z;Pv#+cw;poZGER)l3i*j2uU!=oK&rZ=Nv72()MXt#VkHHJG)Lt>~Nn!1?}8^bI6&y$sA={!Jg8vU^P9N$5L~=JlSh%&mrs&!!LLVFW8?xoG8VU$)+G+A}ZYYqB4zxeD1t(~;d_ zn;uw~R^jH12^=Su)j`#)SkD?Cx%8oH{G@t$MRc}&z4p&m=)LaU_4??xml>fiOu0q; z1DM2GJ=q_-SNP1deTG(wrU`5t)+ zowN;`@O4T{n7h}SCN!7AL9-cN2OX>_7~WQ?*N(dlTHgk|B9Y|NCGrXAcXzl<6&1#Y zA4WuJFu#{8c25#f)diW}!)^FiyDjvMT*73;+Og$e8#Ejon#6^m3!*#_(xF3 z()*D{XTU0-Qq5a)WO>^Vmf9yj`KS^~nheXf8NbX1=B2Dpe>)O&_@wc|v296-v?Zf?>a$Oaprn0- zn5yM%k(6)!I)zT~eXv+PV*+vds>o6EbL`lDE3d`b)a9xlnI(7h+y>m&6>HBt2@r`7 zSa@z)IlEYPK+>FDIg$!gbm!LCtwZ=%>aH)4s%-FNldr7%g3Fcj>)^&iVP(|^lYfH_ znk)6~hLLhR%Py8)z6 z`=?Jd=5dE2jL=?~36jp>e>IY?dZaN*_1aML<*@OqVN<<-*p?ibB@&zrApBbOcHQY^ z4GQM^Q(>t^k;XT44;3=?EZoQ`r0ZbyZWq_p z;Nb?#%kvY{BH}Gk0`!#syev4s*fZU1=nJM0j!59nhfRh$HLguwM(gH@G>NP2-&oKI zk(z{Gav2|3a@RvhNOoU&Tl~EkT45#Jo9h{bHU}W z+dD`v=H7ljC2;ImV+n|4$vy}1n&UDVorUP?>KnB~;fukyqEF6Dz9GEfS{}7F~+_->G}XI7Lh z7-TuYjtUYo(_(gSM7dk8gSITpGap-=KBHz)d2(4zTLLR5#f@2|!qA7%Ve_d+({o01 znsaS_uCFUaGbb@u^G~dIsb18G&AL>wu0U$q(h`^39(!O|a)NC`wb$LF+{MUat-xS^ z$_)Bd=ab_lJzB0e4!ql~-{4!BROpG{^mP+HXVyhHZ0k{>Fsx*1=E_pUUQ}6Zd|ckS zm;zRSYs!KLGCo`RPDJQViC+jXvEgw;yMaS9$+s@6;)wIRCfTDLfqma+CHAX4il<4s z3{K9#sd=lVswJUA;7@2H{zLv`sApzG`>L5<#Kk?Y*9t{icJjWDJwzy0LxoRzv$RcG ze=eBRg65u&s(VIVq9tm)KAL_fjB#9g?W}arC{SZ8;(aqREI;KRB!{8A=Y)8`#~fn! z>8)!2&yQ&>x2La}=ShIm2P;*#hOb2YX{jePH0Wb=b?w06yPe0YQdN+#p9{+2e?+H; zxLf;kdDE%Z3K&L^)LyfWoJ3zv>&B;?>R{TH+NWUs$%m>tjQS}HKlO>=ou6=Uevcyh ze3WJ!20L;e^O%qlZ2EZjQq0m4k1@3PCw^51i;HE}FDAJ@x9!P8?Jm83*whp$IG!mlV5-uY!Cv5u-$ciO zQ$GcI?^^SOKw(@_qSQA{#<9WTxV(cX zc%nANzQ(iIMC%_tMHex*eb1AQ0aaHb8CAlM;6#t#mVTa9L$o+Lcr<@kR+B=X44FB)+jO z?Fx(ByU*b0QRQ!G8t@8Lb2pcdYNG8iRn zb}XZz4q}9FsEcQ7)F|?LSr$hnwZ7~X2V>rrSZ}Yp=9juGu)`&b-z!U5NPS};l4Rkx znXbq=;=oV$<}x+S$_tp}Rfsj85bf;Ei4n6ro~fpHML~CMqnm_{D6{upcE82>DIxLd z>z6+fmZ+_t%lLYUlVY3q%>dGFsG!=Ipi)jVQCoMawa<%SW@()<(n0^5# zrpq2Cb;~Alt0$=tdKg7~VyrCu@PKHdL@U>+<6nTrEh1+_8T_~0|RcCsD z|2QFkvi$Um>><0=?E=wfQhRRp=Zr}l$GBsK)kVii23D?aG&5mH@fhZ`^<0ypfxBy8 z&ggjDhTCj$s@$VkyE=%QaFEK_(g?&>EQ`p{*H};oS7?bHe?z$NrJ(ds^ojuAV4pZP zPZsr879SeC0&~Kvj~op0g_XOb>T}`L#H%nfKK{|L{%_Q87v9s8D@|G15CIegS(%}p z=3j}fhiT3+H^BVi78)sXOP~iFP5S7(O03vEBp|NoTDi#ZKKGRsrf!+7^wgw^E)Hv( z98%N6?*5uVo}gcWT@fyQrvXeEbXdu9OJ$(7qmsFzkIO(1T9}WPIhg1!%t!vp7rla$ zDbGVf>50`?Q$Kd@OVXioZ*L@oR{;3k#;wNz>ILLVlcdzj4TY zc|1bXxXPs>eJMA6CO9g`^OoGU>nU(3FiZ7rLU=eW)Q;!k%h>G&)S(g>QF=X?KpHX zkj3u}ye&`tUP6!`(~9>bll`^9ZBbhFiK(CwX%knfzvj)}LGO=!tQ*MK%c9{5+EBNH zMt1U#IRfKoi8O@U#VRwEJZOF~(Kh#pRp5uq?rw3crk0+4?zh$xX^98)M>paGYM>k7 zOg7~=V=Ntie{>GJ>9L$i z#00KCIfTmh6GXerj!iezfry0aK#YNxcZAR`=Y&JD5557P1PO#?*Nf$*n~9%K*0qS+ z%yP!fE9v}z{}tdM!e!Mfz=2UXtcI{C5(coj9!{i&xW9dE#=M|*qej&77qD0$H!0t<-V}T z4O1wW40G0`A;U%XAbDqEh$wi_o64SMZ@bDMW^DlXH&xJ%7uN zFfH$8eS0H;pi&ri?(Fs#@oXMcgI^jEqfdk|<8^NFxb1;BLnZv%9sa>SStoqMQ~=4n z%jatUOxYQ#LTr4VxtvM1!@(;By(XS_!<O!5}` z-}dZ9=XT{E1o#b3kReWzl-sWS*cnyHt@HYm#gc z$sTKfr=x|;V57%`?wlSCD`1A44%Jxa z&yiSDM9^;TU?TnBTpx{upF-VV0>(6e(z}QDN(^5n^*0b8WRU0;F~F>$JN&fpf|Y>% zn2Fo**}gZkXx!YwY38%Ho|u^o*}pBLOt33Jr8GKcsOi*CNh3NHUUm01--R#+(`jB zJo3DXp?}ii1q#Q|f9|gxv^xpk`Jz8@ZCM{0A4*?CZ6Y!pfE#RK*gOuxb#tawSLB7L zp@5J;YAuDc?8J?Vj_X}szcZ-YzG{FRs$CCjbSN5iFY;{6`+a~F`SH5mW1&c&>gJO}jHr21pdYNq{sz6%arRKd(#mjGnV&@2F~Hq7mTl zxp07^j{$&ETRZyO1rh+tz=Th92%gl}$W_N`SkoU_u_u{?%#PVUa@P!k@-i(+k9kXm z4u3u+&triUB|)J|Rf7 zkkU1A@hsQ&`C%W-sfCZhhowAsi=Z2cqs8l8`GtiA&1`gkR1+;(t{?2lkgAFxg(v~Q zlC;dZ;#9zs$H;xE*xg6c*gQXh1#!Kkg5RdIKN>r0g>APatjY0gay*tJOhTX26U#*b zCMrY)YKC)D;#%Owk`3Q7qVy6@5X9Fdi*=oRrkcB&r*IF=kAJnrA0aAP2v9=|V98X&Ua;1wE>kU+So>0LVx|d^1AH zo}eur2UcVTB_-_Y+}}?dnA!r+_rPXcN3F2(#K*+972fxBVq!WTZq8yPE#BYD)MY_S zf8D(h@h-QqH#JjjvkWtVCZV0LneQt38qqM?X?%nkj|51A&L+7C$RGVNfeOXghxmTF zE5&Sg2Ax-_l_Goc?R}EZAkvn7CmEZ3_SoOL?L1hUJJTP)- zpcSBOX4Y&1P~XRtf#JmOHP5_V2K>%rw-4qY3_t;fcTJAlHTvLVmhZOinb&J4fOC&H zcgAXZh$DTxr#Cpn>zOns-9RrJbjrRh?aQT=>;NgwRO)?oOof&yX?5AX0$EU=s}^y3 znE<(CmKkbO$p|eNLe|RzX?t!sv9c@_$l1?|onczXmOIX38Ni5Hh=osVuvT>-Z+xjI zhGUe)Cg7<-iA#W-AbMMNyQG#lo(9mCP~1%>A2j+%Ha>^}A-UzbX|r;o$>&jyEPy`+ufVhq~V;&Dyc+ zH%5u$;X5h&9LQ9#p4SWM2V>z>WPKa3Y8+irFFvbeM%N{h!z8>yVtI)uJxVuDTk-o* z;R8yMBVYz6a&w*8RTgjEqN_K3H(txCNtYEZdFHsUXvM-u<dIzZLz(o=Ws`K2bMmy4#c#VFMd36cGnqeLOb(MaTQ( z^u%K4Ghpl|!j8&rFJB&`SDL8|(`#SK0LNACEr6N>{9`72%8e!ZXaa;9W#TJlAH_md zm-RCZaR_F;WLhSMMSu#4$#7Fo;IrxrBZt}C2F`@+9KI}V+^-jVH<64g$>q7UlHX}+ z7Bk?O&vFmnbwFRXhD9K)xtj13q^gvVwNBu-(<2?h@(uwHyv=O^co{^O-0p=0CL6xAu?e zh1qcwb@X7-nlJD$zd@7h>lW;IZvrda&gDk-xdVw-o)zRBdGAKsluYu6u1Lg3yjt7A z43EG|`qcpt)%tTe48QYDI&7Y_#(lsx&@H0Liz%>C?%#x*Emr6wVGu?3v ztqqVL+b}<|<^g%}W0r3;av%{fBg3a?RTqd2Yig`1!=xw<=PC?=kF|&*2a{(yoJB^W zZe&m~yLVitR*_W{2#4NL2M&n(OkVfSVAl+xs<)3ydW<9mU$9AP&fvZ7zvx$#P^b~^ z1RTy*c%8>`8CYXqC+Z(VYc7GZVUEeiDr*cAE&9v!_HISs(Sbxa!7I?6xQAl+aBL$( zhsvxNhZ5E$R#ay)y@ILLg6_?K|gOO3*Wu|Zc0cXkO9Iw?X}AS>eYlY$f*HK~s| zZSOTtM8Yo16KC|cZ;HNrS#xH^lZ8X`>nQS54w%2_{E$ewu6L9_lcrizsxc*mk*ocW z;6-Fj?34A9*Xi~>&*kufKu|6Awf@6Z`{?%66W09-NBBSQL2KT8!L%AcRCO%75~i0J zCm;?rvEp-EETa(@+9S<{UU$=^mqZQHBBUC0q{o5Gx!D|7FD~}rGsC@TZ~dFIV3f$! z2-mFcf2)5hnA0;;5Gc0f-(1;D{RF}pgiFCAcVWwpTD&PkUgbfmI~0_LMI*;-@jwHj zB%iT!-waWzm3Iy)3n3O#ZIAe~Zy1M{Axp3sR-5I@qfc&7QZm%i$Ge}#uX$$6m=Hh| zdpt8yL{CNEx)paUs?n*U(Y13(5x{`ih?g~H;7F^aDbRJ}*!lz@J)1U}0^#4aim-rw z9r?+7o>awmEKCZi%>^_fUs7c(2I76i2p}WCL?%+RaEFD={%T~6mJ8?Rr#z3#aN?&q zUp)C@tfyBwbK51gSy-jEt04T3$$SpsC+G3VMTCwqn74D%H2e>Lgw3k??j84Upl+5* z5iZU2wre5iy>p&=SREQy!I26ya~&wgp-WYX9}4d#&fPu<^!%f9aKV+>}b)PMMmG?w0d=Lw!-0 z@913pb;`8cXsVJs-m^tdLsrY?N z7lh-1Bizt@E5b57hYNhc?O@Iz>W$mJKnkp&jAvZKpb?Yg0fPs z`H#eUW)UU9%afb)eR_`sq8w}oOqn|`T0L10LBg!bFNk^99l;I%f2L=GwH24^8Fp71}kSArn@?)MG(DXULNAa^XBlTt2m9=8Kr&60|$v+2DGf^5bN`Q4tBIX Vus7Z&ha3PuNCT99`6bu5{{b8kr;q>u literal 0 HcmV?d00001 diff --git a/doc/gsoc/2024/ragul_raj_m.md b/doc/gsoc/2024/ragul_raj_m.md index 2b415e3a..d4a2033d 100644 --- a/doc/gsoc/2024/ragul_raj_m.md +++ b/doc/gsoc/2024/ragul_raj_m.md @@ -1,6 +1,6 @@ --- title: "GSoC'24 API Dash" -publishedAt: "2024-08-25" +publishedAt: "2024-08-26" summary: "Summarizing my GSoC'24 contributions to API Dash." --- @@ -37,16 +37,22 @@ In addition, we aimed to add some important core features such as environment va ### Responsive & Adaptive Layout (Android & iOS) +![Image](./images/responsive.png) + Restructured the app layout with responsive breakpoints to automatically adjust based on available width. Adapted existing components to work with touch inputs, ensuring proper accessibility in Android & iOS. The UI compoents for all new features added are inherently written to be responsive. ### Environment Manager +![Image](./images/env_manager.png) + Added a way for users to manage variables with multiple environment support. Made existing fields support environment variables with custom highlighting indicating the status (availability) of the variable and popover to show current value and environment and trigger suggestions of available variables as user types a variable name. Added code to extend the feature to hold secret variables. Extended the package [multi_trigger_autocomplete](https://pub.dev/packages/multi_trigger_autocomplete) to support custom `triggerEnd` values and handling triggers that could be a substring of another like `{` and `{{`. ### History of Requests +![Image](./images/his_of_requests.png) + Implemented a comprehensive request history feature that enables users to review all the requests and responses they've sent and received in the past. The history is organized with proper grouping of similar requests and sorted by timestamps for easy navigation. Users have the ability to navigate to the original request or duplicate any request directly from the history, carrying over all specific configurations, allowing them to edit the request. Additionally, added an option for users to customize their request history retention period. They can choose from predefined periods—one week, one month, three months, or keep the history indefinitely. This setting ensures that the request history is automatically cleared according to the user's preference, maintaining a clean and efficient history log. @@ -110,7 +116,7 @@ In addition to testing each model, utility and widget added with the new feature - Integration Tests + Integration Tests @@ -128,7 +134,12 @@ In addition to testing each model, utility and widget added with the new feature - UI fixes + + test: common runner file for integration test + + + + UI fixes fix: color, deprecations and tests @@ -154,6 +165,11 @@ In addition to testing each model, utility and widget added with the new feature fix: refactored ui + + + Fix UI inconsistencies in mobile + + @@ -202,6 +218,8 @@ The history of requests stores all instances of requests and responses sent by t #### Solution +![Image](./images/his_model_schema.png) + Use a normal [Box](https://pub.dev/documentation/hive/latest/hive/Box-class.html) to store metadata of all request history, while using a [LazyBox](https://pub.dev/documentation/hive/latest/hive/LazyBox-class.html) to load the full request and response data. Ensure proper concurrency between the history requests stored in both boxes. Implement auto-clearing to automatically remove old history requests on app start, and allow users to select their preferred request history retention period. From c3ee19cb95f6a4fcdbbad9e3d573d2af5c4deca0 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Mon, 26 Aug 2024 17:48:45 +0530 Subject: [PATCH 58/71] doc: env manager user guide --- CONTRIBUTING.md | 14 ------- doc/user_guide/env_user_guide.md | 36 ++++++++++++++++++ doc/user_guide/images/env/active_variable.png | Bin 0 -> 15718 bytes .../images/env/inactive_variable.png | Bin 0 -> 15439 bytes doc/user_guide/images/env/var_suggestions.png | Bin 0 -> 11507 bytes 5 files changed, 36 insertions(+), 14 deletions(-) create mode 100644 doc/user_guide/images/env/active_variable.png create mode 100644 doc/user_guide/images/env/inactive_variable.png create mode 100644 doc/user_guide/images/env/var_suggestions.png diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7b05a160..739167ee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -127,20 +127,6 @@ Example: flutter test test/widgets/codegen_previewer_test.dart ``` -#### Running an Integration test - -To run an integration test, execute the following command: - -``` -flutter test integration_test/.dart -``` - -Example: - -``` -flutter test integration_test/desktop/env_manager_test.dart -``` - ### How to add a new package to pubspec.yaml? Instead of copy pasting from pub.dev, it is recommended that you use `flutter pub add package_name` to add a new package to `pubspec.yaml`. You can read more [here](https://docs.flutter.dev/packages-and-plugins/using-packages#adding-a-package-dependency-to-an-app-using-flutter-pub-add). diff --git a/doc/user_guide/env_user_guide.md b/doc/user_guide/env_user_guide.md index 905cb44e..0bd7a464 100644 --- a/doc/user_guide/env_user_guide.md +++ b/doc/user_guide/env_user_guide.md @@ -27,3 +27,39 @@ Suppose you have a variable named `API_URL` defined in both the Global Scope and - `API_URL` = `https://api.apidash.dev` If the `Development` environment is active, `https://api.apidash.dev.com` will be used as the `API_URL`. If no environment is active, or if a different environment is active, the Global Scope value `https://api.foss42.com` will be used. + +## Using a Variable + +To use a variable in API Dash, follow these steps: + +1. **Declare the Variable:** + +First, ensure that the variable is declared either in the Global Scope or within the desired Environment Scope. You can do this through the Environment Variables Manager. + +2. **Select the Active Environment:** + +If your variable is environment-specific, ensure that the correct environment is selected as active. This ensures that the appropriate value is used. + +3. **Insert the Variable into a Field:** + +![Image](./images/env/var_suggestions.png) + +When constructing a request, insert the variable into a field by typing the variable name prefixed with `{` or `{{`. As you start typing, API Dash will display suggestions based on the available variables. You can select a variable from the suggestions or continue typing to manually enter the variable, such as `{{API_URL}}` or `{{var}}`. + +#### Fields that currently support variables: + +- URL field. +- Params (key & value). +- Header (key & value). + +> Note: The Header key field supports variable substitution but does not offer highlighting or suggestions. + +4. **Checing scope and value** + +![Image](./images/env/active_variable.png) + +You can quickly check the scope and value of a variable by hovering over it on Desktop or tapping it on Mobile. The blue highlighting indicates that the variable is available in a particular scope and its value will be substituted. + +![Image](./images/env/inactive_variable.png) + +If a variable is not found in either the active or global scope, it will be highlighted in red. When substituted, its value will be an empty string, indicating that it's undefined. diff --git a/doc/user_guide/images/env/active_variable.png b/doc/user_guide/images/env/active_variable.png new file mode 100644 index 0000000000000000000000000000000000000000..7f07d39fd12b0632c22cd2755a1b06b1b6a6d8ad GIT binary patch literal 15718 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 literal 0 HcmV?d00001 diff --git a/doc/user_guide/images/env/var_suggestions.png b/doc/user_guide/images/env/var_suggestions.png new file mode 100644 index 0000000000000000000000000000000000000000..f25cca860036879df2dfe65e08c56b84428603ef GIT binary patch literal 11507 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 LeO)eP`r*F;0KzlO literal 0 HcmV?d00001 From 7882c73ec03329aeca8eeaa410fa4627ab4545dd Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Mon, 26 Aug 2024 18:45:54 +0530 Subject: [PATCH 59/71] doc: his of requests user guide --- doc/user_guide/his_user_guide.md | 58 ++++++++++++++++++ .../images/his/date_based_organizing.png | Bin 0 -> 23966 bytes doc/user_guide/images/his/history_actions.png | Bin 0 -> 4777 bytes .../his/history_actions_request_disabled.png | Bin 0 -> 5147 bytes doc/user_guide/images/his/history_card.png | Bin 0 -> 3106 bytes .../images/his/manage_history_dialog.png | Bin 0 -> 25860 bytes .../images/his/timestamp_statuscode.png | Bin 0 -> 14431 bytes 7 files changed, 58 insertions(+) create mode 100644 doc/user_guide/his_user_guide.md create mode 100644 doc/user_guide/images/his/date_based_organizing.png create mode 100644 doc/user_guide/images/his/history_actions.png create mode 100644 doc/user_guide/images/his/history_actions_request_disabled.png create mode 100644 doc/user_guide/images/his/history_card.png create mode 100644 doc/user_guide/images/his/manage_history_dialog.png create mode 100644 doc/user_guide/images/his/timestamp_statuscode.png diff --git a/doc/user_guide/his_user_guide.md b/doc/user_guide/his_user_guide.md new file mode 100644 index 00000000..a860a953 --- /dev/null +++ b/doc/user_guide/his_user_guide.md @@ -0,0 +1,58 @@ +# History of Requests + +The _History of Requests_ in API Dash allows you to review all the requests and responses that you had sent and received in the past. + +## History Organizing + +Your request history is thoughtfully organized to help you easily navigate through your past interactions: + +1. **Date-Based Grouping:** + +![Image](./images/his/date_based_organizing.png) + +All requests are first grouped by the date they were sent. Each day is represented as a collapsible section, allowing you to focus on specific timeframes. + +2. **Request Clustering:** + +![Image](./images/his/history_card.png) + +Within each date, requests that share the same HTTP method and URL or the same HTTP method and name are clustered together. These grouped requests are represented by a single card, with an indicator showing the number of requests that fall under that group. + +3. **Detailed View:** + +![Image](./images/his/timestamp_statuscode.png) + +When you select a grouped request, a detailed list view appears, displaying each individual request’s timestamp and the corresponding response status. This allows you to quickly assess the history and performance of similar requests over time. + +## History Actions + +![Image](./images/his/history_actions.png) + +When viewing a request in the history, you have two actions available: + +1. **Duplicate:** + This action creates a new request with a unique requestId while retaining all the original request options, including URL parameters, headers, and body content. This allows you to easily modify and resend similar requests. + +2. **Request:** + This action navigates you directly to the original request, matching the requestId of the history entry. It enables you to review or edit the original request details. + +> Note: If the original request has been deleted, the "Request" button will be disabled. + +## History Auto-Clearing + +To help manage and maintain an organized history log, API Dash includes an Auto-Clearing feature for request history. This feature allows you to customize the retention period for your request history, ensuring that older entries are automatically removed according to your preferences. + +### History Retention Periods + +![Image](./images/his/manage_history_dialog.png) + +Users can choose from the following predefined retention periods: + +- **One Week:** Automatically clears requests older than 7 days. +- **One Month:** Automatically clears requests older than 30 days. +- **Three Months:** Automatically clears requests older than 90 days. +- **Forever:** Keeps the entire request history without any automatic deletion. + + This customization ensures that your request history remains clean and efficient, only storing the information you find most relevant. By adjusting the retention period, you can balance between having access to past requests and keeping the history log manageable. + +> Note: Auto-clearing occurs at the start of the application, so any changes to your retention settings will take effect the next time you launch the app. diff --git a/doc/user_guide/images/his/date_based_organizing.png b/doc/user_guide/images/his/date_based_organizing.png new file mode 100644 index 0000000000000000000000000000000000000000..2655c5c583a00714c15e61ba2f974707a642bdc5 GIT binary patch literal 23966 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 literal 0 HcmV?d00001 diff --git a/doc/user_guide/images/his/history_actions.png b/doc/user_guide/images/his/history_actions.png new file mode 100644 index 0000000000000000000000000000000000000000..b0373bd606006c63d5e77d39ea524e719da5fc29 GIT binary patch literal 4777 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 literal 0 HcmV?d00001 diff --git a/doc/user_guide/images/his/history_card.png b/doc/user_guide/images/his/history_card.png new file mode 100644 index 0000000000000000000000000000000000000000..ac1b10223eb9859a247cfd3028f8fafb614de2e4 GIT binary patch literal 3106 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 literal 0 HcmV?d00001 diff --git a/doc/user_guide/images/his/manage_history_dialog.png b/doc/user_guide/images/his/manage_history_dialog.png new file mode 100644 index 0000000000000000000000000000000000000000..700311f4d35b1abdcb37b54f11e76362f5b79646 GIT binary patch literal 25860 zcmdqJWl)@5*CvWvkPsj^BoN#!1c%@W?(XjHPH+$I?(Xiv-643eKm#45u}0?3^L*bs zHC5+S&5!wU&QQgsnx^kf>HL8X^L(Q5~c-onc@w2mgM+S$zyTe6+PKS#$vwlxOsY089FeRGu-5!P9LCV0JS>}$Qm3HT&{f+x8r6LPO zbpr+p%Ohs#@nh%j87a)>)4K5!>NO|r{=UZx<;=PV(Y!^?EU{6ziL z_;%v_$DS8j2pMWidmsXh8M*oah!}AtLXmr+0H27A(#8 zukS0XSe!p0aTW{|y*^x&%jMbQUBFA6>U;wsaH^>5z^WFDc%zhW6s=OFp*Wt(!eTm+ zl008LAtk+X2dZIV7AMS!_>nu5 z%ttqC?i~}W*6K9XY*%U5-OrYk(Q4rEX6?G_YdCF}Wr(=#Dk527kCY^o0-5LLQp*5G&VpSm$pQWbsHMXJ%j$%bolg+Y}oz>pBk66<8r(Wuc zFutRZWejVn+#8G!>;*+6{23J^W-0ct)qYP%^QH3o$E)R0Vq#*y>w!*ZlUdZ&I$SI$ zo2^yUZ||PIdq~NTk)oJzt2uL{@Ta>?TMJL5F2Bv@bx-LRJ&&iB%{*ok6T`K8*xXRn zOS{Iu9}Iu*CG$MKz}qxm{PpWfjX;t1t({Vn1)D%hc=*8D!9=R*%?8-yW%`q}yby!= zjBXMSgWt09=1_<%NM%mc4%A8(FCa~G9X~ot##H1|KDi_Z#VVG9d^sq(bJg5KEUCWt z%q4T0Xe7~EYd9#f?|q&xG?z!>prAl*KkopU>}LlT%WprQ=-7w+`s~db96M!QDU93; zTHyW4IuXHNE*cdXnbPA6mF~PDy(XZbaLPDYvX&AnN0FBK@_m&QS*4HNRbTSE`~){c z>-dZ}ru60sUhy4dsK{mPSIx#IUVFY(Qw9yhrNgJ$CMp9tM#x~d<4lRJB04F8Ha|3h z18OH5G~&e>2YWVu@YU&Lk&egxStq4NrCiC2x%Ko=oXt2_@TthQ^TrDm=)3~3CFzJ? zR3^tiW0YF8R3zSg?iKI0st{#`zs8BnP9jR)yd_PgckIrH4G0Y|4O)&_Vlb&1Q7?QQ z=W0P$PRDM2VUJ1wSZlgBepar!8h!C@ekJ__LDUk-Kd!(4nY z3$g1hShqh;eu9RALS2~GQ;E#`fxN-zEY3^j{EtT1TS7uQ2xw^rMTfv{t)jSsfiKNN zA%>&s>Mfm;3a|U|xPgS27-7LgYHEJ#O`=izqXI^6F0Rv)>&n;Uan=3BF=a0qwW&<* z)D$M|bk|ty+tdV$+ZI9fZ^;Q@OZu}XjKj?L@z}x z)?$c2k@xePH*ZYtBk?Mhel6d5l&dvoxST9X3yhD~O|RGJdM{xEeE_Fjk;j!@R;_9x zd-%PeKDqPrqo=e`Y7N;(OWQSzuu_J&Zm?fO#F)<>2{v}ZQebd|(+YgtSc35gByIL$ zjTS+Kk(d6bIcqSneOE$%ba;4!YNr{-@oGyk85&csHqV2T=-E$RaWS!d?RB;Gg#gyy z0io=3L|j(?ti!ezmo4K8-m518OA+KIdew?A!2BSufu8KAh?Y`AA$E(R`Fo9u;X+!i zmJEYoakHYp?Y9QKg@Pw4maMN8ED*9qjg4PZP@pYha8-OYDF=!EM9dZ?W^=+NcL#cf zU2C9{Gn>BC!3vR>zIzM7)4Oy@oYSG2iKM1Cj7zpY!;6M+L7BhNrGq zCYuSXm-nmMkMj$HNrRQz5tH$2ajUf@A@GuM1X?LDF3p@4^0F=8hZEd{D_bE4u^ycU z&Ekm^W~%binQ>?GXLUcGtb$>YxMfI@J2aI2@J7z*>rsi)b}~n$LQfNkG!GL#i>~4o zib^Pg7I>NtwM#JU z*7{nB#8vbqq@luEskFkyR#P3x-Tr-Qtxobc->{{}UJ_|Tg~HEFi?A^VA!0l(pYD_7 zBKX-0O8GCs{FG{kn~^cz-DwhkOQKW#{LER^*>yMjE`m|_bBrkV!8Lffk`o&-3^s;V zit?c;*^s+c4R%79m00jzlcM3b_Dj5XDC1dH()+RCyl1Qa|3pY{C3MrSz?Yd zh~nQ4Vah1Iom~8vg66jJ$!ra1UchXk*ru&&kn4D;;75va9suXS><<5)YjvBP&nK&P za?un>^{{TjUhj)C0pK;$8jf`-@2jmDQ|s(4qI> zkwCUbc=A(5p|e=IQ3hff027v3F}F*YAcjlwcUxEJS(J&dNC@Ngn91Qo9oQ&e%1f$TN-z z8!;A(h8j(wdVH=XFY!ZIqWss(@{RBWFG zm@G*{ixEPT&kvj%>{b4QRaPN5LKk~omIY<;c)NwVk}oT@iqf085@LbCmY@ZV>pq>` zR@}`B8;HhEku6rann^N(l{heS9gfadzva@J149V<_G<)-dYj2!;G<4=^Uu5A>#n>9 zp;?)pbhLV>5yVXx8&PH@XAh9H6`H8m3GU+5GHG|2a^`I=_uq?(J^gBQTvVFm+Pm-O zCjdJ`%Gk+uJ!txa!l>Nz>DQQL`j!($3Cg@6S>QzovfYZP*fgi-{2nhJ5n1M0F0bns zv#A`W^%mQS`DSbHv*K@kW>7Ir@l>YjpmYe+GKSUA=v)%#@m5Cfsv_=}xblzi zcgsz7su9t&bhlW?70V*~Ci!+PV74%~(^a-~j$2|+gN-MzX!pr{>;v@3cZ%Cc9^PE! zaxx*ZWzH64pxOVpe2Q_5<;;4}(#}f^t%e?Ph)TJRcxgB-_fB!%;S!q#leAd>1<`CQ zi2%hbe5NG#ZGU;!i4>N^q(xkTCH{)wp_>i<6 z4<@Ik&gTx0>h#&-Se5*zcfbX%k{Q~p(hESh3UbCcSPX8pb;FJ2Zyt2&TfUUE*w~7! zCo0wP$4^21<8NkX{}}(bQ>@!9e_yFyt0o@HW!*-ehi3EPvzQp7cuGL1q;|9Ix>}3f zCjElogH*G_qt7tDQh^8R@p`GfL^shYdGKGiRQqF(#+9CvUy$yHaMbX%^YI%+dJ5%t zjlU8<1LLMSX#!Ct7g7{kS9&Qub{KitOyAq!2E7Z_No)P<^pljk*SV4k%PkvT6dK5I zNC1xqxb6@oD)n=8X5F5uhkhd<JCSibZ4Pz<7zdNIV2xy7bXdx7f3>|VWPo~0`Y3WJ^6{n=zy_@M9q)LitTw5*B6dZk+RY2B_@m*R_L z87KD6UB9E4iUM06kItA9)j@)gP}MwO+$z5MJ$JueErI8{J5QiN8yi2HtxZlAS5&Nd z7e2vae3^Ys#m1&Mlb&w*7=VNkfJ1fI`ccf&=9jURmz%498Mu)p%C`TbVFt@8CbVVG zc@AZG%kzObCuO8-l?GA7Ek=g_%>9Z~@a6F;8f<~Y z=YNx+Io#*oZ*ROD_+)q3-(rePk_6iAuNYCROF(Q04L;HvHNpY&)qUgD2 ze@0Z=J6nGVjtJ zeyTvSFFMn~0o!n}dpVt8#jBl8WAY85b3ZP0Gyn8yqRG1Fu;=S|-mN;8digrN1;gA- z-|IyV`TNNb`;8_=TZUNd$LUoy5`FjwEC*fMW;GD3eoT(TBxPD4T@a1Akubvt1sWtF zyGLornS+wZmEWIR`M9i^r`wnbmxi$4Xmjk(itaVAG+gpSt^t;|>n*A@Vo+WOyqH9$ zw6lgmX_v=4R86OVHl-1_^u*1>wy6sHM_>vnuBr{^xqh0JNo2{{v=uHAreS8Cc_ufh|P+Et&VRq?Gs)a zk^Z@W$5!Lu_y%n$cZ(=*x+Hh0yb1}6s7hILSCGEX%~U=05H*M@i^o|45_9(rVaa~2 zq4;E7Q-1$&-#%?8JS(gfM^J8uIyH<$LN2YN;FK3f#~BVEmuIcE$H15Rmd|Z6jj?~e z?}E+ghg5P7_#g=J^|hbSiJd@FGEWUQ!5Kce!DddTU57Metes#M4K+)6QhK?`q={X< zjk~j^;z}tP3B!e+0hIjI|09?euWN6F1G!wdN^{R{a<&E1jPxcajkldiyQR!eBF4rS1q6`eS@~QdU8I;n)6wvlC(va= z;Dgn&9}Bh|b=$yDuHdW@^`d;9I0^lQY$eCS{ou;jI63XSXm-lIk9Cx&JO+Q?{>BY> zz4XCt*=S%MyV!NycA-SJqSxg3d-TlpV*0()xYp5rCC5-hpx61@k;*$^RuR&r(N2kq zQ0D%(31s{pOnIia>sl-tr#yy*N;1)Mp6*8q?!triMy-Z}NK4$}iTgGH&rm#(`hkjT z$XIf>(z$fHB5uc2z%iXcTNz;=i!n|uRuoK9L8O<4^Xa5`HeXT1?sF_$Abd5u;;C5> zeh{7@IfR)S-ze;@X8FGnZyuX2b)mo!~8*Y!okj=TY zcDqyY$zXIkhYC5<$YUT*NH-oaS-puZVd5J9nOV zH(C}#Z$&WaS5umohu>SE7@dR!-sa$u{WqW-wUp5%)cTl#jBUO`F;!t>QY7ceZM~ez zi|6%>Yn|UVsV%9ihoz!H;oPjt*@l7XzGDLflD*}}7oR*U=x+JeTF#&_nS zN2kF18`g!<23%IFQl%Slc?~(7js;?QB97_pMz>V6UKCj*vJ;;X#C_Dg?QquvHdspV z#Ar_j{Y2-g1$GS9*`{#mR;nDUQLwi+A4|aP)2LfPc<01}J}3`mpeAnC{v$abBO?h# z_uihT>!h7D3HeVqlV^DUjPrj1$(jERl4Cyq1Ck3ILU0lxCg7Ri@ajJ<=#Bhv$Qh%B zB8VB2g~=pxv>tDM$dd9^cz(th`dWfVbt04T(Y}JnEpLg6EH6qpIFzC%Qn4B$R+8S^ zJlNr2G=jQ?DgK?h?0r}i*hcs5dcZJsNrrvv}#(} zMCZN;9B!GyaMWsMM~cdwVlo1rDE1cU#Hqu;P=xPw+8h}O9(!??{q^>Wmm>_pu!oJL z$TuDQ9FYj$u*4oz80wL4QHYPBIp@+p{DwI2rrd}9M%YYVsxP2>1Z=s+A%>4`Wwq$4 ztQ=MN=D@Q5LH%tBwO*-7{xuZ!wbSkFnn2Jtci>eJjvY_m7%`3ARTCOtS3V4u+^`SI zcMr1AIQ7@Egd;nxj`?$kJi=s5DOOYD3QC^D4 zlORdvDaNE6AB9QBvE{ASW{PTIaTgdCLQVQ*9L1(>&g&maZca;D2G>rZ3F)wZq(`M{ zhq*zlo+JeMDo6|X%qQS8bBc2(&jr=mt>aB`f=WyzLb(?qNwj*OA8w8_6>E`fAX2+0 zIuAVAm(v@C?#?qg74nG0S}6tR%3MfuOl0K_7aIE>qSeGI3+LY8`(SkPk1`NG_wy>E zFpc+j*daIz!n?=jIX4eopPfO0bXFvSkM6(|@cq6ndJYhf*H*tbe^sU!PGf`nc^4rD zspQf4%dmh)TLyPrBb@RF)U;4Z>wLKwG_nmVgQsqoYL+>#?*pP`#38JCiuu82P!Ads zyPl!F`dO(TeW>*k%_k?JI{X45>JNzc=QuyA@d6~?dHh1LE6w}U9{@x}vGFuhHxWP% z0poA9!vpvK={94CrT3X47zs=1Dd@EOc3WN7kM1{=&TitZ=X1!>!Q=gvm&}%Sn?-)K zzfI6j`PGylwDne>At!T{ zJo5&}o=3?27o8Ohoi7Fur3E~dnA;xiVMn&s?R%NrV+iQ%yCEqX-r#PpFCJgz$W0sWIN|E^9PV{GYGbSr(TJ& z_^c;@FR^taABIV4a#C3<-R^X7oz@jX^2@e$v!-P)RExW88uh#fYIi@OKMZ<5ZXcK< zj|LAUKQ4`xAF2zgz~(0>aF&!{@s zZ1?DvHiZ3Zh{@zWe&4Y7oIk)Ee-*)vslgfGy+JC^Qi2JLXFr=0zf*lZ zVcXh828cVEux{RG>9>se7xah zK<|BiSd2ctW7KJ0OE=QwAQxO&@X0aIo;2Tq?m)AJa+_(fK-bs4{7bEdg8Nl<7*d(G z?0?di&xW2`4u3SP3#H)?x-El)FmnyJOAVGo3PgQAhSvJJgCS04PuqTc`mTr0RdAa~ zllCqB+IFN1^|oq3=m zpHSC~(ihs6iD%L-QsRJ%ts6Fk#@u?+GWF(j8b;$6@?YMD#DCV;<8VM-X*1iv^89L6 zlmc=AE)_c!`n^3i8*OasLP(e;srsIpe)k6f{C9o96&4NNx99ARzg|r4jo0rx84taW zvi<+Y--NC-ztw9Mb!EZZy}6H$*W{{13$qw*q~CG?B1iN2@^>Nq-$hGiP*vO88BG|x zd?2=$n#uGow*)YJFa5~nc_;XUXX6hI0k>V&mrO`fY`B7W^Nmm<=?z1Vxr_`|bJkjk z`b{Y67z(NS>{(5ilIubR-==C-1)0*>b#TaQ~yjh zx4bQ7itqD1{-V7V%en8~6!)Tk|3h~@%W2>Y+Q`kk=>$KLqzBs^g_FmhQMw_}dGZF<(x5??tf$isY`h(#|4j zbtM2pe@C+(Ff(8ueLv;qp>#A%yacurxS$j^^zJ8Oyal3(XO0_R)2EtGc8$=Q0EiJ8 z0`^t*AHHAQ&4VtdqF3D)p0A1VD<5*+5iC}#%d0YJoNcr#LoQyBA8#kYrq5Wv_X?0- z#!i{Fp7199hU*=BK?Cy-{zF4pUkFc+n0%00HXn3PUu>gqZ+(=~JkTvhv$^cIbVvir zxQV$gnIjl+%85dD;xrX@3`D57P z@#WfGQ^%LmVYODq`BkLW8M4FJcPf{0j9$DN6Gu~Ej)av+pGX0?f~5n}`6_NBKj}T_>ZQREY;0 z%Z|6%NY*<=9rJBJ1YVm)?n7_Yn(vTrM0l!trA{@Dlwf~11f55}G8|S2{OVQ~uP3`I zL;w-lxu8zJEWL7DVQlQ&ELnzoH&PdjPD{tdd&8!G~zr`vdC!OWbRupk^@&+Vg*Kb6P|5Iu+G z++A%`&vd=z`I!%k;$&AU=QT66&rPB+x=c9|TJV}g18I0FlW)e@yqBoqmDySlOyJf+ zOCf_reCJ1hmqi?(%UtXLcMesYxpe&4cw*iR^aDii*d2I|jJ8&@V*V`M;kbXv% z8D3r0;#)kew ze|7YAKEEisFO;|%$Q@Q=?qqOT0Yw#px{u(c+46a9ASGNrFK0EIQS~CxjTG*D_kIcGabA z`E6J)0#LTt0&9df@~%BMp0cFoZcYuk7yCr4N0ftErL-;$2u;7sEG*Yss%uObFGi3yDnnN;Pq+H2RSE;fhcguJv_#jfRZkh zH?)^^=QNGmJ|Y};i@LjH1dL&r+ujE!s$Q|y3Q0kU$UU{`_?F82dn~xS22abr;kLc` zX3MYW{BN;hHJg46ItHF`uZMv{fJi@m-N~D{&X1(DY`ytoS^%Jzsdxp40BTYywG#uCqaj-G-KEijEo**KkLHe9vpzsdj?77*Rq8bC2fTrB3Jgof}r%uRp=gH6^& ztm>UG>M_stlh7^B=Mg%r-+mJy^cL={0Nks-uu`L0S?Y1q2ahGCxgt1nJCKXR-Gz+i z@5lKx+-na>{v z1#Zi{uN@2n!N#%G->E)pe_JoHC0^pb&CyJM`HjK7sMe3S9tab1#@>od&^9_`4|B*Z z>_`<$(sT;WfLH8HRo4*b}@?r`28O4VDY zqbS%2oX6f6xjmR<;W&t-?UK#GT}8Bad$c={5=+uos0s}uWKBz80rD*>@ABnPTt&;w z#RME`^)#chBdY%D>x2i4*A=+Gb8DIV2tfm zuedjSG{#xn^LDZVfV-)eGYpS<-AE)i4^QZyRl0q+hyiI0WX`S^G?p8NG6$yt(1-8_ z<=I#~c@_#P`riW|^LEAk5_7Vu%8kC|#fHlvGa{r2 zq18Mh(2i~ImnRvP)xjeT84^j^br=?^^y+F@jEG%oa4gbd=38b_IIC@rdn!aM zD}$U}X;>_=*S9kb0Jww<2zSL0f>`bIn`_p6?2f}xAZpe)i-4Su(ck;ho<620edd$T zUHFL*0Jp99x6Gosx%?qZ>Q+8(dVU9{5DM{Act-u(;f|9J`wRx|C#%vBfo?l87K^5y zKzy3V_N&A40{SZQ_4^4H&Xt;8IcU_$l(JCy)&Al00)9c`XMDiQ>mWk7m z^Uco6R2i*X_!j(L3_TUA*pL&hiU~<_S!H_y%+NJ?fLfluBV~&QTkLo=P6yW{fbMAZ z3ch9JY}Wt6xY-3VQ;X|0Y(h6jk_$rL5nsLi&BH%o=lyUCE`3%uDA}7jlE@ngeYzd? z#|7OkDj<$fGb?F>QZ3#fQrN%~<#gGSX%OL&7SI2lM7@ui5mK)W>xG_oF0BW!Rh!4< zoK%l{_;v`vLbu5WKefRR==L|LE8Gr z=kj|t8e&9XznXo2AR`cF@1foDbai3npzi-|c5WLVO zN|-a_Iy>2TJak_mS&=y3Y`A>(>Eex@I>JVzI3*782r_7PS!;+EDpURIah~O{+Hv_1 zn4sjZ+Jo$SvukVqIDT$)Lt*z=64&b+>t`--Akh5-Y&_KLNfd%|rJaq9)$bnP4qvXiYvcw#g&V8^ zno)p`VvR+&ppnm3@pWFZHhtDOKAKnE#z$b{b6$%1C9vqO!H2-BxFmPF^GkKq|RB!K7}^NFZ6eu<;EG=e)FI3afUd^%Sc zM&B^tM2k7~3PrEP;*GfAu8w)2ui5rXN#hZP6MUqDfbU0MTx&SFYg)bMeWBbkUcp77 z-CHcz*eQKPQ*W6acGr#y!C9raueh|5NSw<2uGgc@YGI-6MA5W(`455Y7zF&)>-yF! zRjN-y8_sd`hZCR&I8ID92tMs){fg#dh3bN}G3u$%{-4JLznf^e158{^Dk|#Z_h43i zt4O|96Vq5k|FvGM$a6SLl^~M$K^uT4R1wcx_Ud_zPdr0is)t3})7PP>L+~4QPD);09Yg9=y#OF4m&-Ryd*Fiz zPxwz=-&(ka>0~wwqWtkyH&0^y&6;@#n&y zBWRu>{7qtQ=%<_=cS&rEI`P*0H={TE`>THtp|7EM;yWmzC1k0zGy{TpyC=l5K?&=9 zxszE5kSLx-?luDw&^X5Ht&28c&uM@wYQ5$^~|gy zS^TU@AAfJ?J=w52y)iwfk?)9d!s6i6u%ek)hN(VX+j}8V0sy5~-ChtSU)E%(g_^w| zJmb}HrhVB(J1T}xe5Y8V4+36eT)qBWD4Z@}Gq~8NHskd^5BR9^r zA8cum;R>x2#J$S*nEK;ZwU5->Qrgw&H|@jXJokFEeiy5;R}pMkJwi1e0eDF){yzap zG0`fwWV`ZiCrzDB#NrlZnQ+RQacYijSV+lp{j8wai1H%(<$60G!Sh#*AD4w^e;kqW zIB85>U5uPlkaA86Z(_+&5C}ORqPvDXjtIpnE?fm!!k$!5!?`lH#TWqE7As=Mpx)ik zpO>c#GbZf>ef9wq45#puQNn@Vh4I1Tva5ES_>wg{uT}(qm=_L$qu-Vy0T(;IR$zv$ zm|}736Y1z}zu$m;1dO2|xk%pUAv7<{1OO9rP0Nf34vj&l`M`_L9ztqP!9wf7d+_*v zqppTz$}*UNB*f3G(B#`3#kzm%6SAih@b$ekOKG!qB8j=f_9XTObS8tBi&3OXV37|5 z_RSm@p!(13dU5#!Mwdk+fOutfygq&ak2}&!!(8MZ)itpU`OMSVKs;G2hcyq6;*)3I zG(1GIOh}ZCNYRhg)!yTg;6y|~03nc80o@`*%0{QE;!1dvG>33>gYW>KB&@7ZtlLa) znKZGn9y2C9{1Oo~3rI})ftm*j3d#RJw!k4>@9_;|)M=CIN#jv=l`;mztUT%os5sQm zB?~;4zrL~nge9-vW1A0$dk&*^Js_6mEgmrgNGP86c1bKkc8~ZUKYsjG-6xU%D?}K? zhCR#lyxJeMBp+L^Pw0p|P~I^pdeS z6ROjvm=OC%O`G;uZ!(4j$j}T)XSY0W1@6H?i1fPMT3qROs|bXyD^lgkrLjqL$`X;d zbOpRcY&GUHWbLcn}SW zOS9W^f$w!pBE`s<1hlqV%?Ty48RI~4$AOqr18*FpqrH_#y;6;7rQIejqZLRc&zBq1 zaFMAp{v|ywdWmoNxG=Ld?aP&l7ajc2T!mk)OT3RtWlX-`_AQ-{A?8j|*Z0%>*f6tshCczb9O!xwc75%V&I{oxS8k2vJ%%zcGs`hGx}6aTP&dv9z39m5cbU|aB@zxB z^Dc{uA!-9Obg^osT18ZBP2O-aJzfG^jbSi=5mX|}lh}GBAVgkOQi;^E?8AJT8c3Xr z$xlZM@~XTQK1niOJ_TYaF)D-4eO($z6L8B1_D|YnAE>Fv2jln|0g2p-@#Bgxc^~md z8wKA^s;{YK&&#>~YQB6NTgS-U*m%r_gtI-`b;93u*X#&59`wo;`hAz)pfCoT!h!~P zuh5*NHoaVw#Sv$OZo9}pz2V^~?R&9&{79k*_B3hPYVk$ zTTu<9rX}kQaoaL(&=SZjc^Z{5$~8dKTu7a`oCAxq~cM(q&hC7it4d(3nEk9iVKg z+#ud?gPvWUJ>JvX+N?E*GpQF2H=`Ld-p2VqE8~e$tgHg~NwqhWbJ#GW`8WIQ{n_hD zvWEpxD?Ok91uePmm4*3C)*M|zS=l9Tvh%X5#tJE>-T4}avOor%p3O>2_KCa2iXI+| zRupWhKff*_F9T!P{ay@F74W?-*u07J6XI!?3oqED$+mGP`kJaaqrcL1K(Y;y_8>WY(#E=UGJd zk@W9p?_->gk4#R8m6wc1{z|l#+Csu*A80xxz>YGY22ttm; zw?A1`EQW=JuO^k{c$OvRE(aL&^9OGDq4TF6%N>*Oa2m!WXkmhQTy?fL53y>7iONPP(Z(uuF)0#DXX2yj5 zK*b~eamY>yL!vHAbs~jH)&FJdNso`ygbglxo0eQvuS7nxrsZ!MQf(cmGolAN zils+k&;ub>w^FTIbz($b5cOA4pe0FFEin1ds(%j>B7_5j55tbc5*#|W>1yo^`# zS|=VJUb)L+rMr{wZm);M-hpMU0U7pWt1CsR@dD3KAc$JTPAik z>qq<=I=DB_O)%3tq@ftcD$9L#x=E#vi4wf}S)AKymq5zrUU8e|dc5!Fqx&iY$;MbT@33 znU)qg4@6Jx4$nayPOx2F&M(hTZ*t@Fc#Ep-{Ycp@;$@yeI7-D|Oa(&2%XVmWJEB!8 zwPe35e|J8pUh=S{&7lR7o@@ft{@)%Da}9Gv8^1`)CL~v?eYLU4Iu&vdR#H}8y1P7> zn22#bkpeAO7kRhVydh1#7i6TM824s$`T&3!l)rvPEf%OHQTwaD-ADPqsRaVY^;U3v z$SeFYfFi8Gw2TID+Y$XgWSI-Eq@K^QY4*7ONE~^p{o!#nCjqBT!+rbv zy(kja_p~&EzeRwTgZt8PAQg*G6nVZJ_BlSE&M4cxAQfbL+H(csrl)B_m;fyz%GKm8tF`d9a`t?aMdbJUq~1`9vdcHf1;Tda3;JVtL(uB2|C- z`?jC+$ia>6`nWEo#Hepg0GHjV1|gSI(e?4t*OQfMnp76u@1kK?m7d)8vB^n`D(&UI z-pwRH<)q{3s-h=2`1S)6lWZJn^%L^VSCe$9a=q>jvP7X2#(R;{&y-cp7T_*u&g9EE zUviCJbq2q~%O}hAk{F^JvxUQ{OF%^d@a>{jcQ@Mh$;jkw`o(lFryI@6Na!T*Ug0w) z{i|XQQP$DP^5KDgO(y$fkcTFyW~K#XCyT|eQs`|zP*aWnbhTFfxzbRr-JG&J7+UG# zhw6&SWJ4UnsMo+rnZ{+ep`qt{e_#_#Xh1d@WeNl8>$nv4^6HSue#lrX*Ottlg@G(z*mWNlLYeUE|OKd9oe^_fWp(B&NLN|L1jiA)e1&jOHU)$FjbY!NvL z8;V-$y0-)r*Yqej*ax9?=oZ^d8t=x&#^?piuDg};KO-h%BBb;;1h^Vzdm_@@019L2 zYOUsdKoq)S>|6o}OX(G=HQJTp#^?cWPy#qcyUr&!;4m_3q`5=W!qIYpM^>&>Df%vE zLQY&8zZ@Z?4Dt3=aj^WAaF%qjisyJf5Ypi(euMk|B%oG?)pu9h`>chi*63&7 z&V?H%`0=7%W<8LE2|I!1KcEGbe)FOvQ$>itj8ubWIU664uTm6B#LSEKcm7qM#zjS` z`gRg>n2(NPQ$iHfP|V~7u9Bj2S@pbbPneYTGb=iNh)P%FBf&+}WEydg{bnmv^_om$ zvm)H*ttUg^D^#1(y1d&vAK&IXZxiKu6y|o5S?=g1iV9le-YqcF|!vswFXk)DS(ws8H06bQamrqX&^cjb9eo(moKiYn8-SYWb{pu?3Wt^e53|!~bd~2jiQTjzNp(^7-n{RKU{hD!Mep<)+1Z=HI zgVq_~b~8zs{U#!kzE+vXTsO>hbN6sM-@OdRm?dMq&M+9w4TXMdGFM1{YOfq|aYrOr zm30m9-f?49`rCfIuCfy;=Vm{6#uVSv5MpEJhi%BP^1D5@o#5b6TbR87uE69Z8t^-2 z>1NfHH_d=8>k2cdo$_Wo;R_*6bYOxy24(+x?U&w^U_#h~DgW-`pllWksSSlyR_-?op;rz(~PnJmC+rAMT+ftRF&$AV&<7}$1twXWrY3k4K8hkpQ_-D?eM z*$l?*4kvA-$ltRBKM?rjWeVfM=eOW-JkY4iSY9=w0Tr>h*!=?T+m!yE`*SkcftiJ8 z3l1F3yZ^38Xc(HLWM|Zj-?L}s3G!eSJP7?3`E4N{WhAmJB_=La0b)_DP7>YpS0PY) zNRgsyD-u;fmnwBbv`U2vkFvRGu9+{pl-mTaEC95A&-On(4mE>TCIGR43NK9^<1(yS z_AYuq>QF#i1d;54Vv>cH@I#R4tkE1!6|!BGvLqxT9IXO4QocGg276b89Io^Z%cj*J zbo16uCzG}nP~wcBB8C8e^M|HEb_)fgpwy)ePiNJ7(<8ZTNLtq5O_m&Aby#@#X9h-i zt-axxB6F-;*@3TrWJc_JRgZ-Y2pL~#;4dE|Y<70szJg0?X57T*jFGG(^g2>-eIbC$ z^pwF1+5=<@1LpO`y|aH1)=lkJTebsa#c-p6K?U5Vr%UQ}Uu{7zAtW#IvHumF@gQa# z30)qAsq)6_;GOdf%1(YwVO9*Z-FVt70yLo=?ZCfEVD_rnK__Gv1|A@67Z(?o*Nrn{h@01B;Ln{`(|kpqjb>J>c9qu8wLK47en0nXSm+hI+W22i zIp!xP9>v7&VT(T;a2MKOiA-`YWPFL$GC;0cseHYopW5o7JJPIW3wh{i~pX1s5V6t&?ALC|O z85!~8?b=+OPyQmCTv#3xARseKCHP*%RZ4sNk=JDQUHan+P0<%Tb8bVbfrkCNOuF5#|H9oUWJt zF!qxiq0txqi*H?!oAt|M^V~`R4h?cn275+?4~zj2$z?p*?O^#aAt|Zo;b=Nr*|+rH ztm1!wlF;^KCJj)?B2}Z;T`_W}1{6#a@*T#p0~`JiK;;2S!GW@FdVm^PG!DA$H0(`O ztJXTsf8_eN_-}~U_2jZj?nnwO=<7=F|LzuYklMK4y1n7p?z`UNv=WZ(l*?ccL(Hcp z5ko@f>jnA#;0hW$_?o&uyE|yLHI->hMa89RXSJ=zBq^p6pErFPMW{at*d9=SEw!5R z4ugm@-pO?0nC;$t>^r#&J?B8j=tZ{swlc(60U&uipV!Ry*UWQL^t$a*fCB5XECPVz z!CYX)Iz$9`1BK^xpf2n$7J)=a?eC8*MpFzb@cI0%ljHfiiqtgpHD&w}+@pL96nITk zvh*_At~V(T_WVyY^G5Eca%m6sU-cx7qf4BukIZt7ZsF0N8Rr?H(iBqfZ4H58zN&Sf zx4}r*Jc|Dt@ zCQpGL$d7frR^t`X%6L~_WBRG$fJX)VsDQBB#aUyyo^FrtK{>;wnL~M$2JeQ-G%cS+ z4N2LN{n0f8Y+@ZdT9FiX>+S!lYH|oh8)-vALOiU@jyTso9?{s4)P)6KPXz9T#W6X3 zX4ZFFd(gV#Zv;%S7c<9rtn>CO1Bru&5u?UuZoqUYriXyomMgzme^fN{Q*<`!W?zQK zPj*UiqmO%=Bad?L-0H|-qubZ4uV;6SeM`4EtFQ4&2bguWbk>8Gr75*bzdL8XI!6;C z-Wz74zeIHF910nKxpC|;DX=L3FPms0x?;)m5<)+qF85u`uGP7Nc{I z$N16wE}O&Jg7qZ%)P)1&%D4X$PiN%;?zJUu5OaF{$@fQF$R)qpz(Nb40~AX99!V?T zPqN+?)?KXc@x8#=OG05fi{d?{B<=`WJ%7r_7Aiw(c!`lieZLJUajQ{)g|@3iFe+rgv)nebKCcW;eF3lQJ~_9oMy>)4YM{N^t5cR7kb{@sXLc_8pzViTJ-l#sL! z#J6)p?>hhZSDDnTyuFJMe$wafKWn$0)yk1%6ETJAGzk=u`~dD1#oo%H=TWgWTR7x5 zA}2Dozk?3RDw9>Ra+y-;=kMSAzpBWY+Ufr3;U;_rI0esdNacA_ifhrAW8b&2 z|4Ar?ya4hhYHDTSr!wv>1FWN$PV

N)V@4(|gbrT=?hPne>{GeAMn^&tPZ2N;AT{ z*d3Zr23YBPms*tiUuNXCEME5q|11iqzV+ZsHe8uE7!j48sJa)JBRCAn9>k?m!@d66 z^m|@+FjsdrrDa=FJ#ht==?VJEH_6}3t87kb5k1` z=Dl2(p0An@n2BCpi)t30oO2A29LK8xv&@hPS4igh*{nlImqTnnJMF$R!J63vrUJ!ORlJ4K z5A|z+8fh-;ktAc^p#>Z{F?>-n__lMtk>Z-w|?k?TlJ^YzCPyiSx3Cr;b(-W=x+w*I&owv)O-5?XrP?+63YEZk<@gMh=VNHk@O{CYV8H|SMKo!*yW35CGR zV=aH0dNbvm`K67l**9l@e{z*5nwVCkaBsHB6Gn_O`>I2^Z+!7~x32l@78>#@@SM7& z-hPfzW9}vMkcQayt1nN(spQ_;VSAsoUz?~jmQ8T~B7S0#K_N&65(L9og}k^m6ndEV0-)taoTC;whfEPgUc)yK<|V8qMJJb&f${O8adJ-ibS$I)PHNRpbh}!6!(80 zOTu(!{BK!0-{pI|8h=9fbi8-6Li8YqEP1B>RQI!t$=NlE-(*06Wfyci0c!(2 zQgYvBC&=l?5@iTCH6y)Zvo1#hPi{dYu^*SG>F>x#CiQEwZ%)eSTOy%uqqU|8!5dl! z%@Fa}xS>_6i3y&F_#Bzte0fkP-3}SLUs1in5IV1;4M`+ZTd3|tZhGv@?Jhg)umg>B zwU(oMZ87NhhWw?F=es_=k8&iY`(42q_t$@pda(IabPbyJhjLZ9EON(ZIKX)<)yf;? z+Y~O3^=H$obq?s2H9JCoMd?>L7SPDa#oCD!%EWPUt3{JmaoCmTWwsRRNIWP*$SC#N z5{}txz~z0;pNLy7H|kWm;Vtxp1n=PcP2_5r%K`R@$J*W2Sbb78=$h$GTKv>gdt+ z2uzrWoRNOW6lkI!ZtKzl+`M~XX$V6bDqf55e{~^(oY%pL8d-l=Y1QB%Xgqu$hZFe* zR80GI8nsIak*;l?wT{c0hbB7aW4Dnb^e+;u5gD9%D?OHt`Nm>xHatCRnKUy7=gCov zX=ewbTdxN@>&PhXu{H6vt|xFSEr*Ej^*%QUEGEFcr`YB*pmP|RHj~7jX@<*>+br~j z1b(SvT|^Q}`FlmnsStzudh@8DDq^Aq~EIcgi-qh-ZlC6-E zYQXn-K9d=Ugi8|OZ+6bveseYt^bIoH8S^|TGDVp#6EJ8Ubl~F`A+xcr|7_*`V%+Gc zP+(?7Q+9^pefvAfE@{ISP-squ81dhVlYhSxWSN0oQEtn^q3958aW=Sbu>Jhp*YT*F zq25%!Q1!FX*NKHRzU9`brU!JbPCw69S1)PHKvnNu`r~p9CJIi|&LHKNWup7-BITI6 zA<&rD`67Q-xsK}fQNiasutZMkB0Ty838Wq}Tz=rzkLA>h`Up! zlbzPSO2eBaLxHM7@Fp4Y)Ak(@ucZqzEXb1$eVgp*AH#g71l+!t1uaBv_vK|cg&*lj zo7I1;-Ru*{B6I`(SsB#THkL=P5v9RI%2GB~w648UTh`XNIUP8v(3L#?NT5SRc}{L0q@Kd&ZB z5%b&D*AfE37sRx6voBxKZLS%Yz|4nkH22#BPEahiKjE*a$&ObLzAHz6; z&eWmvn$7%T3PDh4+frQBqlb}N*db~i6cwV94zDRHJ%?fGP5kg+&9>7T%|*4RB)^#1 zCf6GK4sx3NnrDT~x#2^izw1h+1C$s#d@b5diOek4NuTjnYc|b~-eV>B;0ye{++&V* z9VfDv*Mlu9e-Zv{{fH5w+TZB3-rhT#FPiP_Eo)j+Oi5&={vt#gPGbT zs0IzsEyQd#ttCN3cR+AzEzj(ewEH!c?N2^bs-b>U1}I1kfvQD_jUVMt8GjDzCmh4} zxwv>()_rs0U)kcZP=v_+(X{jeku3*`1T5Puy9t~l$G5KGR^3yPkuJ+pWiuAGhBWy` zbZw0&WICP<1WrtVk18;$efZ4rQ%-|d*7h4R&b3m?Cf368woKgKtgTfu#kB*o#FD!? zsHiq64Nq&;52j~R9Q6Tu5bU^a?IOd2`7BPN;+waW23fMY=ns>bZoWVimIy^vJFEMn zDqg8Sd-v-Z?68(+^-bdocHPD}fucq49@%$=&l=u2YhWNq9)8cjWgS_MjQMa{4zR9sSm(dr>pznjPgW%65fRTn5~;oYvm6tPmc9Xh8oRlKv1LqU$|u@h^9+@7%hxMdkD<}$t9~{ezF8;<5z2ZN zooU)uhohczgxuL1*WHV=uArspjn`c3h}G-QXo;$v4WO>;R=Cv8^Tuz>1Q)x__EKbO z%4h;Txp((_-u75Y6eNW0-q~DGkUW9}X>deOj2@NxK41`oL8d*NHnz5{2{1gGr=8h3 zzn!cq&VJV>vo5mp{k)^*OB)Kh1@&TS$Ni`mI0G+Hx=lIgsJfKjZTW9^EJZti2b|h# zM1Jmp0-9qXOjzgf?A5O`q~i%^H5%&5?|SDqxt{Y@!>^o@-|fSS6e7)H?0hjMGI-*k z^4-x=wK^un%qNXFq1DUdnkR|5J9j>X&f^!J1uHa$6ko?+g?I6v zT?QW7aqT07x%4D{gfqgR2^tVI3GqfTI;YzDY%sf<2;CB}%-z5pI4Ik{afmb7i&KVl zu~63?NcK6Z@ghun%X(oQq4)lmok!%@r?==aZiUUypUyIUTKE>zEgq`sAMdJ`?~;?M zZ7vKQ*lu4>N))>g0}A|GOr_;avAQ;dFGq_=nDA6Ic5@?<9WE;^?GAW9IB_H`hhMFZ zox05)f2p6_w_^rOERK$Ho_vtDQqx~tGZtmjL>0PN=;)4|w@b&X?F+ud?m7-jd``7I zXu)v5##~|l{y>H;y2KQyx62Alw2g*wSXjUe02Kc(*{T1d=fV~Xn1-by*ZY#4AxnSh zn@#%6S3F^(_OYZY${Is#hhP>HFrF|If;Mpl>c%{6deiea|8dI%)cr-TDFMT1mLGAe zJB9!#yLr6g)13JA1949n2~kWv9W{5GogJ5D+1zR0@vc)si)RLg#LSrXklq`a(7yRB zZlP(ikepiCp_FHK}La^|#*7tw6J!fcb-kPfnIor?hE@oc&S4WwNV_FrCO<*=5 z8G+T%0CV)byRnXk;UWUL$qL3pW({s)8M7|Sb=R-rUy?Rf06n13S4wSB27|7?O2a{F z&cmJ3WazMyvE5c4w{OLkuAsy6rqve{&`GyD)xEBeMZTBMO^ zjuesgI9x5!kPd)gcbJEH$ME@!$i<4l_O-UWClA7C9MglhHs|yMUF;c^5WRgsBOb71 zMCor(p&O*doYFu3aAoOk%`MvbM2i^tIB2TSn;0gffKGN zALI&S9pMLL>nm_>yozw+i;JS+GAgc1XE%GyKr*pLbvWp@wv?Y=JK|xqaAI35o)zoa z;72_5fpNhXy1h4WMBF?t^`m1o?EV4yL0jFq?9!Y;dBRDVA*{ryCWXh&E$7V5)(;gi z#Y(GpiZ5A^3f*SxGP*Pt#*Mj8%93YXJz3_}qJ%dh>piA%&OLq`#XKr_NpHWFH+t4#a{VGl9HQ7#~JW=DWjA~N}R zMufjZ)!zb}Aggj63zxOfzI*lPMd)WmFTy~z%NI^qnikJ}OT74b%9dpJETNZ(*sK+Y zm501%UE3LG`B|N5Gls<1XrfcmjO&>QL|v;DiPIkme!rt?se8` zdIv0Dn%J37{Yv*K=u^|6*mvfg`G|@^_w6(Ddak3=**oHTw*JQmqNVWyfN4BA=cUUG z*JF$bhtSi+S&Cw_p{={a0l^dZfTpkMDL$KK6@)#W8>Uw5hJOS57NJ#;Wy z;QGb*a)LTKOO4dqzlaODDb+F?i@(gwJnh6OTQQV z0v-}WJ_V0UaUr-zcKi@~b(QPC@mfhBbcN0}sZDn^i#6FGF@!gI=rO18LMJ^NFV%gT zj0#cc8Pu#>E+XNNJFIOquGp!;?RQPU;)Y-Bi2^-OPmTWYQ1UdvrqGixIU8!_vr+dd zLR>srrwlN6Ev!{V-Q{$%SXIoby4=_PBi!(gbx)2Hv9{kq(< zn#(Ygg?9V&&RcSUP+Sgyf=+LlFpb^wZ?gUMHM97>=aHS!3qq};zpm_uv0>+Lcd!Cm zORbq;KUON~$$u?@PQZ~x0o7E*RPKOd6MPm+UCCVN>$==S(crQg1K`(xCjp1i45SHz z+m*ERMR)i(sf)xT&j(5ce{J#0Hz6gZ_#aFCd=qm1@dB*sJ?&oZ!U~!Xm=y% zG2g|qoon`rNqZ%Q*~~pq>vd8zwG`Pm@~~_{vAN1r zAq{gRPETwk-gYT55?QpIyTV=w*(X$+AIno>7AQ*mwWz{+jCkT`NaYW3{82k@d*Jl# z9p9_eLUvJ2o*+90NT8^jgg41|q{?t>mX=JkTkJ~HM3Cx{i~ux#mG4UB6+T9uC3y*) z8Z+$=X5nDJn70{tbX!(*B>Zcw;k{@W6a#>!xc%^u`RdX;Jt7NunO-ynG@<)TruVQh zkZP+A{hbZ;V54O#2^@hjy*isBo7oyQunVSw&kb}dXJ_xdpkp{xdHMNWhjTnJ^gep! zKq5vD&C^6WKkh$1W>mqZnqTb^f8YEobh-EVrZE80w+hp)xTY;SugRTU{56P;A;1G4 z4mM}FR{Aqzlal1si=VSU4ggpiHF^0^PVF6kPJ`y>>5ID>8CM0Ef6q$_d!Ho%q>tVU z4%l~N@brK*8--waD=JI0O~?S7DlhMDZ?l)@@^pg>J3R#p__09Zft14J<=P%NXOq?M zEKW;ZMvos{F6&D8he3FHK*s(qJG&Y;;v?tapql!0JOB;n_^qr@@K;$bJ_U)c%AWWI z8vUqS$-G8C>_yVI{^{`r9Lv3lW0+$QPZxDpud`QC<#Bx4ts@D`!cI}EwSpDFTV8|m zhyi-ElvO6|r?#;o_?9s7m2$Yg6{(xk;F%wJE+g&q`*SRSn{Mg?i6WN=5WMt>eiv+xDij~ z8^0L1c{#$Bl?R?lOMY-#p6tC7c8fG2?%^BgA`3|R#qC143q>l23RFs%I>&nuUI0vjIp?GEgY(JUuzx(ULmH2qr{TXs{}U8n|31%oG-T#=JemM)Vr G^!pDokVU2d literal 0 HcmV?d00001 diff --git a/doc/user_guide/images/his/timestamp_statuscode.png b/doc/user_guide/images/his/timestamp_statuscode.png new file mode 100644 index 0000000000000000000000000000000000000000..2f6cf34691805b939cb6a1f3c76ef4d26534b434 GIT binary patch literal 14431 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 literal 0 HcmV?d00001 From d537e6d9bbd2ee29931edef45414897acb60c841 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Mon, 26 Aug 2024 18:57:03 +0530 Subject: [PATCH 60/71] doc: req user guide --- doc/user_guide/images/req/req_tooltip.png | Bin 0 -> 14618 bytes doc/user_guide/req_user_guide.md | 5 +++++ 2 files changed, 5 insertions(+) create mode 100644 doc/user_guide/images/req/req_tooltip.png create mode 100644 doc/user_guide/req_user_guide.md diff --git a/doc/user_guide/images/req/req_tooltip.png b/doc/user_guide/images/req/req_tooltip.png new file mode 100644 index 0000000000000000000000000000000000000000..a38bd658f3fcb1241ef88f9c1ae979e47f04e876 GIT binary patch literal 14618 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 literal 0 HcmV?d00001 diff --git a/doc/user_guide/req_user_guide.md b/doc/user_guide/req_user_guide.md new file mode 100644 index 00000000..526c57b3 --- /dev/null +++ b/doc/user_guide/req_user_guide.md @@ -0,0 +1,5 @@ +# Request Editor + +![Image](./images/req/req_tooltip.png) + +Hovering on Desktop or long-pressing on Mobile displays a tooltip with the full request name for requests with long names. From 4ba39a334dfa5ef7bcb6e08b1b807dd9e07d83db Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Mon, 26 Aug 2024 19:12:13 +0530 Subject: [PATCH 61/71] doc: integration testing --- doc/dev_guide/integration_testing.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 doc/dev_guide/integration_testing.md diff --git a/doc/dev_guide/integration_testing.md b/doc/dev_guide/integration_testing.md new file mode 100644 index 00000000..5d8ff32e --- /dev/null +++ b/doc/dev_guide/integration_testing.md @@ -0,0 +1,15 @@ +# Integration Testing + +## Running Integration tests + +Integration tests can be run by calling the runner file with specific platform + +```shell +flutter test .\integration_test\runner.dart -d +``` + +Example: + +```shell +flutter test .\integration_test\runner.dart -d windows +``` From fd205f842164692e4166014199306b0c4bf6af92 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Mon, 26 Aug 2024 19:33:32 +0530 Subject: [PATCH 62/71] fix: revert README changes --- README.md | 168 +++++++++++++++++++++++++++--------------------------- 1 file changed, 84 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 431578f8..2da72ff9 100644 --- a/README.md +++ b/README.md @@ -111,36 +111,36 @@ API Dash can be downloaded from the links below: API Dash currently supports API integration code generation for the following languages/libraries. -| Language | Library | Comment/Issues | -| ---------------------- | ----------------- | -------------- | -| cURL | | | -| HAR | | | -| C | `libcurl` | | -| C# | `HttpClient` | | -| C# | `RestSharp` | | -| Dart | `http` | | -| Dart | `dio` | | -| Go | `net/http` | | -| JavaScript | `axios` | | -| JavaScript | `fetch` | | -| JavaScript (`node.js`) | `axios` | | -| JavaScript (`node.js`) | `fetch` | | -| Python | `requests` | | -| Python | `http.client` | | -| Kotlin | `okhttp3` | | -| Ruby | `faraday` | | -| Ruby | `net/http` | | -| Rust | `reqwest` | | -| Rust | `ureq` | | -| Rust | `Actix Client` | | -| Java | `asynchttpclient` | | -| Java | `HttpClient` | | -| Java | `okhttp3` | | -| Java | `Unirest` | | -| Julia | `HTTP` | | -| PHP | `curl` | | -| PHP | `guzzle` | | -| PHP | `HTTPlug` | | +| Language | Library | Comment/Issues | +| ---------------------- | ------------- | ------- | +| cURL | | | +| HAR | | | +| C | `libcurl` | | +| C# | `HttpClient` | | +| C# | `RestSharp` | | +| Dart | `http` | | +| Dart | `dio` | | +| Go | `net/http` | | +| JavaScript | `axios` | | +| JavaScript | `fetch` | | +| JavaScript (`node.js`) | `axios` | | +| JavaScript (`node.js`) | `fetch` | | +| Python | `requests` | | +| Python | `http.client` | | +| Kotlin | `okhttp3` | | +| Ruby | `faraday` | | +| Ruby | `net/http` | | +| Rust | `reqwest` | | +| Rust | `ureq` | | +| Rust | `Actix Client` | | +| Java | `asynchttpclient` | | +| Java | `HttpClient` | | +| Java | `okhttp3` | | +| Java | `Unirest` | | +| Julia | `HTTP` | | +| PHP | `curl` | | +| PHP | `guzzle` | | +| PHP | `HTTPlug` | | We welcome contributions to support other programming languages/libraries/frameworks. Please check out more details [here](https://github.com/foss42/apidash/discussions/80). @@ -150,49 +150,49 @@ API Dash is a next-gen API client that supports exploring, testing & previewing Here is the complete list of MIME types that can be directly previewed in API Dash: -| File Type | Mimetype | Extension | Comment | -| --------- | -------------------------- | ----------------- | --------------- | -| PDF | `application/pdf` | `.pdf` | | -| Video | `video/mp4` | `.mp4` | | -| Video | `video/webm` | `.webm` | | -| Video | `video/x-ms-wmv` | `.wmv` | | -| Video | `video/x-ms-asf` | `.wmv` | | -| Video | `video/avi` | `.avi` | | -| Video | `video/msvideo` | `.avi` | | -| Video | `video/x-msvideo` | `.avi` | | -| Video | `video/quicktime` | `.mov` | | -| Video | `video/x-quicktime` | `.mov` | | -| Video | `video/x-matroska` | `.mkv` | | -| Image | `image/apng` | `.apng` | Animated | -| Image | `image/avif` | `.avif` | | -| Image | `image/bmp` | `.bmp` | | -| Image | `image/gif` | `.gif` | Animated | -| Image | `image/jpeg` | `.jpeg` or `.jpg` | | -| Image | `image/jp2` | `.jp2` | | -| Image | `image/jpx` | `.jpf` or `.jpx` | | -| Image | `image/pict` | `.pct` | | -| Image | `image/portable-anymap` | `.pnm` | | -| Image | `image/png` | `.png` | | -| Image | `image/sgi` | `.sgi` | | -| Image | `image/svg+xml` | `.svg` | | -| Image | `image/tiff` | `.tiff` | | -| Image | `image/targa` | `.tga` | | -| Image | `image/vnd.wap.wbmp` | `.wbmp` | | -| Image | `image/webp` | `.webp` | | -| Image | `image/xwindowdump` | `.xwd` | | -| Image | `image/x-icon` | `.ico` | | -| Image | `image/x-portable-anymap` | `.pnm` | | -| Image | `image/x-portable-bitmap` | `.pbm` | | -| Image | `image/x-portable-graymap` | `.pgm` | | -| Image | `image/x-portable-pixmap` | `.ppm` | | -| Image | `image/x-tga` | `.tga` | | -| Image | `image/x-xwindowdump` | `.xwd` | | -| Audio | `audio/flac` | `.flac` | | -| Audio | `audio/mpeg` | `.mp3` | | -| Audio | `audio/mp4` | `.m4a` or `.mp4a` | | -| Audio | `audio/x-m4a` | `.m4a` | | -| Audio | `audio/wav` | `.wav` | | -| Audio | `audio/wave` | `.wav` | | +| File Type | Mimetype | Extension | Comment | +| --------- | -------------------------- | ----------------- | -------- | +| PDF | `application/pdf` | `.pdf` | | +| Video | `video/mp4` | `.mp4` | | +| Video | `video/webm` | `.webm` | | +| Video | `video/x-ms-wmv` | `.wmv` | | +| Video | `video/x-ms-asf` | `.wmv` | | +| Video | `video/avi` | `.avi` | | +| Video | `video/msvideo` | `.avi` | | +| Video | `video/x-msvideo` | `.avi` | | +| Video | `video/quicktime` | `.mov` | | +| Video | `video/x-quicktime` | `.mov` | | +| Video | `video/x-matroska` | `.mkv` | | +| Image | `image/apng` | `.apng` | Animated | +| Image | `image/avif` | `.avif` | | +| Image | `image/bmp` | `.bmp` | | +| Image | `image/gif` | `.gif` | Animated | +| Image | `image/jpeg` | `.jpeg` or `.jpg` | | +| Image | `image/jp2` | `.jp2` | | +| Image | `image/jpx` | `.jpf` or `.jpx` | | +| Image | `image/pict` | `.pct` | | +| Image | `image/portable-anymap` | `.pnm` | | +| Image | `image/png` | `.png` | | +| Image | `image/sgi` | `.sgi` | | +| Image | `image/svg+xml` | `.svg` | | +| Image | `image/tiff` | `.tiff` | | +| Image | `image/targa` | `.tga` | | +| Image | `image/vnd.wap.wbmp` | `.wbmp` | | +| Image | `image/webp` | `.webp` | | +| Image | `image/xwindowdump` | `.xwd` | | +| Image | `image/x-icon` | `.ico` | | +| Image | `image/x-portable-anymap` | `.pnm` | | +| Image | `image/x-portable-bitmap` | `.pbm` | | +| Image | `image/x-portable-graymap` | `.pgm` | | +| Image | `image/x-portable-pixmap` | `.ppm` | | +| Image | `image/x-tga` | `.tga` | | +| Image | `image/x-xwindowdump` | `.xwd` | | +| Audio | `audio/flac` | `.flac` | | +| Audio | `audio/mpeg` | `.mp3` | | +| Audio | `audio/mp4` | `.m4a` or `.mp4a` | | +| Audio | `audio/x-m4a` | `.m4a` | | +| Audio | `audio/wav` | `.wav` | | +| Audio | `audio/wave` | `.wav` | | | CSV | `text/csv` | `.csv` | Can be improved | We welcome PRs to add support for previewing other multimedia MIME types. Please go ahead and raise an issue so that we can discuss the approach. @@ -200,18 +200,18 @@ We are adding support for other MIME types with each release. But, if you are lo Here is the complete list of MIME types that are syntax highlighted in API Dash: -| Mimetype | Extension | Comment | -| ------------------ | --------- | ------------------------------------------------------------------------------------------------------------------- | +| Mimetype | Extension | Comment | +| ------------------ | --------- | ------------------------------------------------------------------------------------------------------------------ | | `application/json` | `.json` | Other MIME types like `application/geo+json`, `application/vcard+json` that are based on `json` are also supported. | | `application/xml` | `.xml` | Other MIME types like `application/xhtml+xml`, `application/vcard+xml` that are based on `xml` are also supported. | -| `text/xml` | `.xml` | | -| `application/yaml` | `.yaml` | Others - `application/x-yaml` or `application/x-yml` | -| `text/yaml` | `.yaml` | Others - `text/yml` | -| `application/sql` | `.sql` | | -| `text/css` | `.css` | | -| `text/html` | `.html` | Only syntax highlighting, no web preview. | -| `text/javascript` | `.js` | | -| `text/markdown` | `.md` | | +| `text/xml` | `.xml` | | +| `application/yaml` | `.yaml` | Others - `application/x-yaml` or `application/x-yml` | +| `text/yaml` | `.yaml` | Others - `text/yml` | +| `application/sql` | `.sql` | | +| `text/css` | `.css` | | +| `text/html` | `.html` | Only syntax highlighting, no web preview. | +| `text/javascript` | `.js` | | +| `text/markdown` | `.md` | | ## What's new in v0.3.0? @@ -223,7 +223,7 @@ Just click on the [Issue tab](https://github.com/foss42/apidash/issues) to raise ## Roadmap -Please find the Roadmap for API Dash [here](https://github.com/foss42/apidash/blob/main/ROADMAP.md). +Please find the Roadmap for API Dash [here](https://github.com/foss42/apidash/blob/main/ROADMAP.md). ## Contribute to API Dash From 1e480d1924e50ee8848f199628c7394207927fb1 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Mon, 26 Aug 2024 19:37:35 +0530 Subject: [PATCH 63/71] fix: revert CONTRIBUTING changes --- CONTRIBUTING.md | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 739167ee..a14cc4b8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ We value your participation in this open source project. This page will give you a quick overview of how to get involved. -You can contribute to the project in any or all of the following ways: +You can contribute to the project in any or all of the following ways: - [Ask a question](https://github.com/foss42/apidash/discussions) - [Submit a bug report](https://github.com/foss42/apidash/issues/new/choose) @@ -22,13 +22,12 @@ In case you are new to the open source ecosystem, we would be more than happy to > PRs with precise changes (like adding a new test, resolving a bug/issue, adding a new feature) are always preferred over a single PR with a ton of file changes as they are easier to review and merge. We currently do not accept PRs that involve: - - Code refactoring without any new feature addition/existing issue resolution. - Bumping of dependency versions (SDKs, Packages). ### Resolving an existing issue / Adding a requested feature -You can find all existing issues [here](https://github.com/foss42/apidash/issues). A good place to start is to take a look at ["good first issues"](https://github.com/foss42/apidash/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). +You can find all existing issues [here](https://github.com/foss42/apidash/issues). A good place to start is to take a look at ["good first issues"](https://github.com/foss42/apidash/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). **Step 1** - Identify the issue you want to work on. **Step 2** - Comment on the issue so that we can discuss how to approach and solve the problem. @@ -44,7 +43,7 @@ You can find all existing issues [here](https://github.com/foss42/apidash/issues **Step 1** - Open an [issue](https://github.com/foss42/apidash/issues/new/choose) so that we can discuss on the new feature. **Step 2** - Fork the [`foss42/apidash`](https://github.com/foss42/apidash) repo to your account. -**Step 3** - Create a new branch in your fork and name it `add-feature-xyz`. +**Step 3** - Create a new branch in your fork and name it `add-feature-xyz`. **Step 4** - Run API Dash locally (More details [here](#how-to-run-api-dash-locally)). **Step 5** - Make the necessary code changes required to implement the feature in the branch. **Step 6** - Once the feature is implemented. Make sure you add the relevant tests in the `test` folder and run tests (More details [here](#how-to-run-tests)). @@ -54,12 +53,11 @@ You can find all existing issues [here](https://github.com/foss42/apidash/issues ### Adding a new test You can contribute by adding missing/new tests for: - - Widgets (`lib/widgets/`) - Models (`lib/models/`) - Utilities (`lib/utils/`) - Riverpod providers (`lib/providers/`) -- Code generation (`lib/codegen/`) +- Code generation (`lib/codegen/`) - Services (`lib/services/`). **Step 1** - Identify the test you want to add or improve. @@ -70,11 +68,11 @@ You can contribute by adding missing/new tests for: **Step 6** - [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) with a clear title and description of the tests you are adding. **Step 7** - Wait for feedback and review. We will closely work with you on the Pull Request. -## General Instructions +## General Instructions ### What is the supported Flutter/Dart version? -This project supports the latest Dart 3 & Flutter version. If you are using older Flutter version that does not support Dart 3, you might get errors. +This project supports the latest Dart 3 & Flutter version. If you are using older Flutter version that does not support Dart 3, you might get errors. In case you are setting up Flutter for the first time, just go ahead and download the latest (Stable) SDK from the [Flutter SDK Archive](https://docs.flutter.dev/release/archive). Then proceed with the Flutter installation. @@ -102,12 +100,12 @@ flutter test --coverage To generate coverage report as html execute: ``` -genhtml coverage/lcov.info -o coverage/html +genhtml coverage/lcov.info -o coverage/html ``` **Note**: On macOS you need to have `lcov` installed on your system (`brew install lcov`) to run the above command. -To view the coverage report in the browser for further analysis, execute: +To view the coverage report in the browser for further analysis, execute: ``` open coverage/html/index.html @@ -168,4 +166,4 @@ android { multiDexEnabled true } } -``` +``` \ No newline at end of file From 735ced5ff8d9118535e511bf988bf78d5682d218 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Mon, 26 Aug 2024 19:47:38 +0530 Subject: [PATCH 64/71] mentioned doc PR --- doc/gsoc/2024/ragul_raj_m.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/gsoc/2024/ragul_raj_m.md b/doc/gsoc/2024/ragul_raj_m.md index d4a2033d..2439855e 100644 --- a/doc/gsoc/2024/ragul_raj_m.md +++ b/doc/gsoc/2024/ragul_raj_m.md @@ -170,6 +170,14 @@ In addition to testing each model, utility and widget added with the new feature Fix UI inconsistencies in mobile + + Documentation + + + + doc: added user guides + + From bd5c83525e028958631ce6b0e65680405c6743d9 Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Sun, 8 Sep 2024 05:05:00 +0530 Subject: [PATCH 65/71] kEncoder -> kJsonEncoder --- lib/codegen/java/okhttp.dart | 2 +- lib/codegen/js/axios.dart | 13 +++++++------ lib/codegen/js/fetch.dart | 4 ++-- lib/codegen/others/har.dart | 2 +- lib/codegen/python/http_client.dart | 4 ++-- lib/codegen/python/requests.dart | 4 ++-- lib/consts.dart | 3 ++- lib/utils/convert_utils.dart | 2 +- lib/utils/http_utils.dart | 2 +- lib/widgets/json_previewer.dart | 6 +++--- lib/widgets/response_widgets.dart | 2 +- 11 files changed, 23 insertions(+), 21 deletions(-) diff --git a/lib/codegen/java/okhttp.dart b/lib/codegen/java/okhttp.dart index 12b2f90f..9045aa55 100644 --- a/lib/codegen/java/okhttp.dart +++ b/lib/codegen/java/okhttp.dart @@ -140,7 +140,7 @@ import okhttp3.MultipartBody;"""; var templateBody = jj.Template(kTemplateRequestBody); result += templateBody.render({ "contentType": contentType, - "body": kEncoder.convert(requestBody) + "body": kJsonEncoder.convert(requestBody) }); } } diff --git a/lib/codegen/js/axios.dart b/lib/codegen/js/axios.dart index faf97304..6314bdcd 100644 --- a/lib/codegen/js/axios.dart +++ b/lib/codegen/js/axios.dart @@ -79,7 +79,7 @@ axios(config) m[i["name"]] = i["value"]; } result += templateParams - .render({"params": padMultilineString(kEncoder.convert(m), 2)}); + .render({"params": padMultilineString(kJsonEncoder.convert(m), 2)}); } var headers = harJson["headers"]; @@ -92,8 +92,8 @@ axios(config) if (requestModel.hasFormData) { m[kHeaderContentType] = ContentType.formdata.header; } - result += templateHeader - .render({"headers": padMultilineString(kEncoder.convert(m), 2)}); + result += templateHeader.render( + {"headers": padMultilineString(kJsonEncoder.convert(m), 2)}); } var templateBody = jj.Template(kTemplateBody); if (requestModel.hasFormData && requestModel.formDataMapList.isNotEmpty) { @@ -108,12 +108,13 @@ axios(config) : "fileInput$formFileCounter.files[0]"; if (element["type"] == "file") formFileCounter++; } - var sanitizedJSObject = sanitzeJSObject(kEncoder.convert(formParams)); + var sanitizedJSObject = + sanitzeJSObject(kJsonEncoder.convert(formParams)); result += templateBody .render({"body": padMultilineString(sanitizedJSObject, 2)}); } else if (harJson["postData"]?["text"] != null) { - result += templateBody - .render({"body": kEncoder.convert(harJson["postData"]["text"])}); + result += templateBody.render( + {"body": kJsonEncoder.convert(harJson["postData"]["text"])}); } result += kStringRequest; return result; diff --git a/lib/codegen/js/fetch.dart b/lib/codegen/js/fetch.dart index 21c4f238..c4ca14b0 100644 --- a/lib/codegen/js/fetch.dart +++ b/lib/codegen/js/fetch.dart @@ -106,7 +106,7 @@ fetch(url, options) } if (m.isNotEmpty) { result += templateHeader.render({ - "headers": padMultilineString(kEncoder.convert(m), 2), + "headers": padMultilineString(kJsonEncoder.convert(m), 2), }); } } @@ -114,7 +114,7 @@ fetch(url, options) if (harJson["postData"]?["text"] != null) { var templateBody = jj.Template(kTemplateBody); result += templateBody.render({ - "body": kEncoder.convert(harJson["postData"]["text"]), + "body": kJsonEncoder.convert(harJson["postData"]["text"]), }); } else if (requestModel.hasFormData) { var templateBody = jj.Template(kTemplateBody); diff --git a/lib/codegen/others/har.dart b/lib/codegen/others/har.dart index 32a35abe..acef6357 100644 --- a/lib/codegen/others/har.dart +++ b/lib/codegen/others/har.dart @@ -9,7 +9,7 @@ class HARCodeGen { String? boundary, }) { try { - var harString = kEncoder.convert(requestModelToHARJsonRequest( + var harString = kJsonEncoder.convert(requestModelToHARJsonRequest( requestModel, defaultUriScheme: defaultUriScheme, useEnabled: true, diff --git a/lib/codegen/python/http_client.dart b/lib/codegen/python/http_client.dart index ea783ebe..f58607c2 100644 --- a/lib/codegen/python/http_client.dart +++ b/lib/codegen/python/http_client.dart @@ -111,7 +111,7 @@ body = b'\r\n'.join(dataList) if (params.isNotEmpty) { hasQuery = true; var templateParams = jj.Template(kTemplateParams); - var paramsString = kEncoder.convert(params); + var paramsString = kJsonEncoder.convert(params); result += templateParams.render({"params": paramsString}); } } @@ -143,7 +143,7 @@ body = b'\r\n'.join(dataList) }); } } - var headersString = kEncoder.convert(headers); + var headersString = kJsonEncoder.convert(headers); var templateHeaders = jj.Template(kTemplateHeaders); result += templateHeaders.render({"headers": headersString}); } diff --git a/lib/codegen/python/requests.dart b/lib/codegen/python/requests.dart index bb41ad9d..8ca4c152 100644 --- a/lib/codegen/python/requests.dart +++ b/lib/codegen/python/requests.dart @@ -107,7 +107,7 @@ print('Response Body:', response.text) if (params.isNotEmpty) { hasQuery = true; var templateParams = jj.Template(kTemplateParams); - var paramsString = kEncoder.convert(params); + var paramsString = kJsonEncoder.convert(params); result += templateParams.render({"params": paramsString}); } } @@ -162,7 +162,7 @@ print('Response Body:', response.text) } if (headers.isNotEmpty) { hasHeaders = true; - var headersString = kEncoder.convert(headers); + var headersString = kJsonEncoder.convert(headers); headersString = refactorHeaderString(headersString); var templateHeaders = jj.Template(kTemplateHeaders); result += templateHeaders.render({"headers": headersString}); diff --git a/lib/consts.dart b/lib/consts.dart index 9d72e0e3..1681069a 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -413,7 +413,8 @@ enum ImportFormat { final String label; } -const JsonEncoder kEncoder = JsonEncoder.withIndent(' '); +const JsonEncoder kJsonEncoder = JsonEncoder.withIndent(' '); +const JsonDecoder kJsonDecoder = JsonDecoder(); const LineSplitter kSplitter = LineSplitter(); const String kGlobalEnvironmentId = "global"; diff --git a/lib/utils/convert_utils.dart b/lib/utils/convert_utils.dart index d0b2ac26..b3c3b2bc 100644 --- a/lib/utils/convert_utils.dart +++ b/lib/utils/convert_utils.dart @@ -150,7 +150,7 @@ Uint8List jsonMapToBytes(Map? map) { if (map == null) { return Uint8List.fromList([]); } else { - String text = kEncoder.convert(map); + String text = kJsonEncoder.convert(map); var l = utf8.encode(text); var bytes = Uint8List.fromList(l); return bytes; diff --git a/lib/utils/http_utils.dart b/lib/utils/http_utils.dart index 4688d2af..e323939a 100644 --- a/lib/utils/http_utils.dart +++ b/lib/utils/http_utils.dart @@ -138,7 +138,7 @@ String? formatBody(String? body, MediaType? mediaType) { try { if (subtype.contains(kSubTypeJson)) { final tmp = jsonDecode(body); - String result = kEncoder.convert(tmp); + String result = kJsonEncoder.convert(tmp); return result; } if (subtype.contains(kSubTypeXml)) { diff --git a/lib/widgets/json_previewer.dart b/lib/widgets/json_previewer.dart index 3a4f5648..020015d6 100644 --- a/lib/widgets/json_previewer.dart +++ b/lib/widgets/json_previewer.dart @@ -182,7 +182,7 @@ class _JsonPreviewerState extends State { TextButton( onPressed: () async { await _copy( - kEncoder.convert(widget.code), sm); + kJsonEncoder.convert(widget.code), sm); }, child: const Text( 'Copy', @@ -215,7 +215,7 @@ class _JsonPreviewerState extends State { visualDensity: VisualDensity.compact, onPressed: () async { await _copy( - kEncoder.convert(widget.code), sm); + kJsonEncoder.convert(widget.code), sm); }, icon: const Icon( Icons.copy, @@ -273,7 +273,7 @@ class _JsonPreviewerState extends State { ), onPressed: () async { await _copy( - kEncoder.convert(toJson(node)), sm); + kJsonEncoder.convert(toJson(node)), sm); }, ), ) diff --git a/lib/widgets/response_widgets.dart b/lib/widgets/response_widgets.dart index 0df9343b..42154478 100644 --- a/lib/widgets/response_widgets.dart +++ b/lib/widgets/response_widgets.dart @@ -262,7 +262,7 @@ class ResponseHeadersHeader extends StatelessWidget { ), if (map.isNotEmpty) CopyButton( - toCopy: kEncoder.convert(map), + toCopy: kJsonEncoder.convert(map), ), ], ), From 802d9e4607abf9644f4e63457a67be8e589d90b8 Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Sun, 8 Sep 2024 15:17:21 +0530 Subject: [PATCH 66/71] workspace --- lib/app.dart | 2 +- lib/main.dart | 53 ++++++++++++---- lib/models/settings_model.dart | 19 ++++-- lib/providers/settings_providers.dart | 15 ++--- lib/services/history_service.dart | 13 +--- lib/services/hive_services.dart | 37 +++-------- lib/services/services.dart | 1 + lib/services/shared_preferences_services.dart | 23 +++++++ lib/utils/history_utils.dart | 2 +- lib/widgets/widgets.dart | 1 + lib/widgets/workspace_selector.dart | 62 +++++++++++++++++++ pubspec.lock | 56 +++++++++++++++++ pubspec.yaml | 1 + test/providers/ui_providers_test.dart | 2 +- 14 files changed, 220 insertions(+), 67 deletions(-) create mode 100644 lib/services/shared_preferences_services.dart create mode 100644 lib/widgets/workspace_selector.dart diff --git a/lib/app.dart b/lib/app.dart index 3d36968a..7083eb4a 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:window_manager/window_manager.dart' hide WindowCaption; -import 'widgets/widgets.dart' show WindowCaption; +import 'widgets/widgets.dart' show WindowCaption, WorkspaceSelector; import 'providers/providers.dart'; import 'extensions/extensions.dart'; import 'screens/screens.dart'; diff --git a/lib/main.dart b/lib/main.dart index 4a8b07fd..6c70bdf8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,35 +1,62 @@ +import 'package:apidash/providers/settings_providers.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'models/models.dart'; +import 'providers/providers.dart'; import 'services/services.dart'; -import 'consts.dart' show kIsLinux, kIsMacOS, kIsWindows; +import 'consts.dart'; import 'app.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - - await initApp(); - await initWindow(); + final settingsModel = await getSettingsFromSharedPrefs(); + await initApp(settingsModel: settingsModel); + if (kIsDesktop) { + await initWindow(settingsModel: settingsModel); + } runApp( - const ProviderScope( - child: DashApp(), + ProviderScope( + overrides: [ + settingsProvider.overrideWith( + (ref) => ThemeStateNotifier(settingsModel: settingsModel), + ) + ], + child: const DashApp(), ), ); } -Future initApp() async { +Future initApp({SettingsModel? settingsModel}) async { GoogleFonts.config.allowRuntimeFetching = false; - await openBoxes(); - await autoClearHistory(); + await openBoxes( + kIsDesktop, + settingsModel?.workspaceFolderPath, + ); + await autoClearHistory(settingsModel: settingsModel); } -Future initWindow({Size? sz}) async { +Future initWindow({ + Size? sz, + SettingsModel? settingsModel, +}) async { if (kIsLinux) { - await setupInitialWindow(sz: sz); + await setupInitialWindow( + sz: sz ?? settingsModel?.size, + ); } if (kIsMacOS || kIsWindows) { - var win = sz != null ? (sz, const Offset(100, 100)) : getInitialSize(); - await setupWindow(sz: win.$1, off: win.$2); + if (sz != null) { + await setupWindow( + sz: sz, + off: const Offset(100, 100), + ); + } else { + await setupWindow( + sz: settingsModel?.size, + off: settingsModel?.offset, + ); + } } } diff --git a/lib/models/settings_model.dart b/lib/models/settings_model.dart index e82ffdca..e7e5c3b0 100644 --- a/lib/models/settings_model.dart +++ b/lib/models/settings_model.dart @@ -14,6 +14,7 @@ class SettingsModel { this.promptBeforeClosing = true, this.activeEnvironmentId, this.historyRetentionPeriod = HistoryRetentionPeriod.oneWeek, + this.workspaceFolderPath, }); final bool isDark; @@ -26,6 +27,7 @@ class SettingsModel { final bool promptBeforeClosing; final String? activeEnvironmentId; final HistoryRetentionPeriod historyRetentionPeriod; + final String? workspaceFolderPath; SettingsModel copyWith({ bool? isDark, @@ -38,6 +40,7 @@ class SettingsModel { bool? promptBeforeClosing, String? activeEnvironmentId, HistoryRetentionPeriod? historyRetentionPeriod, + String? workspaceFolderPath, }) { return SettingsModel( isDark: isDark ?? this.isDark, @@ -52,6 +55,7 @@ class SettingsModel { activeEnvironmentId: activeEnvironmentId ?? this.activeEnvironmentId, historyRetentionPeriod: historyRetentionPeriod ?? this.historyRetentionPeriod, + workspaceFolderPath: workspaceFolderPath ?? this.workspaceFolderPath, ); } @@ -86,8 +90,7 @@ class SettingsModel { final promptBeforeClosing = data["promptBeforeClosing"] as bool?; final activeEnvironmentId = data["activeEnvironmentId"] as String?; final historyRetentionPeriodStr = data["historyRetentionPeriod"] as String?; - HistoryRetentionPeriod historyRetentionPeriod = - HistoryRetentionPeriod.oneWeek; + HistoryRetentionPeriod? historyRetentionPeriod; if (historyRetentionPeriodStr != null) { try { historyRetentionPeriod = @@ -96,6 +99,7 @@ class SettingsModel { // pass } } + final workspaceFolderPath = data["workspaceFolderPath"] as String?; const sm = SettingsModel(); @@ -109,7 +113,9 @@ class SettingsModel { saveResponses: saveResponses, promptBeforeClosing: promptBeforeClosing, activeEnvironmentId: activeEnvironmentId, - historyRetentionPeriod: historyRetentionPeriod, + historyRetentionPeriod: + historyRetentionPeriod ?? HistoryRetentionPeriod.oneWeek, + workspaceFolderPath: workspaceFolderPath, ); } @@ -127,12 +133,13 @@ class SettingsModel { "promptBeforeClosing": promptBeforeClosing, "activeEnvironmentId": activeEnvironmentId, "historyRetentionPeriod": historyRetentionPeriod.name, + "workspaceFolderPath": workspaceFolderPath, }; } @override String toString() { - return toJson().toString(); + return kJsonEncoder.convert(toJson()); } @override @@ -149,7 +156,8 @@ class SettingsModel { other.saveResponses == saveResponses && other.promptBeforeClosing == promptBeforeClosing && other.activeEnvironmentId == activeEnvironmentId && - other.historyRetentionPeriod == historyRetentionPeriod; + other.historyRetentionPeriod == historyRetentionPeriod && + other.workspaceFolderPath == workspaceFolderPath; } @override @@ -166,6 +174,7 @@ class SettingsModel { promptBeforeClosing, activeEnvironmentId, historyRetentionPeriod, + workspaceFolderPath, ); } } diff --git a/lib/providers/settings_providers.dart b/lib/providers/settings_providers.dart index 6293e04b..43256ab3 100644 --- a/lib/providers/settings_providers.dart +++ b/lib/providers/settings_providers.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../models/models.dart'; -import '../services/services.dart' show hiveHandler, HiveHandler; +import '../services/services.dart'; import '../consts.dart'; final codegenLanguageStateProvider = StateProvider((ref) => @@ -11,14 +11,13 @@ final activeEnvironmentIdStateProvider = StateProvider((ref) => ref.watch(settingsProvider.select((value) => value.activeEnvironmentId))); final StateNotifierProvider - settingsProvider = - StateNotifierProvider((ref) => ThemeStateNotifier(hiveHandler)); + settingsProvider = StateNotifierProvider((ref) => ThemeStateNotifier()); class ThemeStateNotifier extends StateNotifier { - ThemeStateNotifier(this.hiveHandler) : super(const SettingsModel()) { - state = SettingsModel.fromJson(hiveHandler.settings); + ThemeStateNotifier({this.settingsModel}) : super(const SettingsModel()) { + state = settingsModel ?? const SettingsModel(); } - final HiveHandler hiveHandler; + final SettingsModel? settingsModel; Future update({ bool? isDark, @@ -31,6 +30,7 @@ class ThemeStateNotifier extends StateNotifier { bool? promptBeforeClosing, String? activeEnvironmentId, HistoryRetentionPeriod? historyRetentionPeriod, + String? workspaceFolderPath, }) async { state = state.copyWith( isDark: isDark, @@ -43,7 +43,8 @@ class ThemeStateNotifier extends StateNotifier { promptBeforeClosing: promptBeforeClosing, activeEnvironmentId: activeEnvironmentId, historyRetentionPeriod: historyRetentionPeriod, + workspaceFolderPath: workspaceFolderPath, ); - await hiveHandler.saveSettings(state.toJson()); + await setSettingsToSharedPrefs(state); } } diff --git a/lib/services/history_service.dart b/lib/services/history_service.dart index d34d17fb..487b9164 100644 --- a/lib/services/history_service.dart +++ b/lib/services/history_service.dart @@ -1,18 +1,9 @@ import 'package:apidash/models/models.dart'; import 'package:apidash/utils/utils.dart'; -import 'package:apidash/consts.dart'; import 'hive_services.dart'; -Future autoClearHistory() async { - final settingsMap = hiveHandler.settings; - final retentionPeriod = settingsMap['historyRetentionPeriod']; - - HistoryRetentionPeriod historyRetentionPeriod = - HistoryRetentionPeriod.oneWeek; - if (retentionPeriod != null) { - historyRetentionPeriod = - HistoryRetentionPeriod.values.byName(retentionPeriod); - } +Future autoClearHistory({SettingsModel? settingsModel}) async { + final historyRetentionPeriod = settingsModel?.historyRetentionPeriod; DateTime? retentionDate = getRetentionDate(historyRetentionPeriod); if (retentionDate == null) { diff --git a/lib/services/hive_services.dart b/lib/services/hive_services.dart index 1a0f7ac0..3caa3c28 100644 --- a/lib/services/hive_services.dart +++ b/lib/services/hive_services.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; const String kDataBox = "apidash-data"; @@ -11,54 +10,36 @@ const String kHistoryMetaBox = "apidash-history-meta"; const String kHistoryBoxIds = "historyIds"; const String kHistoryLazyBox = "apidash-history-lazy"; -const String kSettingsBox = "apidash-settings"; - -Future openBoxes() async { - await Hive.initFlutter(); +Future openBoxes( + bool isDesktop, + String? workspaceFolderPath, +) async { + if (isDesktop) { + Hive.init(workspaceFolderPath); + } else { + await Hive.initFlutter(); + } await Hive.openBox(kDataBox); - await Hive.openBox(kSettingsBox); await Hive.openBox(kEnvironmentBox); await Hive.openBox(kHistoryMetaBox); await Hive.openLazyBox(kHistoryLazyBox); } -(Size?, Offset?) getInitialSize() { - Size? sz; - Offset? off; - var settingsBox = Hive.box(kSettingsBox); - double? w = settingsBox.get("width") as double?; - double? h = settingsBox.get("height") as double?; - if (w != null && h != null) { - sz = Size(w, h); - } - double? dx = settingsBox.get("dx") as double?; - double? dy = settingsBox.get("dy") as double?; - if (dx != null && dy != null) { - off = Offset(dx, dy); - } - return (sz, off); -} - final hiveHandler = HiveHandler(); class HiveHandler { late final Box dataBox; - late final Box settingsBox; late final Box environmentBox; late final Box historyMetaBox; late final LazyBox historyLazyBox; HiveHandler() { dataBox = Hive.box(kDataBox); - settingsBox = Hive.box(kSettingsBox); environmentBox = Hive.box(kEnvironmentBox); historyMetaBox = Hive.box(kHistoryMetaBox); historyLazyBox = Hive.lazyBox(kHistoryLazyBox); } - Map get settings => settingsBox.toMap(); - Future saveSettings(Map data) => settingsBox.putAll(data); - dynamic getIds() => dataBox.get(kKeyDataBoxIds); Future setIds(List? ids) => dataBox.put(kKeyDataBoxIds, ids); diff --git a/lib/services/services.dart b/lib/services/services.dart index 7551de9b..fd8ca0b0 100644 --- a/lib/services/services.dart +++ b/lib/services/services.dart @@ -2,3 +2,4 @@ export 'http_service.dart'; export 'hive_services.dart'; export 'history_service.dart'; export 'window_services.dart'; +export 'shared_preferences_services.dart'; diff --git a/lib/services/shared_preferences_services.dart b/lib/services/shared_preferences_services.dart new file mode 100644 index 00000000..af1bbd71 --- /dev/null +++ b/lib/services/shared_preferences_services.dart @@ -0,0 +1,23 @@ +import 'package:apidash/consts.dart'; +import 'package:apidash/models/models.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +const String kSharedPrefSettingsKey = 'apidash-settings'; + +Future getSettingsFromSharedPrefs() async { + final prefs = await SharedPreferences.getInstance(); + var settingsStr = prefs.getString(kSharedPrefSettingsKey); + if (settingsStr != null) { + var jsonSettings = kJsonDecoder.convert(settingsStr); + var jsonMap = Map.from(jsonSettings); + var settingsModel = SettingsModel.fromJson(jsonMap); + return settingsModel; + } else { + return null; + } +} + +Future setSettingsToSharedPrefs(SettingsModel settingsModel) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(kSharedPrefSettingsKey, settingsModel.toString()); +} diff --git a/lib/utils/history_utils.dart b/lib/utils/history_utils.dart index cce87998..e6db9d38 100644 --- a/lib/utils/history_utils.dart +++ b/lib/utils/history_utils.dart @@ -114,7 +114,7 @@ List getRequestGroup( return requestGroup; } -DateTime? getRetentionDate(HistoryRetentionPeriod retentionPeriod) { +DateTime? getRetentionDate(HistoryRetentionPeriod? retentionPeriod) { DateTime now = DateTime.now(); DateTime today = stripTime(now); diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 06bc4bef..b7a8732e 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -60,3 +60,4 @@ export 'tabs.dart'; export 'texts.dart'; export 'uint8_audio_player.dart'; export 'window_caption.dart'; +export 'workspace_selector.dart'; diff --git a/lib/widgets/workspace_selector.dart b/lib/widgets/workspace_selector.dart new file mode 100644 index 00000000..1ff7ddd5 --- /dev/null +++ b/lib/widgets/workspace_selector.dart @@ -0,0 +1,62 @@ +import 'package:file_selector/file_selector.dart'; +import 'package:apidash/services/hive_services.dart'; +import 'package:flutter/material.dart'; + +class WorkspaceSelector extends StatefulWidget { + final Future Function(String)? onSelect; + const WorkspaceSelector({ + super.key, + required this.onSelect, + }); + + @override + WorkspaceSelectorState createState() => WorkspaceSelectorState(); +} + +class WorkspaceSelectorState extends State { + void selectFolder() async { + String? selectedDirectory = await getDirectoryPath(); + if (selectedDirectory != null) { + widget.onSelect?.call(selectedDirectory); + } + } + + @override + Widget build(BuildContext context) { + const circularLoader = MaterialApp( + home: Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ), + ); + + return FutureBuilder( + future: getHiveSaveFolder(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return circularLoader; + } + + // If there isn't hive selected folder choose it + if (snapshot.data == null) { + selectFolder(); + return circularLoader; + } + + // Once _hiveSaveFolder is set, display DashApp after hive init + return FutureBuilder( + future: openHiveBoxes(snapshot.data!), + builder: (BuildContext context, AsyncSnapshot snapshot) { + // if loading show circularLoader + if (snapshot.connectionState != ConnectionState.done) { + return circularLoader; + } + // Display widget + return widget.child; + }, + ); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index f4140996..2c257859 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1215,6 +1215,62 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.8" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + url: "https://pub.dev" + source: hosted + version: "2.5.2" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" shelf: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0eadf529..a9bf5998 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,6 +63,7 @@ dependencies: provider: ^6.1.2 riverpod: ^2.5.1 scrollable_positioned_list: ^0.3.8 + shared_preferences: ^2.3.2 url_launcher: ^6.2.5 uuid: ^4.3.3 vector_graphics_compiler: ^1.1.9+1 diff --git a/test/providers/ui_providers_test.dart b/test/providers/ui_providers_test.dart index 39ef1ff6..60b67241 100644 --- a/test/providers/ui_providers_test.dart +++ b/test/providers/ui_providers_test.dart @@ -39,7 +39,7 @@ void main() { } return null; }); - await openBoxes(); + await openBoxes(false, null); final flamante = rootBundle.load('google_fonts/OpenSans-Medium.ttf'); final fontLoader = FontLoader('OpenSans')..addFont(flamante); await fontLoader.load(); From 59fdbae41d84f23b8a6d7c9476428a680a8934ce Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Mon, 9 Sep 2024 04:03:52 +0530 Subject: [PATCH 67/71] Workspace selector feature --- lib/app.dart | 53 ++++++--- lib/consts.dart | 5 + lib/main.dart | 28 +++-- lib/models/settings_model.dart | 18 +++ lib/services/hive_services.dart | 28 +++-- lib/widgets/field_outlined.dart | 54 +++++++++ lib/widgets/widgets.dart | 1 + lib/widgets/workspace_selector.dart | 167 ++++++++++++++++++++-------- 8 files changed, 273 insertions(+), 81 deletions(-) create mode 100644 lib/widgets/field_outlined.dart diff --git a/lib/app.dart b/lib/app.dart index 7083eb4a..a2011dcd 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:window_manager/window_manager.dart' hide WindowCaption; import 'widgets/widgets.dart' show WindowCaption, WorkspaceSelector; import 'providers/providers.dart'; +import 'services/services.dart'; import 'extensions/extensions.dart'; import 'screens/screens.dart'; import 'consts.dart'; @@ -107,29 +108,49 @@ class DashApp extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final isDarkMode = ref.watch(settingsProvider.select((value) => value.isDark)); + final workspaceFolderPath = ref + .watch(settingsProvider.select((value) => value.workspaceFolderPath)); + final showWorkspaceSelector = kIsDesktop && (workspaceFolderPath == null); return Portal( child: MaterialApp( debugShowCheckedModeBanner: false, theme: kLightMaterialAppTheme, darkTheme: kDarkMaterialAppTheme, themeMode: isDarkMode ? ThemeMode.dark : ThemeMode.light, - home: Stack( - children: [ - !kIsLinux && !kIsMobile - ? const App() - : context.isMediumWindow - ? const MobileDashboard() - : const Dashboard(), - if (kIsWindows) - SizedBox( - height: 29, - child: WindowCaption( - backgroundColor: Colors.transparent, - brightness: isDarkMode ? Brightness.dark : Brightness.light, - ), + home: showWorkspaceSelector + ? WorkspaceSelector( + onContinue: (val) async { + await openBoxes(kIsDesktop, val); + ref + .read(settingsProvider.notifier) + .update(workspaceFolderPath: val); + }, + onCancel: () async { + try { + await windowManager.destroy(); + } catch (e) { + debugPrint(e.toString()); + } + }, + ) + : Stack( + children: [ + !kIsLinux && !kIsMobile + ? const App() + : context.isMediumWindow + ? const MobileDashboard() + : const Dashboard(), + if (kIsWindows) + SizedBox( + height: 29, + child: WindowCaption( + backgroundColor: Colors.transparent, + brightness: + isDarkMode ? Brightness.dark : Brightness.light, + ), + ), + ], ), - ], - ), ), ); } diff --git a/lib/consts.dart b/lib/consts.dart index 1681069a..cb679187 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -744,6 +744,9 @@ const kLabelSaving = "Saving"; const kLabelSaved = "Saved"; const kLabelCode = "Code"; const kLabelDuplicate = "Duplicate"; +const kLabelSelect = "Select"; +const kLabelContinue = "Continue"; +const kLabelCancel = "Cancel"; // Request Pane const kLabelRequest = "Request"; const kLabelHideCode = "Hide Code"; @@ -778,3 +781,5 @@ const kNullResponseModelError = "Error: Response data does not exist."; const kMsgNullBody = "Response body is missing (null)."; const kMsgNoContent = "No content"; const kMsgUnknowContentType = "Unknown Response Content-Type"; +// Workspace Selector +const kMsgSelectWorkspace = "Create your workspace"; diff --git a/lib/main.dart b/lib/main.dart index 6c70bdf8..b5fa019f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,11 +10,14 @@ import 'app.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - final settingsModel = await getSettingsFromSharedPrefs(); - await initApp(settingsModel: settingsModel); + var settingsModel = await getSettingsFromSharedPrefs(); + final initStatus = await initApp(settingsModel: settingsModel); if (kIsDesktop) { await initWindow(settingsModel: settingsModel); } + if (!initStatus) { + settingsModel = settingsModel?.copyWithPath(workspaceFolderPath: null); + } runApp( ProviderScope( @@ -28,13 +31,22 @@ void main() async { ); } -Future initApp({SettingsModel? settingsModel}) async { +Future initApp({SettingsModel? settingsModel}) async { GoogleFonts.config.allowRuntimeFetching = false; - await openBoxes( - kIsDesktop, - settingsModel?.workspaceFolderPath, - ); - await autoClearHistory(settingsModel: settingsModel); + try { + final openBoxesStatus = await openBoxes( + kIsDesktop, + settingsModel?.workspaceFolderPath, + ); + debugPrint("openBoxesStatus: $openBoxesStatus"); + if (openBoxesStatus) { + await autoClearHistory(settingsModel: settingsModel); + } + return openBoxesStatus; + } catch (e) { + debugPrint("initApp failed due to $e"); + return false; + } } Future initWindow({ diff --git a/lib/models/settings_model.dart b/lib/models/settings_model.dart index e7e5c3b0..6784f214 100644 --- a/lib/models/settings_model.dart +++ b/lib/models/settings_model.dart @@ -59,6 +59,24 @@ class SettingsModel { ); } + SettingsModel copyWithPath({ + String? workspaceFolderPath, + }) { + return SettingsModel( + isDark: isDark, + alwaysShowCollectionPaneScrollbar: alwaysShowCollectionPaneScrollbar, + size: size, + defaultUriScheme: defaultUriScheme, + defaultCodeGenLang: defaultCodeGenLang, + offset: offset, + saveResponses: saveResponses, + promptBeforeClosing: promptBeforeClosing, + activeEnvironmentId: activeEnvironmentId, + historyRetentionPeriod: historyRetentionPeriod, + workspaceFolderPath: workspaceFolderPath, + ); + } + factory SettingsModel.fromJson(Map data) { final isDark = data["isDark"] as bool?; final alwaysShowCollectionPaneScrollbar = diff --git a/lib/services/hive_services.dart b/lib/services/hive_services.dart index 3caa3c28..fab18540 100644 --- a/lib/services/hive_services.dart +++ b/lib/services/hive_services.dart @@ -10,19 +10,29 @@ const String kHistoryMetaBox = "apidash-history-meta"; const String kHistoryBoxIds = "historyIds"; const String kHistoryLazyBox = "apidash-history-lazy"; -Future openBoxes( +Future openBoxes( bool isDesktop, String? workspaceFolderPath, ) async { - if (isDesktop) { - Hive.init(workspaceFolderPath); - } else { - await Hive.initFlutter(); + try { + if (isDesktop) { + if (workspaceFolderPath != null) { + Hive.init(workspaceFolderPath); + } else { + return false; + } + } else { + await Hive.initFlutter(); + } + + await Hive.openBox(kDataBox); + await Hive.openBox(kEnvironmentBox); + await Hive.openBox(kHistoryMetaBox); + await Hive.openLazyBox(kHistoryLazyBox); + return true; + } catch (e) { + return false; } - await Hive.openBox(kDataBox); - await Hive.openBox(kEnvironmentBox); - await Hive.openBox(kHistoryMetaBox); - await Hive.openLazyBox(kHistoryLazyBox); } final hiveHandler = HiveHandler(); diff --git a/lib/widgets/field_outlined.dart b/lib/widgets/field_outlined.dart new file mode 100644 index 00000000..0c981543 --- /dev/null +++ b/lib/widgets/field_outlined.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +class OutlinedField extends StatelessWidget { + const OutlinedField({ + super.key, + this.keyId, + this.initialValue, + this.hintText, + this.onChanged, + this.colorScheme, + }); + + final String? keyId; + final String? initialValue; + final String? hintText; + final void Function(String)? onChanged; + final ColorScheme? colorScheme; + + @override + Widget build(BuildContext context) { + var clrScheme = colorScheme ?? Theme.of(context).colorScheme; + return TextFormField( + key: keyId != null ? Key(keyId!) : null, + initialValue: initialValue, + style: kCodeStyle.copyWith( + color: clrScheme.onSurface, + ), + decoration: InputDecoration( + hintStyle: kCodeStyle.copyWith( + color: clrScheme.outline.withOpacity( + kHintOpacity, + ), + ), + hintText: hintText, + contentPadding: kP10, + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: clrScheme.primary.withOpacity( + kHintOpacity, + ), + ), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: clrScheme.surfaceContainerHighest, + ), + ), + isDense: true, + ), + onChanged: onChanged, + ); + } +} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index b7a8732e..ebf56e8e 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -31,6 +31,7 @@ export 'field_cell_obscurable.dart'; export 'field_cell.dart'; export 'field_header.dart'; export 'field_json_search.dart'; +export 'field_outlined.dart'; export 'field_raw.dart'; export 'field_read_only.dart'; export 'field_url.dart'; diff --git a/lib/widgets/workspace_selector.dart b/lib/widgets/workspace_selector.dart index 1ff7ddd5..2d41b311 100644 --- a/lib/widgets/workspace_selector.dart +++ b/lib/widgets/workspace_selector.dart @@ -1,62 +1,133 @@ -import 'package:file_selector/file_selector.dart'; -import 'package:apidash/services/hive_services.dart'; +import 'package:apidash/consts.dart'; import 'package:flutter/material.dart'; +import 'package:file_selector/file_selector.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:path/path.dart' as p; +import 'field_outlined.dart'; -class WorkspaceSelector extends StatefulWidget { - final Future Function(String)? onSelect; +class WorkspaceSelector extends HookWidget { const WorkspaceSelector({ super.key, - required this.onSelect, + required this.onContinue, + this.onCancel, }); - @override - WorkspaceSelectorState createState() => WorkspaceSelectorState(); -} - -class WorkspaceSelectorState extends State { - void selectFolder() async { - String? selectedDirectory = await getDirectoryPath(); - if (selectedDirectory != null) { - widget.onSelect?.call(selectedDirectory); - } - } + final Future Function(String)? onContinue; + final Future Function()? onCancel; @override Widget build(BuildContext context) { - const circularLoader = MaterialApp( - home: Scaffold( - body: Center( - child: CircularProgressIndicator(), + var selectedDirectory = useState(null); + var workspaceName = useState(null); + return Scaffold( + body: Center( + child: SizedBox( + width: 400, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + kMsgSelectWorkspace, + style: kTextStyleButton, + ), + kVSpacer20, + Row( + children: [ + Text( + "CHOOSE DIRECTORY", + style: kCodeStyle.copyWith( + fontSize: 12, + ), + ), + ], + ), + kVSpacer5, + Row( + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + border: Border.all( + width: 1, + color: Theme.of(context).colorScheme.primaryContainer, + ), + borderRadius: kBorderRadius6, + ), + padding: kP4, + child: Text( + style: kTextStyleButtonSmall, + selectedDirectory.value ?? "", + maxLines: 4, + overflow: TextOverflow.ellipsis, + ), + ), + ), + kHSpacer10, + FilledButton.tonalIcon( + onPressed: () async { + selectedDirectory.value = await getDirectoryPath(); + }, + label: const Text(kLabelSelect), + icon: const Icon(Icons.folder_rounded), + ), + ], + ), + kVSpacer10, + Row( + children: [ + Text( + "WORKSPACE NAME [OPTIONAL]\n(FOLDER WILL BE CREATED IN THE SELECTED DIRECTORY)", + style: kCodeStyle.copyWith( + fontSize: 12, + ), + ), + ], + ), + kVSpacer5, + OutlinedField( + keyId: "workspace-name", + onChanged: (value) { + workspaceName.value = value.trim(); + }, + colorScheme: Theme.of(context).colorScheme, + ), + kVSpacer40, + Row( + mainAxisSize: MainAxisSize.min, + children: [ + FilledButton( + onPressed: selectedDirectory.value == null + ? null + : () async { + String finalPath = selectedDirectory.value!; + if (workspaceName.value != null && + workspaceName.value!.trim().isNotEmpty) { + finalPath = + p.join(finalPath, workspaceName.value); + } + await onContinue?.call(finalPath); + }, + child: const Text(kLabelContinue), + ), + kHSpacer10, + FilledButton( + onPressed: onCancel, + style: FilledButton.styleFrom( + backgroundColor: + Theme.of(context).brightness == Brightness.dark + ? kColorDarkDanger + : kColorLightDanger, + surfaceTintColor: kColorRed, + foregroundColor: + Theme.of(context).colorScheme.onPrimary), + child: const Text(kLabelCancel), + ) + ], + ) + ], + ), ), ), ); - - return FutureBuilder( - future: getHiveSaveFolder(), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState != ConnectionState.done) { - return circularLoader; - } - - // If there isn't hive selected folder choose it - if (snapshot.data == null) { - selectFolder(); - return circularLoader; - } - - // Once _hiveSaveFolder is set, display DashApp after hive init - return FutureBuilder( - future: openHiveBoxes(snapshot.data!), - builder: (BuildContext context, AsyncSnapshot snapshot) { - // if loading show circularLoader - if (snapshot.connectionState != ConnectionState.done) { - return circularLoader; - } - // Display widget - return widget.child; - }, - ); - }, - ); } } From 9a056c37024cc2f2b7588d8f0923cfffb853fbea Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Mon, 9 Sep 2024 04:11:07 +0530 Subject: [PATCH 68/71] Update settings_model_test.dart --- test/models/settings_model_test.dart | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/test/models/settings_model_test.dart b/test/models/settings_model_test.dart index 6df01335..0a1a33a9 100644 --- a/test/models/settings_model_test.dart +++ b/test/models/settings_model_test.dart @@ -15,6 +15,7 @@ void main() { promptBeforeClosing: true, activeEnvironmentId: null, historyRetentionPeriod: HistoryRetentionPeriod.oneWeek, + workspaceFolderPath: null, ); test('Testing toJson()', () { @@ -31,6 +32,7 @@ void main() { "promptBeforeClosing": true, "activeEnvironmentId": null, "historyRetentionPeriod": "oneWeek", + "workspaceFolderPath": null, }; expect(sm.toJson(), expectedResult); }); @@ -49,6 +51,7 @@ void main() { "promptBeforeClosing": true, "activeEnvironmentId": null, "historyRetentionPeriod": "oneWeek", + "workspaceFolderPath": null, }; expect(SettingsModel.fromJson(input), sm); }); @@ -75,8 +78,21 @@ void main() { }); test('Testing toString()', () { - const expectedResult = - "{isDark: false, alwaysShowCollectionPaneScrollbar: true, width: 300.0, height: 200.0, dx: 100.0, dy: 150.0, defaultUriScheme: http, defaultCodeGenLang: curl, saveResponses: true, promptBeforeClosing: true, activeEnvironmentId: null, historyRetentionPeriod: oneWeek}"; + const expectedResult = '''{ + "isDark": false, + "alwaysShowCollectionPaneScrollbar": true, + "width": 300.0, + "height": 200.0, + "dx": 100.0, + "dy": 150.0, + "defaultUriScheme": "http", + "defaultCodeGenLang": "curl", + "saveResponses": true, + "promptBeforeClosing": true, + "activeEnvironmentId": null, + "historyRetentionPeriod": "oneWeek", + "workspaceFolderPath": null +}'''; expect(sm.toString(), expectedResult); }); From 83cdc130e54673dfcaae2e3ba47513f3fd5301af Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Mon, 9 Sep 2024 04:46:46 +0530 Subject: [PATCH 69/71] Update integration test --- integration_test/test_helper.dart | 17 +++++++++++++---- lib/main.dart | 12 +++++++++--- lib/services/hive_services.dart | 4 ++-- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/integration_test/test_helper.dart b/integration_test/test_helper.dart index 70cafa9d..4cb151dc 100644 --- a/integration_test/test_helper.dart +++ b/integration_test/test_helper.dart @@ -1,3 +1,5 @@ +import 'package:apidash/models/settings_model.dart'; +import 'package:apidash/providers/providers.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -34,17 +36,24 @@ class ApidashTestHelper { final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; - await app.initApp(); + await app.initApp(false); await app.initWindow(sz: size); return binding; } static Future loadApp(WidgetTester tester) async { - await app.initApp(); + await app.initApp(false); await tester.pumpWidget( - const ProviderScope( - child: DashApp(), + ProviderScope( + overrides: [ + settingsProvider.overrideWith( + (ref) => ThemeStateNotifier( + settingsModel: const SettingsModel() + .copyWithPath(workspaceFolderPath: "test")), + ) + ], + child: const DashApp(), ), ); } diff --git a/lib/main.dart b/lib/main.dart index b5fa019f..51fbd840 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,7 +11,10 @@ import 'app.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); var settingsModel = await getSettingsFromSharedPrefs(); - final initStatus = await initApp(settingsModel: settingsModel); + final initStatus = await initApp( + kIsDesktop, + settingsModel: settingsModel, + ); if (kIsDesktop) { await initWindow(settingsModel: settingsModel); } @@ -31,11 +34,14 @@ void main() async { ); } -Future initApp({SettingsModel? settingsModel}) async { +Future initApp( + bool initializeUsingPath, { + SettingsModel? settingsModel, +}) async { GoogleFonts.config.allowRuntimeFetching = false; try { final openBoxesStatus = await openBoxes( - kIsDesktop, + initializeUsingPath, settingsModel?.workspaceFolderPath, ); debugPrint("openBoxesStatus: $openBoxesStatus"); diff --git a/lib/services/hive_services.dart b/lib/services/hive_services.dart index fab18540..cc7f8960 100644 --- a/lib/services/hive_services.dart +++ b/lib/services/hive_services.dart @@ -11,11 +11,11 @@ const String kHistoryBoxIds = "historyIds"; const String kHistoryLazyBox = "apidash-history-lazy"; Future openBoxes( - bool isDesktop, + bool initializeUsingPath, String? workspaceFolderPath, ) async { try { - if (isDesktop) { + if (initializeUsingPath) { if (workspaceFolderPath != null) { Hive.init(workspaceFolderPath); } else { From 3fa51a39c40d2d17237df9fb9e9dbaf25b5b2ef0 Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Mon, 9 Sep 2024 05:13:24 +0530 Subject: [PATCH 70/71] Update main.dart --- lib/main.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/main.dart b/lib/main.dart index 51fbd840..e1e70ec6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -40,6 +40,8 @@ Future initApp( }) async { GoogleFonts.config.allowRuntimeFetching = false; try { + debugPrint("initializeUsingPath: $initializeUsingPath"); + debugPrint("workspaceFolderPath: ${settingsModel?.workspaceFolderPath}"); final openBoxesStatus = await openBoxes( initializeUsingPath, settingsModel?.workspaceFolderPath, From 8820d26885dc1a5b93123a72ea4bdf6b79772f54 Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Mon, 9 Sep 2024 05:13:42 +0530 Subject: [PATCH 71/71] Add clearSharedPrefs() --- integration_test/test_helper.dart | 2 ++ lib/screens/settings_page.dart | 2 ++ lib/services/shared_preferences_services.dart | 5 +++++ 3 files changed, 9 insertions(+) diff --git a/integration_test/test_helper.dart b/integration_test/test_helper.dart index 4cb151dc..f05a7cf7 100644 --- a/integration_test/test_helper.dart +++ b/integration_test/test_helper.dart @@ -1,5 +1,6 @@ import 'package:apidash/models/settings_model.dart'; import 'package:apidash/providers/providers.dart'; +import 'package:apidash/services/services.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -132,6 +133,7 @@ void apidashWidgetTest( size: width != null ? Size(width, kMinWindowSize.height) : null); await ApidashTestHelper.loadApp(widgetTester); await test(widgetTester, ApidashTestHelper(widgetTester)); + await clearSharedPrefs(); }, semanticsEnabled: false, ); diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index 8e9c8486..933b54b9 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/providers.dart'; +import '../services/services.dart'; import '../widgets/widgets.dart'; import '../common/utils.dart'; import '../consts.dart'; @@ -206,6 +207,7 @@ class SettingsPage extends ConsumerWidget { TextButton( onPressed: () async { Navigator.pop(context, 'Yes'); + await clearSharedPrefs(); await ref .read(collectionStateNotifierProvider .notifier) diff --git a/lib/services/shared_preferences_services.dart b/lib/services/shared_preferences_services.dart index af1bbd71..89d4b590 100644 --- a/lib/services/shared_preferences_services.dart +++ b/lib/services/shared_preferences_services.dart @@ -21,3 +21,8 @@ Future setSettingsToSharedPrefs(SettingsModel settingsModel) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString(kSharedPrefSettingsKey, settingsModel.toString()); } + +Future clearSharedPrefs() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(kSharedPrefSettingsKey); +}