diff --git a/README.md b/README.md index 2dbe2301..fab24a65 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ API Dash can be downloaded from the links below: | --- | --- | | HTTP | βœ… | | GraphQL | βœ… | -| SSE | https://github.com/foss42/apidash/issues/116 | +| SSE/Streaming | βœ… | | WebSocket | https://github.com/foss42/apidash/issues/15 | | MQTT | https://github.com/foss42/apidash/issues/115 | | gRPC | https://github.com/foss42/apidash/issues/14 | diff --git a/lib/consts.dart b/lib/consts.dart index c5a829d0..66972b14 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -159,6 +159,7 @@ enum ResponseBodyView { preview("Preview", Icons.visibility_rounded), code("Preview", Icons.code_rounded), raw("Raw", Icons.text_snippet_rounded), + sse("SSE", Icons.stream), none("Preview", Icons.warning); const ResponseBodyView(this.label, this.icon); @@ -182,6 +183,10 @@ const kPreviewCodeRawBodyViewOptions = [ ResponseBodyView.code, ResponseBodyView.raw ]; +const kPreviewSSERawBodyViewOptions = [ + ResponseBodyView.sse, + ResponseBodyView.raw +]; const Map>> kResponseBodyViewOptions = { @@ -195,6 +200,15 @@ const Map>> kSubTypeYaml: kCodeRawBodyViewOptions, kSubTypeXYaml: kCodeRawBodyViewOptions, kSubTypeYml: kCodeRawBodyViewOptions, + kSubTypeXNdjson: kPreviewSSERawBodyViewOptions, + kSubTypeNdjson: kPreviewSSERawBodyViewOptions, + kSubTypeJsonSeq: kPreviewSSERawBodyViewOptions, + kSubTypeXLdjson: kPreviewSSERawBodyViewOptions, + kSubTypeLdjson: kPreviewSSERawBodyViewOptions, + kSubTypeXJsonStream: kPreviewSSERawBodyViewOptions, + kSubTypeJsonStream: kPreviewSSERawBodyViewOptions, + kSubTypeJsonstream: kPreviewSSERawBodyViewOptions, + kSubTypeStreamJson: kPreviewSSERawBodyViewOptions, }, kTypeImage: { kSubTypeDefaultViewOptions: kPreviewBodyViewOptions, @@ -216,6 +230,7 @@ const Map>> kSubTypeTextXml: kCodeRawBodyViewOptions, kSubTypeTextYaml: kCodeRawBodyViewOptions, kSubTypeTextYml: kCodeRawBodyViewOptions, + kSubTypeEventStream: kPreviewSSERawBodyViewOptions, }, }; @@ -437,6 +452,7 @@ const kLabelDuplicate = "Duplicate"; const kLabelSelect = "Select"; const kLabelContinue = "Continue"; const kLabelCancel = "Cancel"; +const kLabelStop = "Stop"; const kLabelOk = "Ok"; const kLabelImport = "Import"; const kUntitled = "untitled"; diff --git a/lib/models/request_model.dart b/lib/models/request_model.dart index 4d63c2ad..b7052f81 100644 --- a/lib/models/request_model.dart +++ b/lib/models/request_model.dart @@ -22,6 +22,7 @@ class RequestModel with _$RequestModel { HttpResponseModel? httpResponseModel, @JsonKey(includeToJson: false) @Default(false) bool isWorking, @JsonKey(includeToJson: false) DateTime? sendingTime, + @JsonKey(includeToJson: false) @Default(false) bool isStreaming, String? preRequestScript, String? postRequestScript, }) = _RequestModel; diff --git a/lib/models/request_model.freezed.dart b/lib/models/request_model.freezed.dart index 72f607bd..3ba8979b 100644 --- a/lib/models/request_model.freezed.dart +++ b/lib/models/request_model.freezed.dart @@ -35,6 +35,8 @@ mixin _$RequestModel { bool get isWorking => throw _privateConstructorUsedError; @JsonKey(includeToJson: false) DateTime? get sendingTime => throw _privateConstructorUsedError; + @JsonKey(includeToJson: false) + bool get isStreaming => throw _privateConstructorUsedError; String? get preRequestScript => throw _privateConstructorUsedError; String? get postRequestScript => throw _privateConstructorUsedError; @@ -66,6 +68,7 @@ abstract class $RequestModelCopyWith<$Res> { HttpResponseModel? httpResponseModel, @JsonKey(includeToJson: false) bool isWorking, @JsonKey(includeToJson: false) DateTime? sendingTime, + @JsonKey(includeToJson: false) bool isStreaming, String? preRequestScript, String? postRequestScript}); @@ -99,6 +102,7 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel> Object? httpResponseModel = freezed, Object? isWorking = null, Object? sendingTime = freezed, + Object? isStreaming = null, Object? preRequestScript = freezed, Object? postRequestScript = freezed, }) { @@ -147,6 +151,10 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel> ? _value.sendingTime : sendingTime // ignore: cast_nullable_to_non_nullable as DateTime?, + isStreaming: null == isStreaming + ? _value.isStreaming + : isStreaming // ignore: cast_nullable_to_non_nullable + as bool, preRequestScript: freezed == preRequestScript ? _value.preRequestScript : preRequestScript // ignore: cast_nullable_to_non_nullable @@ -207,6 +215,7 @@ abstract class _$$RequestModelImplCopyWith<$Res> HttpResponseModel? httpResponseModel, @JsonKey(includeToJson: false) bool isWorking, @JsonKey(includeToJson: false) DateTime? sendingTime, + @JsonKey(includeToJson: false) bool isStreaming, String? preRequestScript, String? postRequestScript}); @@ -240,6 +249,7 @@ class __$$RequestModelImplCopyWithImpl<$Res> Object? httpResponseModel = freezed, Object? isWorking = null, Object? sendingTime = freezed, + Object? isStreaming = null, Object? preRequestScript = freezed, Object? postRequestScript = freezed, }) { @@ -287,6 +297,10 @@ class __$$RequestModelImplCopyWithImpl<$Res> ? _value.sendingTime : sendingTime // ignore: cast_nullable_to_non_nullable as DateTime?, + isStreaming: null == isStreaming + ? _value.isStreaming + : isStreaming // ignore: cast_nullable_to_non_nullable + as bool, preRequestScript: freezed == preRequestScript ? _value.preRequestScript : preRequestScript // ignore: cast_nullable_to_non_nullable @@ -315,6 +329,7 @@ class _$RequestModelImpl implements _RequestModel { this.httpResponseModel, @JsonKey(includeToJson: false) this.isWorking = false, @JsonKey(includeToJson: false) this.sendingTime, + @JsonKey(includeToJson: false) this.isStreaming = false, this.preRequestScript, this.postRequestScript}); @@ -350,13 +365,16 @@ class _$RequestModelImpl implements _RequestModel { @JsonKey(includeToJson: false) final DateTime? sendingTime; @override + @JsonKey(includeToJson: false) + final bool isStreaming; + @override final String? preRequestScript; @override final String? postRequestScript; @override String toString() { - return 'RequestModel(id: $id, apiType: $apiType, name: $name, description: $description, requestTabIndex: $requestTabIndex, httpRequestModel: $httpRequestModel, responseStatus: $responseStatus, message: $message, httpResponseModel: $httpResponseModel, isWorking: $isWorking, sendingTime: $sendingTime, preRequestScript: $preRequestScript, postRequestScript: $postRequestScript)'; + return 'RequestModel(id: $id, apiType: $apiType, name: $name, description: $description, requestTabIndex: $requestTabIndex, httpRequestModel: $httpRequestModel, responseStatus: $responseStatus, message: $message, httpResponseModel: $httpResponseModel, isWorking: $isWorking, sendingTime: $sendingTime, isStreaming: $isStreaming, preRequestScript: $preRequestScript, postRequestScript: $postRequestScript)'; } @override @@ -382,6 +400,8 @@ class _$RequestModelImpl implements _RequestModel { other.isWorking == isWorking) && (identical(other.sendingTime, sendingTime) || other.sendingTime == sendingTime) && + (identical(other.isStreaming, isStreaming) || + other.isStreaming == isStreaming) && (identical(other.preRequestScript, preRequestScript) || other.preRequestScript == preRequestScript) && (identical(other.postRequestScript, postRequestScript) || @@ -403,6 +423,7 @@ class _$RequestModelImpl implements _RequestModel { httpResponseModel, isWorking, sendingTime, + isStreaming, preRequestScript, postRequestScript); @@ -435,6 +456,7 @@ abstract class _RequestModel implements RequestModel { final HttpResponseModel? httpResponseModel, @JsonKey(includeToJson: false) final bool isWorking, @JsonKey(includeToJson: false) final DateTime? sendingTime, + @JsonKey(includeToJson: false) final bool isStreaming, final String? preRequestScript, final String? postRequestScript}) = _$RequestModelImpl; @@ -467,6 +489,9 @@ abstract class _RequestModel implements RequestModel { @JsonKey(includeToJson: false) DateTime? get sendingTime; @override + @JsonKey(includeToJson: false) + bool get isStreaming; + @override String? get preRequestScript; @override String? get postRequestScript; diff --git a/lib/models/request_model.g.dart b/lib/models/request_model.g.dart index d4f6ec20..8e0a5a68 100644 --- a/lib/models/request_model.g.dart +++ b/lib/models/request_model.g.dart @@ -27,6 +27,7 @@ _$RequestModelImpl _$$RequestModelImplFromJson(Map json) => _$RequestModelImpl( sendingTime: json['sendingTime'] == null ? null : DateTime.parse(json['sendingTime'] as String), + isStreaming: json['isStreaming'] as bool? ?? false, preRequestScript: json['preRequestScript'] as String?, postRequestScript: json['postRequestScript'] as String?, ); diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 26357574..525185a6 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:apidash_core/apidash_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -270,19 +271,20 @@ class CollectionStateNotifier Future sendRequest() async { final requestId = ref.read(selectedIdStateProvider); ref.read(codePaneVisibleStateProvider.notifier).state = false; - final defaultUriScheme = ref.read(settingsProvider).defaultUriScheme; - final EnvironmentModel? originalEnvironmentModel = - ref.read(activeEnvironmentModelProvider); if (requestId == null || state == null) { return; } - RequestModel? requestModel = state![requestId]; + RequestModel? requestModel = state![requestId]; if (requestModel?.httpRequestModel == null) { return; } + final defaultUriScheme = ref.read(settingsProvider).defaultUriScheme; + final EnvironmentModel? originalEnvironmentModel = + ref.read(activeEnvironmentModelProvider); + RequestModel executionRequestModel = requestModel!.copyWith(); if (!requestModel.preRequestScript.isNullOrEmpty()) { @@ -304,46 +306,111 @@ class CollectionStateNotifier APIType apiType = executionRequestModel.apiType; HttpRequestModel substitutedHttpRequestModel = getSubstitutedHttpRequestModel(executionRequestModel.httpRequestModel!); - - // set current model's isWorking to true and update state - var map = {...state!}; - map[requestId] = requestModel.copyWith( - isWorking: true, - sendingTime: DateTime.now(), - ); - state = map; - bool noSSL = ref.read(settingsProvider).isSSLDisabled; - var responseRec = await sendHttpRequest( + + // Set model to working and streaming + state = { + ...state!, + requestId: requestModel.copyWith( + isWorking: true, + sendingTime: DateTime.now(), + ), + }; + + final stream = await streamHttpRequest( requestId, apiType, - requestModel.httpRequestModel?.authModel, substitutedHttpRequestModel, defaultUriScheme: defaultUriScheme, noSSL: noSSL, ); - late RequestModel newRequestModel; - if (responseRec.$1 == null) { - newRequestModel = requestModel.copyWith( + HttpResponseModel? httpResponseModel; + HistoryRequestModel? historyModel; + RequestModel newRequestModel = requestModel; + bool isStreamingResponse = false; + final completer = Completer<(Response?, Duration?, String?)>(); + + StreamSubscription? sub; + + sub = stream.listen((rec) async { + if (rec == null) return; + + isStreamingResponse = rec.$1 ?? false; + final response = rec.$2; + final duration = rec.$3; + final errorMessage = rec.$4; + + if (isStreamingResponse) { + httpResponseModel = httpResponseModel?.copyWith( + time: duration, + sseOutput: [ + ...(httpResponseModel?.sseOutput ?? []), + if (response != null) response.body, + ], + ); + + newRequestModel = newRequestModel.copyWith( + httpResponseModel: httpResponseModel, + isStreaming: true, + ); + state = { + ...state!, + requestId: newRequestModel, + }; + unsave(); + + if (historyModel != null && httpResponseModel != null) { + historyModel = + historyModel!.copyWith(httpResponseModel: httpResponseModel!); + ref + .read(historyMetaStateNotifier.notifier) + .editHistoryRequest(historyModel!); + } + } + + if (!completer.isCompleted) { + completer.complete((response, duration, errorMessage)); + } + }, onDone: () { + sub?.cancel(); + state = { + ...state!, + requestId: newRequestModel.copyWith(isStreaming: false), + }; + unsave(); + }, onError: (e) { + if (!completer.isCompleted) { + completer.complete((null, null, 'StreamError: $e')); + } + }); + + final (response, duration, errorMessage) = await completer.future; + + if (response == null) { + newRequestModel = newRequestModel.copyWith( responseStatus: -1, - message: responseRec.$3, + message: errorMessage, isWorking: false, + isStreaming: false, ); } else { - final httpResponseModel = baseHttpResponseModel.fromResponse( - response: responseRec.$1!, - time: responseRec.$2!, + final statusCode = response.statusCode; + httpResponseModel = baseHttpResponseModel.fromResponse( + response: response, + time: duration, + isStreamingResponse: isStreamingResponse, ); - int statusCode = responseRec.$1!.statusCode; - newRequestModel = requestModel.copyWith( + + newRequestModel = newRequestModel.copyWith( responseStatus: statusCode, message: kResponseCodeReasons[statusCode], httpResponseModel: httpResponseModel, isWorking: false, ); + String newHistoryId = getNewUuid(); - HistoryRequestModel model = HistoryRequestModel( + historyModel = HistoryRequestModel( historyId: newHistoryId, metaData: HistoryMetaModel( historyId: newHistoryId, @@ -356,13 +423,15 @@ class CollectionStateNotifier timeStamp: DateTime.now(), ), httpRequestModel: substitutedHttpRequestModel, - httpResponseModel: httpResponseModel, + httpResponseModel: httpResponseModel!, preRequestScript: requestModel.preRequestScript, postRequestScript: requestModel.postRequestScript, authModel: requestModel.httpRequestModel?.authModel, ); - ref.read(historyMetaStateNotifier.notifier).addHistoryRequest(model); + ref + .read(historyMetaStateNotifier.notifier) + .addHistoryRequest(historyModel!); if (!requestModel.postRequestScript.isNullOrEmpty()) { newRequestModel = await handlePostResponseScript( @@ -381,10 +450,11 @@ class CollectionStateNotifier } } - // update state with response data - map = {...state!}; - map[requestId] = newRequestModel; - state = map; + // Final state update + state = { + ...state!, + requestId: newRequestModel, + }; unsave(); } diff --git a/lib/providers/history_providers.dart b/lib/providers/history_providers.dart index cc7a587c..f44edc6e 100644 --- a/lib/providers/history_providers.dart +++ b/lib/providers/history_providers.dart @@ -90,6 +90,21 @@ class HistoryMetaStateNotifier await loadHistoryRequest(id); } + void editHistoryRequest(HistoryRequestModel model) async { + final id = model.historyId; + state = { + ...state ?? {}, + id: model.metaData, + }; + final existingKeys = state?.keys.toList() ?? []; + if (!existingKeys.contains(id)) { + hiveHandler.setHistoryIds([...existingKeys, id]); + } + hiveHandler.setHistoryMeta(id, model.metaData.toJson()); + await hiveHandler.setHistoryRequest(id, model.toJson()); + await loadHistoryRequest(id); + } + Future clearAllHistory() async { await hiveHandler.clearAllHistory(); ref.read(selectedHistoryIdStateProvider.notifier).state = null; diff --git a/lib/screens/common_widgets/auth/api_key_auth_fields.dart b/lib/screens/common_widgets/auth/api_key_auth_fields.dart index edf13de9..ca11370c 100644 --- a/lib/screens/common_widgets/auth/api_key_auth_fields.dart +++ b/lib/screens/common_widgets/auth/api_key_auth_fields.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; -import 'package:apidash/widgets/widgets.dart'; +import '../common_widgets.dart'; import 'consts.dart'; class ApiKeyAuthFields extends StatefulWidget { @@ -20,24 +20,25 @@ class ApiKeyAuthFields extends StatefulWidget { } class _ApiKeyAuthFieldsState extends State { - late TextEditingController _keyController; - late TextEditingController _nameController; + late String _key; + late String _name; late String _addKeyTo; @override void initState() { super.initState(); final apiAuth = widget.authData?.apikey; - _keyController = TextEditingController(text: apiAuth?.key ?? ''); - _nameController = - TextEditingController(text: apiAuth?.name ?? kApiKeyHeaderName); + _key = apiAuth?.key ?? ''; + _name = (apiAuth?.name != null && apiAuth!.name.isNotEmpty) + ? apiAuth.name + : kApiKeyHeaderName; _addKeyTo = apiAuth?.location ?? kAddToDefaultLocation; } @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + return ListView( + shrinkWrap: true, children: [ Text( kLabelAddTo, @@ -66,20 +67,26 @@ class _ApiKeyAuthFieldsState extends State { }, ), const SizedBox(height: 16), - AuthTextField( + EnvAuthField( readOnly: widget.readOnly, - controller: _nameController, hintText: kHintTextFieldName, - onChanged: (value) => _updateApiKeyAuth(), + initialValue: _name, + onChanged: (value) { + _name = value; + _updateApiKeyAuth(); + }, ), const SizedBox(height: 16), - AuthTextField( + EnvAuthField( readOnly: widget.readOnly, - controller: _keyController, title: kLabelApiKey, hintText: kHintTextKey, isObscureText: true, - onChanged: (value) => _updateApiKeyAuth(), + initialValue: _key, + onChanged: (value) { + _key = value; + _updateApiKeyAuth(); + }, ), ], ); @@ -87,8 +94,8 @@ class _ApiKeyAuthFieldsState extends State { void _updateApiKeyAuth() { final apiKey = AuthApiKeyModel( - key: _keyController.text.trim(), - name: _nameController.text.trim(), + key: _key.trim(), + name: _name.trim(), location: _addKeyTo, ); widget.updateAuth?.call(widget.authData?.copyWith( diff --git a/lib/screens/common_widgets/auth/basic_auth_fields.dart b/lib/screens/common_widgets/auth/basic_auth_fields.dart index 5be5f1e4..5e7822d2 100644 --- a/lib/screens/common_widgets/auth/basic_auth_fields.dart +++ b/lib/screens/common_widgets/auth/basic_auth_fields.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:apidash_core/apidash_core.dart'; -import 'package:apidash/widgets/widgets.dart'; +import '../common_widgets.dart'; import 'consts.dart'; -class BasicAuthFields extends StatelessWidget { +class BasicAuthFields extends StatefulWidget { final AuthModel? authData; final Function(AuthModel?)? updateAuth; final bool readOnly; @@ -16,50 +16,55 @@ class BasicAuthFields extends StatelessWidget { }); @override - Widget build(BuildContext context) { - final usernameController = TextEditingController( - text: authData?.basic?.username ?? '', - ); - final passwordController = TextEditingController( - text: authData?.basic?.password ?? '', - ); + State createState() => _BasicAuthFieldsState(); +} - return Column( - crossAxisAlignment: CrossAxisAlignment.start, +class _BasicAuthFieldsState extends State { + late String _username; + late String _password; + + @override + void initState() { + super.initState(); + _username = widget.authData?.basic?.username ?? ''; + _password = widget.authData?.basic?.password ?? ''; + } + + @override + Widget build(BuildContext context) { + return ListView( + shrinkWrap: true, children: [ - AuthTextField( - readOnly: readOnly, + EnvAuthField( + readOnly: widget.readOnly, hintText: kHintUsername, - controller: usernameController, - onChanged: (_) => _updateBasicAuth( - usernameController, - passwordController, - ), + initialValue: _username, + onChanged: (value) { + _username = value; + _updateBasicAuth(); + }, ), const SizedBox(height: 16), - AuthTextField( - readOnly: readOnly, + EnvAuthField( + readOnly: widget.readOnly, hintText: kHintPassword, isObscureText: true, - controller: passwordController, - onChanged: (_) => _updateBasicAuth( - usernameController, - passwordController, - ), + initialValue: _password, + onChanged: (value) { + _password = value; + _updateBasicAuth(); + }, ), ], ); } - void _updateBasicAuth( - TextEditingController usernameController, - TextEditingController passwordController, - ) { + void _updateBasicAuth() { final basicAuth = AuthBasicAuthModel( - username: usernameController.text.trim(), - password: passwordController.text.trim(), + username: _username.trim(), + password: _password.trim(), ); - updateAuth?.call(authData?.copyWith( + widget.updateAuth?.call(widget.authData?.copyWith( type: APIAuthType.basic, basic: basicAuth, ) ?? diff --git a/lib/screens/common_widgets/auth/bearer_auth_fields.dart b/lib/screens/common_widgets/auth/bearer_auth_fields.dart index 2ab8cdb1..88a50f59 100644 --- a/lib/screens/common_widgets/auth/bearer_auth_fields.dart +++ b/lib/screens/common_widgets/auth/bearer_auth_fields.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:apidash_core/apidash_core.dart'; -import 'package:apidash/widgets/widgets.dart'; +import '../common_widgets.dart'; import 'consts.dart'; class BearerAuthFields extends StatefulWidget { @@ -20,29 +20,31 @@ class BearerAuthFields extends StatefulWidget { } class _BearerAuthFieldsState extends State { - late TextEditingController _tokenController; + late String _token; @override void initState() { super.initState(); - final bearerAuth = widget.authData?.bearer; - _tokenController = TextEditingController(text: bearerAuth?.token ?? ''); + _token = widget.authData?.bearer?.token ?? ''; } @override Widget build(BuildContext context) { - return AuthTextField( + return EnvAuthField( readOnly: widget.readOnly, - controller: _tokenController, hintText: kHintToken, isObscureText: true, - onChanged: (value) => _updateBearerAuth(), + initialValue: _token, + onChanged: (value) { + _token = value; + _updateBearerAuth(); + }, ); } void _updateBearerAuth() { final bearer = AuthBearerModel( - token: _tokenController.text.trim(), + token: _token.trim(), ); widget.updateAuth?.call(widget.authData?.copyWith( type: APIAuthType.bearer, diff --git a/lib/screens/common_widgets/auth/digest_auth_fields.dart b/lib/screens/common_widgets/auth/digest_auth_fields.dart index 8b060ccc..869ed1e8 100644 --- a/lib/screens/common_widgets/auth/digest_auth_fields.dart +++ b/lib/screens/common_widgets/auth/digest_auth_fields.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; -import 'package:apidash/widgets/widgets.dart'; +import '../common_widgets.dart'; import 'consts.dart'; class DigestAuthFields extends StatefulWidget { @@ -21,64 +21,76 @@ class DigestAuthFields extends StatefulWidget { } class _DigestAuthFieldsState extends State { - late TextEditingController _usernameController; - late TextEditingController _passwordController; - late TextEditingController _realmController; - late TextEditingController _nonceController; + late String _username; + late String _password; + late String _realm; + late String _nonce; late String _algorithmController; - late TextEditingController _qopController; - late TextEditingController _opaqueController; + late String _qop; + late String _opaque; @override void initState() { super.initState(); final digest = widget.authData?.digest; - _usernameController = TextEditingController(text: digest?.username ?? ''); - _passwordController = TextEditingController(text: digest?.password ?? ''); - _realmController = TextEditingController(text: digest?.realm ?? ''); - _nonceController = TextEditingController(text: digest?.nonce ?? ''); + _username = digest?.username ?? ''; + _password = digest?.password ?? ''; + _realm = digest?.realm ?? ''; + _nonce = digest?.nonce ?? ''; _algorithmController = digest?.algorithm ?? kDigestAlgos[0]; - _qopController = TextEditingController(text: digest?.qop ?? kQop[0]); - _opaqueController = TextEditingController(text: digest?.opaque ?? ''); + _qop = digest?.qop ?? kQop[0]; + _opaque = digest?.opaque ?? ''; } @override Widget build(BuildContext context) { return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: ListView( + shrinkWrap: true, children: [ - AuthTextField( + EnvAuthField( readOnly: widget.readOnly, - controller: _usernameController, hintText: kHintUsername, infoText: kInfoDigestUsername, - onChanged: (_) => _updateDigestAuth(), + initialValue: _username, + onChanged: (value) { + _username = value; + _updateDigestAuth(); + }, ), const SizedBox(height: 12), - AuthTextField( + EnvAuthField( readOnly: widget.readOnly, - controller: _passwordController, hintText: kHintPassword, isObscureText: true, infoText: kInfoDigestPassword, - onChanged: (_) => _updateDigestAuth(), + initialValue: _password, + onChanged: (value) { + _password = value; + _updateDigestAuth(); + }, ), const SizedBox(height: 12), - AuthTextField( + EnvAuthField( readOnly: widget.readOnly, - controller: _realmController, hintText: kHintRealm, infoText: kInfoDigestRealm, - onChanged: (_) => _updateDigestAuth(), + initialValue: _realm, + onChanged: (value) { + _realm = value; + _updateDigestAuth(); + }, ), const SizedBox(height: 12), - AuthTextField( + EnvAuthField( readOnly: widget.readOnly, - controller: _nonceController, hintText: kHintNonce, infoText: kInfoDigestNonce, - onChanged: (_) => _updateDigestAuth(), + initialValue: _nonce, + onChanged: (value) { + _nonce = value; + _updateDigestAuth(); + }, ), const SizedBox(height: 12), Text( @@ -106,20 +118,26 @@ class _DigestAuthFieldsState extends State { }, ), const SizedBox(height: 12), - AuthTextField( + EnvAuthField( readOnly: widget.readOnly, - controller: _qopController, hintText: kHintQop, infoText: kInfoDigestQop, - onChanged: (_) => _updateDigestAuth(), + initialValue: _qop, + onChanged: (value) { + _qop = value; + _updateDigestAuth(); + }, ), const SizedBox(height: 12), - AuthTextField( + EnvAuthField( readOnly: widget.readOnly, - controller: _opaqueController, hintText: kHintDataString, infoText: kInfoDigestDataString, - onChanged: (_) => _updateDigestAuth(), + initialValue: _opaque, + onChanged: (value) { + _opaque = value; + _updateDigestAuth(); + }, ), ], ), @@ -128,13 +146,13 @@ class _DigestAuthFieldsState extends State { void _updateDigestAuth() { final digest = AuthDigestModel( - username: _usernameController.text.trim(), - password: _passwordController.text.trim(), - realm: _realmController.text.trim(), - nonce: _nonceController.text.trim(), + username: _username.trim(), + password: _password.trim(), + realm: _realm.trim(), + nonce: _nonce.trim(), algorithm: _algorithmController.trim(), - qop: _qopController.text.trim(), - opaque: _opaqueController.text.trim(), + qop: _qop.trim(), + opaque: _opaque.trim(), ); widget.updateAuth?.call(widget.authData?.copyWith( type: APIAuthType.digest, diff --git a/lib/screens/common_widgets/auth/jwt_auth_fields.dart b/lib/screens/common_widgets/auth/jwt_auth_fields.dart index bff16157..308c953d 100644 --- a/lib/screens/common_widgets/auth/jwt_auth_fields.dart +++ b/lib/screens/common_widgets/auth/jwt_auth_fields.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; -import 'package:apidash/widgets/widgets.dart'; +import '../common_widgets.dart'; import 'consts.dart'; class JwtAuthFields extends StatefulWidget { @@ -21,7 +21,7 @@ class JwtAuthFields extends StatefulWidget { } class _JwtAuthFieldsState extends State { - late TextEditingController _secretController; + late String _secret; late TextEditingController _privateKeyController; late TextEditingController _payloadController; late String _addTokenTo; @@ -32,7 +32,7 @@ class _JwtAuthFieldsState extends State { void initState() { super.initState(); final jwt = widget.authData?.jwt; - _secretController = TextEditingController(text: jwt?.secret ?? ''); + _secret = jwt?.secret ?? ''; _privateKeyController = TextEditingController(text: jwt?.privateKey ?? ''); _payloadController = TextEditingController(text: jwt?.payload ?? ''); _addTokenTo = jwt?.addTokenTo ?? kAddToDefaultLocation; @@ -42,8 +42,8 @@ class _JwtAuthFieldsState extends State { @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + return ListView( + shrinkWrap: true, children: [ Text( kMsgAddToken, @@ -96,13 +96,16 @@ class _JwtAuthFieldsState extends State { ), const SizedBox(height: 16), if (_algorithm.startsWith(kStartAlgo)) ...[ - AuthTextField( + EnvAuthField( readOnly: widget.readOnly, - controller: _secretController, isObscureText: true, hintText: kHintSecret, infoText: kInfoSecret, - onChanged: (value) => _updateJwtAuth(), + initialValue: _secret, + onChanged: (value) { + _secret = value; + _updateJwtAuth(); + }, ), const SizedBox(height: 16), CheckboxListTile( @@ -207,7 +210,7 @@ class _JwtAuthFieldsState extends State { void _updateJwtAuth() { final jwt = AuthJwtModel( - secret: _secretController.text.trim(), + secret: _secret.trim(), privateKey: _privateKeyController.text.trim(), payload: _payloadController.text.trim(), addTokenTo: _addTokenTo, diff --git a/lib/screens/common_widgets/common_widgets.dart b/lib/screens/common_widgets/common_widgets.dart index dcbf9c70..ff914477 100644 --- a/lib/screens/common_widgets/common_widgets.dart +++ b/lib/screens/common_widgets/common_widgets.dart @@ -7,6 +7,7 @@ export 'editor_title.dart'; export 'env_regexp_span_builder.dart'; export 'env_trigger_field.dart'; export 'env_trigger_options.dart'; +export 'envfield_auth.dart'; export 'envfield_cell.dart'; export 'envfield_header.dart'; export 'envfield_url.dart'; diff --git a/lib/screens/common_widgets/env_trigger_field.dart b/lib/screens/common_widgets/env_trigger_field.dart index a7e470e7..3432135f 100644 --- a/lib/screens/common_widgets/env_trigger_field.dart +++ b/lib/screens/common_widgets/env_trigger_field.dart @@ -17,6 +17,8 @@ class EnvironmentTriggerField extends StatefulWidget { this.decoration, this.optionsWidthFactor, this.autocompleteNoTrigger, + this.readOnly = false, + this.obscureText = false }) : assert( !(controller != null && initialValue != null), 'controller and initialValue cannot be simultaneously defined.', @@ -32,6 +34,8 @@ class EnvironmentTriggerField extends StatefulWidget { final InputDecoration? decoration; final double? optionsWidthFactor; final AutocompleteNoTrigger? autocompleteNoTrigger; + final bool readOnly; + final bool obscureText; @override State createState() => @@ -125,6 +129,9 @@ class EnvironmentTriggerFieldState extends State { onTapOutside: (event) { _focusNode.unfocus(); }, + readOnly: widget.readOnly, + obscureText: widget.obscureText + ); }, ); diff --git a/lib/widgets/field_auth.dart b/lib/screens/common_widgets/envfield_auth.dart similarity index 60% rename from lib/widgets/field_auth.dart rename to lib/screens/common_widgets/envfield_auth.dart index ef60bec0..b81a84ed 100644 --- a/lib/widgets/field_auth.dart +++ b/lib/screens/common_widgets/envfield_auth.dart @@ -1,36 +1,45 @@ +import 'dart:math'; +import 'package:apidash/consts.dart'; import 'package:flutter/material.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; +import 'env_trigger_field.dart'; -class AuthTextField extends StatefulWidget { +class EnvAuthField extends StatefulWidget { final String hintText; final String? title; - final TextEditingController controller; final bool isObscureText; final Function(String)? onChanged; final bool readOnly; final String? infoText; + final String? initialValue; - const AuthTextField( + const EnvAuthField( {super.key, this.title, required this.hintText, - required this.controller, required this.onChanged, this.readOnly = false, this.isObscureText = false, - this.infoText}); + this.infoText, + this.initialValue = ""}); @override - State createState() => _AuthFieldState(); + State createState() => _AuthFieldState(); } -class _AuthFieldState extends State { +class _AuthFieldState extends State { late bool _obscureText; + late String _currentValue; @override void initState() { super.initState(); - _obscureText = widget.isObscureText; + _currentValue = widget.initialValue ?? ""; + if (_currentValue.contains("{{")) { + _obscureText = false; + } else { + _obscureText = widget.isObscureText; + } } void _toggleVisibility() { @@ -67,22 +76,35 @@ class _AuthFieldState extends State { ], ), const SizedBox(height: 6), - TextFormField( + EnvironmentTriggerField( + keyId: "auth-${widget.title ?? widget.hintText}-${Random.secure()}", + onChanged: (value) { + setState(() { + _currentValue = value; + // Update obscure text based on whether the current value contains env vars + if (value.contains("{{")) { + _obscureText = false; + } else { + _obscureText = widget.isObscureText; + } + }); + widget.onChanged?.call(value); + }, + initialValue: widget.initialValue, readOnly: widget.readOnly, - controller: widget.controller, + obscureText: _obscureText, style: kCodeStyle.copyWith( color: Theme.of(context).colorScheme.onSurface, fontSize: Theme.of(context).textTheme.bodyMedium?.fontSize, ), - decoration: InputDecoration( - filled: true, - fillColor: Theme.of(context).colorScheme.surfaceContainerLowest, - constraints: BoxConstraints( - maxWidth: MediaQuery.sizeOf(context).width - 80), - contentPadding: kP10, + decoration: getTextFieldInputDecoration( + Theme.of(context).colorScheme, hintText: widget.hintText, - hintStyle: Theme.of(context).textTheme.bodySmall, - suffixIcon: widget.isObscureText + isDense: true, + contentPadding: kIsMobile ? kPh6b12 : null, + // null when initial text contains env vars + suffixIcon: (widget.isObscureText && + !_currentValue.contains("{{")) ? IconButton( icon: Icon( _obscureText ? Icons.visibility_off : Icons.visibility, @@ -91,25 +113,7 @@ class _AuthFieldState extends State { onPressed: _toggleVisibility, ) : null, - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.outline, - ), - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - ), - ), ), - validator: (value) { - if (value!.isEmpty) { - return "${widget.hintText} cannot be empty!"; - } - return null; - }, - obscureText: _obscureText, - onChanged: widget.onChanged, ), ], ), diff --git a/lib/screens/home_page/editor_pane/url_card.dart b/lib/screens/home_page/editor_pane/url_card.dart index 829bc5c9..5aa1ce02 100644 --- a/lib/screens/home_page/editor_pane/url_card.dart +++ b/lib/screens/home_page/editor_pane/url_card.dart @@ -129,8 +129,11 @@ class SendRequestButton extends ConsumerWidget { ref.watch(selectedIdStateProvider); final isWorking = ref.watch( selectedRequestModelProvider.select((value) => value?.isWorking)); + final isStreaming = ref.watch( + selectedRequestModelProvider.select((value) => value?.isStreaming)); return SendButton( + isStreaming: isStreaming ?? false, isWorking: isWorking ?? false, onTap: () { onTap?.call(); diff --git a/lib/utils/envvar_utils.dart b/lib/utils/envvar_utils.dart index c94f6eeb..3738aa47 100644 --- a/lib/utils/envvar_utils.dart +++ b/lib/utils/envvar_utils.dart @@ -91,10 +91,91 @@ HttpRequestModel substituteHttpRequestModel( ); }).toList(), body: substituteVariables(httpRequestModel.body, combinedEnvVarMap), + authModel: + substituteAuthModel(httpRequestModel.authModel, combinedEnvVarMap), ); return newRequestModel; } +AuthModel? substituteAuthModel( + AuthModel? authModel, Map envVarMap) { + if (authModel == null) return null; + + switch (authModel.type) { + case APIAuthType.basic: + if (authModel.basic != null) { + final basic = authModel.basic!; + return authModel.copyWith( + basic: basic.copyWith( + username: substituteVariables(basic.username, envVarMap) ?? + basic.username, + password: substituteVariables(basic.password, envVarMap) ?? + basic.password, + ), + ); + } + break; + case APIAuthType.bearer: + if (authModel.bearer != null) { + final bearer = authModel.bearer!; + return authModel.copyWith( + bearer: bearer.copyWith( + token: substituteVariables(bearer.token, envVarMap) ?? bearer.token, + ), + ); + } + break; + case APIAuthType.apiKey: + if (authModel.apikey != null) { + final apiKey = authModel.apikey!; + return authModel.copyWith( + apikey: apiKey.copyWith( + key: substituteVariables(apiKey.key, envVarMap) ?? apiKey.key, + name: substituteVariables(apiKey.name, envVarMap) ?? apiKey.name, + ), + ); + } + break; + case APIAuthType.jwt: + if (authModel.jwt != null) { + final jwt = authModel.jwt!; + return authModel.copyWith( + jwt: jwt.copyWith( + secret: substituteVariables(jwt.secret, envVarMap) ?? jwt.secret, + privateKey: substituteVariables(jwt.privateKey, envVarMap) ?? + jwt.privateKey, + payload: substituteVariables(jwt.payload, envVarMap) ?? jwt.payload, + ), + ); + } + break; + case APIAuthType.digest: + if (authModel.digest != null) { + final digest = authModel.digest!; + return authModel.copyWith( + digest: digest.copyWith( + username: substituteVariables(digest.username, envVarMap) ?? + digest.username, + password: substituteVariables(digest.password, envVarMap) ?? + digest.password, + realm: substituteVariables(digest.realm, envVarMap) ?? digest.realm, + nonce: substituteVariables(digest.nonce, envVarMap) ?? digest.nonce, + qop: substituteVariables(digest.qop, envVarMap) ?? digest.qop, + opaque: + substituteVariables(digest.opaque, envVarMap) ?? digest.opaque, + ), + ); + } + break; + case APIAuthType.oauth1: + case APIAuthType.oauth2: + case APIAuthType.none: + break; + } + + return authModel; +} + List? getEnvironmentTriggerSuggestions( String query, Map> envMap, diff --git a/lib/utils/ui_utils.dart b/lib/utils/ui_utils.dart index 68eaa82c..192d816b 100644 --- a/lib/utils/ui_utils.dart +++ b/lib/utils/ui_utils.dart @@ -21,7 +21,7 @@ Color getResponseStatusCodeColor(int? statusCode, } } if (brightness == Brightness.dark) { - col = getDarkModeColor(col); + col = col.toDark; } return col; } @@ -38,7 +38,7 @@ Color getAPIColor( APIType.graphql => kColorGQL, }; if (brightness == Brightness.dark) { - col = getDarkModeColor(col); + col = col.toDark; } return col; } @@ -57,13 +57,6 @@ Color getHTTPMethodColor(HTTPVerb? method) { return col; } -Color getDarkModeColor(Color col) { - return Color.alphaBlend( - col.withValues(alpha: kOpacityDarkModeBlend), - kColorWhite, - ); -} - double? getJsonPreviewerMaxRootNodeWidth(double w) { if (w < 300) { return 150; diff --git a/lib/widgets/button_send.dart b/lib/widgets/button_send.dart index 67e78908..967872e1 100644 --- a/lib/widgets/button_send.dart +++ b/lib/widgets/button_send.dart @@ -5,11 +5,13 @@ import 'package:apidash/consts.dart'; class SendButton extends StatelessWidget { const SendButton({ super.key, + required this.isStreaming, required this.isWorking, required this.onTap, this.onCancel, }); + final bool isStreaming; final bool isWorking; final void Function() onTap; final void Function()? onCancel; @@ -17,13 +19,13 @@ class SendButton extends StatelessWidget { @override Widget build(BuildContext context) { return ADFilledButton( - onPressed: isWorking ? onCancel : onTap, - isTonal: isWorking ? true : false, - items: isWorking - ? const [ + onPressed: (isWorking || isStreaming) ? onCancel : onTap, + isTonal: (isWorking || isStreaming), + items: (isWorking || isStreaming) + ? [ kHSpacer8, Text( - kLabelCancel, + isStreaming ? kLabelStop : kLabelCancel, style: kTextStyleButton, ), kHSpacer6, diff --git a/lib/widgets/response_body.dart b/lib/widgets/response_body.dart index 561d9271..d0af2baa 100644 --- a/lib/widgets/response_body.dart +++ b/lib/widgets/response_body.dart @@ -22,6 +22,7 @@ class ResponseBody extends StatelessWidget { message: '$kNullResponseModelError $kUnexpectedRaiseIssue'); } + final isSSE = responseModel.sseOutput?.isNotEmpty ?? false; var body = responseModel.body; var formattedBody = responseModel.formattedBody; if (body == null) { @@ -35,6 +36,9 @@ class ResponseBody extends StatelessWidget { showIssueButton: false, ); } + if (isSSE) { + body = responseModel.sseOutput!.join(); + } final mediaType = responseModel.mediaType ?? MediaType(kTypeText, kSubTypePlain); @@ -61,6 +65,7 @@ class ResponseBody extends StatelessWidget { bytes: responseModel.bodyBytes!, body: body, formattedBody: formattedBody, + sseOutput: responseModel.sseOutput, highlightLanguage: highlightLanguage, ); } diff --git a/lib/widgets/response_body_success.dart b/lib/widgets/response_body_success.dart index 3774b606..9b22a4b9 100644 --- a/lib/widgets/response_body_success.dart +++ b/lib/widgets/response_body_success.dart @@ -15,13 +15,16 @@ class ResponseBodySuccess extends StatefulWidget { required this.options, required this.bytes, this.formattedBody, + this.sseOutput, this.highlightLanguage}); final MediaType mediaType; final List options; final String body; final Uint8List bytes; final String? formattedBody; + final List? sseOutput; final String? highlightLanguage; + @override State createState() => _ResponseBodySuccessState(); } @@ -144,6 +147,16 @@ class _ResponseBodySuccessState extends State { ), ), ), + ResponseBodyView.sse => Expanded( + child: Container( + width: double.maxFinite, + padding: kP8, + decoration: textContainerdecoration, + child: SSEDisplay( + sseOutput: widget.sseOutput, + ), + ), + ), } ], ), diff --git a/lib/widgets/sse_display.dart b/lib/widgets/sse_display.dart new file mode 100644 index 00000000..efa65f43 --- /dev/null +++ b/lib/widgets/sse_display.dart @@ -0,0 +1,82 @@ +import 'dart:convert'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; + +class SSEDisplay extends StatelessWidget { + final List? sseOutput; + const SSEDisplay({ + super.key, + this.sseOutput, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final fontSizeMedium = theme.textTheme.bodyMedium?.fontSize; + final isDark = theme.brightness == Brightness.dark; + if (sseOutput == null || sseOutput!.isEmpty) { + return Text( + 'No content', + style: kCodeStyle.copyWith( + fontSize: fontSizeMedium, + color: isDark ? kColorDarkDanger : kColorLightDanger, + ), + ); + } + + return ListView( + padding: kP1, + children: sseOutput!.reversed.where((e) => e != '').map((chunk) { + Map? parsedJson; + try { + parsedJson = jsonDecode(chunk); + } catch (_) {} + + return Card( + color: theme.colorScheme.surfaceContainerLowest, + shape: RoundedRectangleBorder( + borderRadius: kBorderRadius6, + ), + child: Padding( + padding: kP8, + child: parsedJson != null + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: parsedJson.entries.map((entry) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${entry.key}: ', + style: kCodeStyle.copyWith( + fontSize: fontSizeMedium, + color: isDark ? kColorGQL.toDark : kColorGQL, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 4), + Expanded( + child: Text( + entry.value.toString(), + style: kCodeStyle, + ), + ), + ], + ), + ); + }).toList(), + ) + : Text( + chunk.toString().trim(), + style: kCodeStyle.copyWith( + fontSize: fontSizeMedium, + ), + ), + ), + ); + }).toList(), + ); + } +} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 68c12aaf..8d6af58a 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -28,7 +28,6 @@ export 'editor_code.dart'; export 'editor_json.dart'; export 'editor.dart'; export 'error_message.dart'; -export 'field_auth.dart'; export 'field_cell_obscurable.dart'; export 'field_cell.dart'; export 'field_json_search.dart'; @@ -61,6 +60,7 @@ export 'splitview_drawer.dart'; export 'splitview_dashboard.dart'; export 'splitview_equal.dart'; export 'splitview_history.dart'; +export 'sse_display.dart'; export 'tabbar_segmented.dart'; export 'table_map.dart'; export 'table_request_form.dart'; diff --git a/packages/apidash_design_system/lib/extensions/color_extensions.dart b/packages/apidash_design_system/lib/extensions/color_extensions.dart new file mode 100644 index 00000000..dc34e5b8 --- /dev/null +++ b/packages/apidash_design_system/lib/extensions/color_extensions.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; +import '../tokens/colors.dart'; + +extension ColorExtension on Color { + Color get toDark => Color.alphaBlend( + withValues(alpha: kOpacityDarkModeBlend), + kColorWhite, + ); +} diff --git a/packages/apidash_design_system/lib/extensions/extensions.dart b/packages/apidash_design_system/lib/extensions/extensions.dart index 96498f9b..bae530cd 100644 --- a/packages/apidash_design_system/lib/extensions/extensions.dart +++ b/packages/apidash_design_system/lib/extensions/extensions.dart @@ -1 +1,2 @@ +export 'color_extensions.dart'; export 'context_extensions.dart'; diff --git a/packages/apidash_design_system/lib/widgets/decoration_input_textfield.dart b/packages/apidash_design_system/lib/widgets/decoration_input_textfield.dart index 0fe86fcf..ef949b82 100644 --- a/packages/apidash_design_system/lib/widgets/decoration_input_textfield.dart +++ b/packages/apidash_design_system/lib/widgets/decoration_input_textfield.dart @@ -8,6 +8,7 @@ InputDecoration getTextFieldInputDecoration( TextStyle? hintTextStyle, double? hintTextFontSize, Color? hintTextColor, + Widget? suffixIcon, EdgeInsetsGeometry? contentPadding, Color? focussedBorderColor, Color? enabledBorderColor, @@ -21,6 +22,7 @@ InputDecoration getTextFieldInputDecoration( fontSize: hintTextFontSize, color: hintTextColor ?? clrScheme.outlineVariant, ), + suffixIcon: suffixIcon, hintText: hintText, contentPadding: contentPadding ?? kP10, focusedBorder: OutlineInputBorder( diff --git a/packages/better_networking/README.md b/packages/better_networking/README.md index 45a3d324..78a14c77 100644 --- a/packages/better_networking/README.md +++ b/packages/better_networking/README.md @@ -1,73 +1,179 @@ -## better_networking +# better_networking +`better_networking` is a lightweight and extensible Dart package designed to simplify HTTP requests and streaming operations. It provides enhanced request modeling, consistent response handling, and built-in utility functions to streamline interactions with both REST and GraphQL APIs. Whether you're handling HTTP requests or streaming data (e.g., Server-Sent Events), this package makes the process more testable and developer-friendly. -### Making HTTP Requests +--- + +## πŸ”§ Features + +- **Unified request modeling** via `HttpRequestModel` +- **Consistent response handling** with `HttpResponseModel` +- **Streamed response support** (e.g., SSE) +- **Client management** with cancellation and lifecycle control +- **Built-in utilities** for parsing headers and content types +- **Support for both REST and GraphQL APIs** + +--- + +## πŸ“¦ Installation + +To install the `better_networking` package, add it to your `pubspec.yaml`: + +```yaml +dependencies: + better_networking: ^ +``` + +Then run the following command in your terminal to fetch the package: + +```bash +flutter pub get +``` + +--- + +## πŸš€ Quick Start + +Here’s a basic example to get you started with the package: ```dart -import 'package:better_networking/better_networking.dart'; +final model = HttpRequestModel( + url: 'https://api.example.com/data', + method: HTTPVerb.post, + headers: [ + NameValueModel(name: 'Authorization', value: 'Bearer '), + ], + body: '{"key": "value"}', +); final (resp, duration, err) = await sendHttpRequest( - 'Request1', - APIType.rest, - HttpRequestModel( - url: 'https://example.com', - method: HTTPVerb.post, - headers: [ - NameValueModel( - name: 'x-api-key', - value: 'AeAze8493ufhd9....', - ), - ], - params: [NameValueModel(name: 'version', value: 'v1')], - query: 'users', - body: jsonEncode({"name": "morpheus", "job": "leader"}), - formData: [ - FormDataModel( - name: 'name', - value: 'morpheus', - type: FormDataType.text, - ), - ], - ), + 'unique-request-id', + APIType.rest, + model, ); -//Similarly, Requests can be made for all the types of requests + +// To cancel the request +cancelHttpRequest('unique-request-id'); ``` -### Making Streaming Requests (SSE) +--- + +## 🧩 API Overview + +### πŸ“₯ `HttpRequestModel` + +The `HttpRequestModel` defines the structure for outgoing HTTP requests, including headers, body content, parameters, and more. + +#### Constructor: ```dart -import 'package:better_networking/better_networking.dart'; - -final stream = await streamHttpRequest( - 'S1', - APIType.rest, - HttpRequestModel( - method: HTTPVerb.get, - url: 'http://example.com', - body: jsonEncode({...}), - ), -); -stream.listen( - (data) { - print('Recieved Data: $data'); - }, - onDone: () { - print('Streaming Complete'); - }, - onError: (e) { - print(e); - }, -); +const factory HttpRequestModel({ + @Default(HTTPVerb.get) HTTPVerb method, + @Default("") String url, + List? headers, + List? params, + List? isHeaderEnabledList, + List? isParamEnabledList, + @Default(ContentType.json) ContentType bodyContentType, + String? body, + String? query, + List? formData, +}); ``` -### Cancelling Requests +#### Fields: + +- **`method`**: The HTTP verb to use (e.g., GET, POST, PUT). +- **`url`**: The target URL for the request. +- **`headers`**: A list of header key-value pairs. +- **`params`**: URL parameters as key-value pairs. +- **`isHeaderEnabledList`**: Toggles for enabling/disabling individual headers. +- **`isParamEnabledList`**: Toggles for enabling/disabling individual parameters. +- **`bodyContentType`**: The MIME type for the request body (e.g., `json`, `form`). +- **`body`**: The raw body of the request (usually a JSON or string). +- **`query`**: A custom query string to be appended to the URL. +- **`formData`**: Multipart form data (for file uploads, etc.). + +--- + +### πŸ” Request Sending Examples + +#### ➀ Standard REST HTTP Request + +This example demonstrates a simple GET request to fetch data from a REST API: + ```dart -import 'package:better_networking/better_networking.dart'; +const model = HttpRequestModel( + url: 'https://jsonplaceholder.typicode.com/posts/1', + method: HTTPVerb.get, + headers: [ + NameValueModel(name: 'User-Agent', value: 'Dart/3.0 (dart:io)'), + NameValueModel(name: 'Accept', value: 'application/json'), + ], +); -cancelHttpRequest('request-id'); +final (resp, dur, err) = await sendHttpRequest( + 'get_test', + APIType.rest, + model, +); + +final output = jsonDecode(resp?.body ?? '{}'); +print(output); ``` -### Make GraphQL Requests +#### ➀ Standard GraphQL HTTP Request + +This example demonstrates a GraphQL query: + ```dart -APIType.graphql +const model = HttpRequestModel( + url: 'https://countries.trevorblades.com/', + query: kGQLquery, // Your GraphQL query string +); + +final (resp, dur, err) = await sendHttpRequest( + 'gql_test', + APIType.graphql, + model, +); + +final output = jsonDecode(resp?.body ?? '{}'); +print(output); ``` + +#### ➀ Streamed HTTP Request (e.g., SSE) + +If you're dealing with server-sent events (SSE) or other streaming data, use the `streamHttpRequest` method: + +```dart +const model = HttpRequestModel( + url: 'https://sse.dev/test', + method: HTTPVerb.get, +); + +final stream = await streamHttpRequest('sse_test', APIType.rest, model); + +stream.listen((data) { + if (data != null) { + final HttpResponse? resp = data.$2; + final Duration? dur = data.$3; + final String? err = data.$4; + + // Handle the response here + } +}); +``` + +## 🀝 Contributing + +We welcome contributions to the `better_networking` package! If you'd like to contribute, please fork the repository and submit a pull request. For major changes or new features, it's a good idea to open an issue first to discuss your ideas. + +## Maintainer + +- Ashita Prasad ([GitHub](https://github.com/ashitaprasad), [LinkedIn](https://www.linkedin.com/in/ashitaprasad/), [X](https://x.com/ashitaprasad)) +- Manas Hejmadi (contributor) ([GitHub](https://github.com/synapsecode)) + +## License + +This project is licensed under the [Apache License 2.0](https://github.com/foss42/apidash/blob/main/packages/better_networking/LICENSE). diff --git a/packages/better_networking/analysis_options.yaml b/packages/better_networking/analysis_options.yaml index ee905fc9..c09736d1 100644 --- a/packages/better_networking/analysis_options.yaml +++ b/packages/better_networking/analysis_options.yaml @@ -3,6 +3,7 @@ include: package:flutter_lints/flutter.yaml analyzer: errors: invalid_annotation_target: ignore + no_leading_underscores_for_local_identifiers: ignore exclude: - "**/*.freezed.dart" - "**/*.g.dart" diff --git a/packages/better_networking/better_networking_example/README.md b/packages/better_networking/better_networking_example/README.md index 91c22658..8f8e04c7 100644 --- a/packages/better_networking/better_networking_example/README.md +++ b/packages/better_networking/better_networking_example/README.md @@ -1,64 +1,3 @@ -# better_networking example +# `better_networking` example -### Making HTTP Requests -```dart - final (resp, duration, err) = await sendHttpRequest( - 'Request1', - APIType.rest, - HttpRequestModel( - url: 'https://example.com', - method: HTTPVerb.post, - headers: [ - NameValueModel( - name: 'x-api-key', - value: 'AeAze8493ufhd9....', - ), - ], - params: [NameValueModel(name: 'version', value: 'v1')], - query: 'users', - body: jsonEncode({"name": "morpheus", "job": "leader"}), - formData: [ - FormDataModel( - name: 'name', - value: 'morpheus', - type: FormDataType.text, - ), - ], - ), -); -//Similarly, Requests can be made for all the types of requests -``` - -### Making Streaming Requests (SSE) -```dart -final stream = await streamHttpRequest( - 'S1', - APIType.rest, - HttpRequestModel( - method: HTTPVerb.get, - url: 'http://example.com', - body: jsonEncode({...}), - ), -); -stream.listen( - (data) { - print('Recieved Data: $data'); - }, - onDone: () { - print('Streaming Complete'); - }, - onError: (e) { - print(e); - }, -); -``` - -### Cancelling Requests -```dart -cancelHttpRequest('request-id'); -``` - -### Make GraphQL Requsts -```dart -APIType.graphql -``` \ No newline at end of file +`better_networking` package example. diff --git a/packages/better_networking/better_networking_example/lib/main.dart b/packages/better_networking/better_networking_example/lib/main.dart index 996867e4..6a7255ed 100644 --- a/packages/better_networking/better_networking_example/lib/main.dart +++ b/packages/better_networking/better_networking_example/lib/main.dart @@ -54,10 +54,10 @@ class _BetterNetworkingExampleState extends State { final (resp, duration, err) = await sendHttpRequest( 'G1', APIType.rest, - AuthModel(type: APIAuthType.none), HttpRequestModel( url: 'https://reqres.in/api/users/2', method: HTTPVerb.get, + authModel: AuthModel(type: APIAuthType.none), headers: [ NameValueModel( name: 'x-api-key', @@ -81,10 +81,10 @@ class _BetterNetworkingExampleState extends State { final (resp, duration, err) = await sendHttpRequest( 'P1', APIType.rest, - AuthModel(type: APIAuthType.none), HttpRequestModel( url: 'https://reqres.in/api/users', method: HTTPVerb.post, + authModel: AuthModel(type: APIAuthType.none), headers: [ NameValueModel( name: 'x-api-key', @@ -112,6 +112,7 @@ class _BetterNetworkingExampleState extends State { HttpRequestModel( method: HTTPVerb.post, url: 'http://localhost:11434/v1/chat/completions', + authModel: null, body: jsonEncode({ 'model': 'gemma3:latest', 'stream': true, diff --git a/packages/better_networking/lib/consts.dart b/packages/better_networking/lib/consts.dart index 21d7cb38..4719a64d 100644 --- a/packages/better_networking/lib/consts.dart +++ b/packages/better_networking/lib/consts.dart @@ -106,6 +106,15 @@ const kSubTypeYaml = 'yaml'; const kSubTypeXYaml = 'x-yaml'; const kSubTypeYml = 'x-yml'; const kSubTypeXWwwFormUrlencoded = 'x-www-form-urlencoded'; +const kSubTypeXNdjson = 'x-ndjson'; +const kSubTypeNdjson = 'ndjson'; +const kSubTypeJsonSeq = 'json-seq'; +const kSubTypeXLdjson = 'x-ldjson'; +const kSubTypeLdjson = 'ldjson'; +const kSubTypeXJsonStream = 'x-json-stream'; +const kSubTypeJsonStream = 'json-stream'; +const kSubTypeJsonstream = 'jsonstream'; +const kSubTypeStreamJson = 'stream+json'; const kTypeText = 'text'; // text @@ -118,6 +127,7 @@ const kSubTypePlain = 'plain'; const kSubTypeTextXml = 'xml'; const kSubTypeTextYaml = 'yaml'; const kSubTypeTextYml = 'yml'; +const kSubTypeEventStream = 'event-stream'; const kTypeImage = 'image'; //image @@ -131,6 +141,19 @@ const kSubTypeFormData = "form-data"; const kSubTypeDefaultViewOptions = 'all'; +List kStreamingResponseTypes = [ + '$kTypeText/$kSubTypeEventStream', + '$kTypeApplication/$kSubTypeXNdjson', + '$kTypeApplication/$kSubTypeNdjson', + '$kTypeApplication/$kSubTypeJsonSeq', + '$kTypeApplication/$kSubTypeXLdjson', + '$kTypeApplication/$kSubTypeLdjson', + '$kTypeApplication/$kSubTypeXJsonStream', + '$kTypeApplication/$kSubTypeJsonStream', + '$kTypeApplication/$kSubTypeJsonstream', + '$kTypeApplication/$kSubTypeStreamJson', +]; + enum ContentType { json("$kTypeApplication/$kSubTypeJson"), text("$kTypeText/$kSubTypePlain"), diff --git a/packages/better_networking/lib/models/http_response_model.dart b/packages/better_networking/lib/models/http_response_model.dart index b3effc76..83c96e6d 100644 --- a/packages/better_networking/lib/models/http_response_model.dart +++ b/packages/better_networking/lib/models/http_response_model.dart @@ -53,6 +53,7 @@ class HttpResponseModel with _$HttpResponseModel { String? formattedBody, @Uint8ListConverter() Uint8List? bodyBytes, @DurationConverter() Duration? time, + List? sseOutput, }) = _HttpResponseModel; factory HttpResponseModel.fromJson(Map json) => @@ -61,14 +62,20 @@ class HttpResponseModel with _$HttpResponseModel { String? get contentType => headers?.getValueContentType(); MediaType? get mediaType => getMediaTypeFromHeaders(headers); - HttpResponseModel fromResponse({required Response response, Duration? time}) { + HttpResponseModel fromResponse({ + required Response response, + Duration? time, + bool isStreamingResponse = false, + }) { final responseHeaders = mergeMaps({ HttpHeaders.contentLengthHeader: response.contentLength.toString(), }, response.headers); MediaType? mediaType = getMediaTypeFromHeaders(responseHeaders); + final body = (mediaType?.subtype == kSubTypeJson) ? utf8.decode(response.bodyBytes) : response.body; + return HttpResponseModel( statusCode: response.statusCode, headers: responseHeaders, @@ -77,6 +84,7 @@ class HttpResponseModel with _$HttpResponseModel { formattedBody: formatBody(body, mediaType), bodyBytes: response.bodyBytes, time: time, + sseOutput: isStreamingResponse ? [body] : null, ); } } diff --git a/packages/better_networking/lib/models/http_response_model.freezed.dart b/packages/better_networking/lib/models/http_response_model.freezed.dart index b4474f4e..3650c122 100644 --- a/packages/better_networking/lib/models/http_response_model.freezed.dart +++ b/packages/better_networking/lib/models/http_response_model.freezed.dart @@ -30,6 +30,7 @@ mixin _$HttpResponseModel { Uint8List? get bodyBytes => throw _privateConstructorUsedError; @DurationConverter() Duration? get time => throw _privateConstructorUsedError; + List? get sseOutput => throw _privateConstructorUsedError; /// Serializes this HttpResponseModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -56,6 +57,7 @@ abstract class $HttpResponseModelCopyWith<$Res> { String? formattedBody, @Uint8ListConverter() Uint8List? bodyBytes, @DurationConverter() Duration? time, + List? sseOutput, }); } @@ -81,6 +83,7 @@ class _$HttpResponseModelCopyWithImpl<$Res, $Val extends HttpResponseModel> Object? formattedBody = freezed, Object? bodyBytes = freezed, Object? time = freezed, + Object? sseOutput = freezed, }) { return _then( _value.copyWith( @@ -112,6 +115,10 @@ class _$HttpResponseModelCopyWithImpl<$Res, $Val extends HttpResponseModel> ? _value.time : time // ignore: cast_nullable_to_non_nullable as Duration?, + sseOutput: freezed == sseOutput + ? _value.sseOutput + : sseOutput // ignore: cast_nullable_to_non_nullable + as List?, ) as $Val, ); @@ -135,6 +142,7 @@ abstract class _$$HttpResponseModelImplCopyWith<$Res> String? formattedBody, @Uint8ListConverter() Uint8List? bodyBytes, @DurationConverter() Duration? time, + List? sseOutput, }); } @@ -159,6 +167,7 @@ class __$$HttpResponseModelImplCopyWithImpl<$Res> Object? formattedBody = freezed, Object? bodyBytes = freezed, Object? time = freezed, + Object? sseOutput = freezed, }) { return _then( _$HttpResponseModelImpl( @@ -190,6 +199,10 @@ class __$$HttpResponseModelImplCopyWithImpl<$Res> ? _value.time : time // ignore: cast_nullable_to_non_nullable as Duration?, + sseOutput: freezed == sseOutput + ? _value._sseOutput + : sseOutput // ignore: cast_nullable_to_non_nullable + as List?, ), ); } @@ -207,8 +220,10 @@ class _$HttpResponseModelImpl extends _HttpResponseModel { this.formattedBody, @Uint8ListConverter() this.bodyBytes, @DurationConverter() this.time, + final List? sseOutput, }) : _headers = headers, _requestHeaders = requestHeaders, + _sseOutput = sseOutput, super._(); factory _$HttpResponseModelImpl.fromJson(Map json) => @@ -246,10 +261,19 @@ class _$HttpResponseModelImpl extends _HttpResponseModel { @override @DurationConverter() final Duration? time; + final List? _sseOutput; + @override + List? get sseOutput { + final value = _sseOutput; + if (value == null) return null; + if (_sseOutput is EqualUnmodifiableListView) return _sseOutput; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } @override String toString() { - return 'HttpResponseModel(statusCode: $statusCode, headers: $headers, requestHeaders: $requestHeaders, body: $body, formattedBody: $formattedBody, bodyBytes: $bodyBytes, time: $time)'; + return 'HttpResponseModel(statusCode: $statusCode, headers: $headers, requestHeaders: $requestHeaders, body: $body, formattedBody: $formattedBody, bodyBytes: $bodyBytes, time: $time, sseOutput: $sseOutput)'; } @override @@ -268,7 +292,11 @@ class _$HttpResponseModelImpl extends _HttpResponseModel { (identical(other.formattedBody, formattedBody) || other.formattedBody == formattedBody) && const DeepCollectionEquality().equals(other.bodyBytes, bodyBytes) && - (identical(other.time, time) || other.time == time)); + (identical(other.time, time) || other.time == time) && + const DeepCollectionEquality().equals( + other._sseOutput, + _sseOutput, + )); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -282,6 +310,7 @@ class _$HttpResponseModelImpl extends _HttpResponseModel { formattedBody, const DeepCollectionEquality().hash(bodyBytes), time, + const DeepCollectionEquality().hash(_sseOutput), ); /// Create a copy of HttpResponseModel @@ -310,6 +339,7 @@ abstract class _HttpResponseModel extends HttpResponseModel { final String? formattedBody, @Uint8ListConverter() final Uint8List? bodyBytes, @DurationConverter() final Duration? time, + final List? sseOutput, }) = _$HttpResponseModelImpl; const _HttpResponseModel._() : super._(); @@ -332,6 +362,8 @@ abstract class _HttpResponseModel extends HttpResponseModel { @override @DurationConverter() Duration? get time; + @override + List? get sseOutput; /// Create a copy of HttpResponseModel /// with the given fields replaced by the non-null parameter values. diff --git a/packages/better_networking/lib/models/http_response_model.g.dart b/packages/better_networking/lib/models/http_response_model.g.dart index e197be09..1ce28977 100644 --- a/packages/better_networking/lib/models/http_response_model.g.dart +++ b/packages/better_networking/lib/models/http_response_model.g.dart @@ -21,6 +21,9 @@ _$HttpResponseModelImpl _$$HttpResponseModelImplFromJson(Map json) => json['bodyBytes'] as List?, ), time: const DurationConverter().fromJson((json['time'] as num?)?.toInt()), + sseOutput: (json['sseOutput'] as List?) + ?.map((e) => e as String) + .toList(), ); Map _$$HttpResponseModelImplToJson( @@ -33,4 +36,5 @@ Map _$$HttpResponseModelImplToJson( 'formattedBody': instance.formattedBody, 'bodyBytes': const Uint8ListConverter().toJson(instance.bodyBytes), 'time': const DurationConverter().toJson(instance.time), + 'sseOutput': instance.sseOutput, }; diff --git a/packages/better_networking/lib/services/http_service.dart b/packages/better_networking/lib/services/http_service.dart index 844ef69b..e5f3ff63 100644 --- a/packages/better_networking/lib/services/http_service.dart +++ b/packages/better_networking/lib/services/http_service.dart @@ -11,9 +11,16 @@ import 'http_client_manager.dart'; typedef HttpResponse = http.Response; +typedef HttpStreamOutput = ( + bool? streamOutput, + HttpResponse? resp, + Duration? dur, + String? err, +)?; + final httpClientManager = HttpClientManager(); -Future<(HttpResponse?, Duration?, String?)> sendHttpRequest( +Future<(HttpResponse?, Duration?, String?)> sendHttpRequestV1( String requestId, APIType apiType, AuthModel? authData, @@ -148,6 +155,24 @@ Future<(HttpResponse?, Duration?, String?)> sendHttpRequest( } } +Future<(HttpResponse?, Duration?, String?)> sendHttpRequest( + String requestId, + APIType apiType, + HttpRequestModel requestModel, { + SupportedUriSchemes defaultUriScheme = kDefaultUriScheme, + bool noSSL = false, +}) async { + final stream = await streamHttpRequest( + requestId, + apiType, + requestModel, + defaultUriScheme: defaultUriScheme, + noSSL: noSSL, + ); + final output = await stream.first; + return (output?.$2, output?.$3, output?.$4); +} + void cancelHttpRequest(String? requestId) { httpClientManager.cancelRequest(requestId); } @@ -176,33 +201,42 @@ http.Request prepareHttpRequest({ return request; } -Future> streamHttpRequest( +Future> streamHttpRequest( String requestId, APIType apiType, - HttpRequestModel requestModel, { + HttpRequestModel httpRequestModel, { SupportedUriSchemes defaultUriScheme = kDefaultUriScheme, bool noSSL = false, }) async { - final controller = StreamController<(String?, Duration?, String?)?>(); - StreamSubscription? subscription; + final authData = httpRequestModel.authModel; + final controller = StreamController(); + StreamSubscription?>? subscription; final stopwatch = Stopwatch()..start(); - cleanup() async { + Future _cleanup() async { stopwatch.stop(); + await subscription?.cancel(); httpClientManager.closeClient(requestId); await Future.microtask(() {}); controller.close(); } - Future handleError(dynamic error) async { + Future _addCancelledMessage() async { + if (!controller.isClosed) { + controller.add((null, null, null, kMsgRequestCancelled)); + } + httpClientManager.removeCancelledRequest(requestId); + await _cleanup(); + } + + Future _addErrorMessage(dynamic error) async { await Future.microtask(() {}); if (httpClientManager.wasRequestCancelled(requestId)) { - controller.add((null, null, kMsgRequestCancelled)); - httpClientManager.removeCancelledRequest(requestId); + await _addCancelledMessage(); } else { - controller.add((null, null, error.toString())); + controller.add((null, null, null, error.toString())); + await _cleanup(); } - await cleanup(); } controller.onCancel = () async { @@ -211,61 +245,145 @@ Future> streamHttpRequest( }; if (httpClientManager.wasRequestCancelled(requestId)) { - controller.add((null, null, kMsgRequestCancelled)); - httpClientManager.removeCancelledRequest(requestId); - controller.close(); + await _addCancelledMessage(); return controller.stream; } final client = httpClientManager.createClient(requestId, noSSL: noSSL); + + HttpRequestModel authenticatedHttpRequestModel = httpRequestModel.copyWith(); + + try { + if (authData != null && authData.type != APIAuthType.none) { + authenticatedHttpRequestModel = await handleAuth( + httpRequestModel, + authData, + ); + } + } catch (e) { + await _addErrorMessage(e.toString()); + return controller.stream; + } + final (uri, uriError) = getValidRequestUri( - requestModel.url, - requestModel.enabledParams, + authenticatedHttpRequestModel.url, + authenticatedHttpRequestModel.enabledParams, defaultUriScheme: defaultUriScheme, ); if (uri == null) { - await handleError(uriError ?? 'Invalid URL'); + await _addErrorMessage(uriError ?? 'Invalid URL'); return controller.stream; } + try { + final streamedResponse = await makeStreamedRequest( + client: client, + uri: uri, + requestModel: authenticatedHttpRequestModel, + apiType: apiType, + ); + + HttpResponse _createResponseFromBytes(List bytes) { + return HttpResponse.bytes( + bytes, + streamedResponse.statusCode, + request: streamedResponse.request, + headers: streamedResponse.headers, + isRedirect: streamedResponse.isRedirect, + persistentConnection: streamedResponse.persistentConnection, + reasonPhrase: streamedResponse.reasonPhrase, + ); + } + + final contentType = + getMediaTypeFromHeaders(streamedResponse.headers)?.mimeType ?? ''; + final chunkList = >[]; + + subscription = streamedResponse.stream.listen( + (bytes) async { + if (controller.isClosed) return; + final isStreaming = kStreamingResponseTypes.contains(contentType); + if (isStreaming) { + final response = _createResponseFromBytes(bytes); + controller.add((true, response, stopwatch.elapsed, null)); + } else { + chunkList.add(bytes); + } + }, + onDone: () async { + if (chunkList.isNotEmpty && !controller.isClosed) { + final allBytes = chunkList.expand((x) => x).toList(); + final response = _createResponseFromBytes(allBytes); + final isStreaming = kStreamingResponseTypes.contains(contentType); + controller.add((isStreaming, response, stopwatch.elapsed, null)); + chunkList.clear(); + } else { + final response = _createResponseFromBytes([]); + controller.add((false, response, stopwatch.elapsed, null)); + } + await _cleanup(); + }, + onError: _addErrorMessage, + ); + return controller.stream; + } catch (e) { + await _addErrorMessage(e); + return controller.stream; + } +} + +Future makeStreamedRequest({ + required http.Client client, + required Uri uri, + required HttpRequestModel requestModel, + required APIType apiType, +}) async { final headers = requestModel.enabledHeadersMap; final hasBody = kMethodsWithBody.contains(requestModel.method); final isMultipart = requestModel.bodyContentType == ContentType.formdata; - try { - //HANDLE MULTI-PART - if (apiType == APIType.rest && isMultipart && hasBody) { - final multipart = http.MultipartRequest( - requestModel.method.name.toUpperCase(), - uri, - )..headers.addAll(headers); + http.StreamedResponse streamedResponse; - for (final data in requestModel.formDataList) { - if (data.type == FormDataType.text) { - multipart.fields[data.name] = data.value; - } else { - multipart.files.add( - await http.MultipartFile.fromPath(data.name, data.value), - ); + //----------------- Request Creation --------------------- + //Handling HTTP Multipart Requests + if (apiType == APIType.rest && isMultipart && hasBody) { + final multipart = http.MultipartRequest( + requestModel.method.name.toUpperCase(), + uri, + )..headers.addAll(headers); + for (final data in requestModel.formDataList) { + if (data.type == FormDataType.text) { + multipart.fields[data.name] = data.value; + } else { + multipart.files.add( + await http.MultipartFile.fromPath(data.name, data.value), + ); + } + } + streamedResponse = await client.send(multipart); + } else if (apiType == APIType.graphql) { + // Handling GraphQL Requests + var requestBody = getGraphQLBody(requestModel); + String? body; + if (requestBody != null) { + var contentLength = utf8.encode(requestBody).length; + if (contentLength > 0) { + body = requestBody; + headers[HttpHeaders.contentLengthHeader] = contentLength.toString(); + if (!requestModel.hasContentTypeHeader) { + headers[HttpHeaders.contentTypeHeader] = ContentType.json.header; } } - - final streamedResponse = await client.send(multipart); - final stream = streamTextResponse(streamedResponse); - - subscription = stream.listen( - (data) => controller.add((data, stopwatch.elapsed, null)), - onDone: () => cleanup(), - onError: handleError, - ); - - return controller.stream; } - + final request = http.Request('POST', uri) + ..headers.addAll(headers) + ..body = body ?? ''; + streamedResponse = await client.send(request); + } else { + //Handling regular REST Requests String? body; bool overrideContentType = false; - if (hasBody && requestModel.body?.isNotEmpty == true) { body = requestModel.body; if (!requestModel.hasContentTypeHeader) { @@ -275,7 +393,6 @@ Future> streamHttpRequest( overrideContentType = true; } } - final request = prepareHttpRequest( url: uri, method: requestModel.method.name.toUpperCase(), @@ -283,23 +400,7 @@ Future> streamHttpRequest( body: body, overrideContentType: overrideContentType, ); - - final streamedResponse = await client.send(request); - final stream = streamTextResponse(streamedResponse); - - subscription = stream.listen( - (data) { - if (!controller.isClosed) { - controller.add((data, stopwatch.elapsed, null)); - } - }, - onDone: () => cleanup(), - onError: handleError, - ); - - return controller.stream; - } catch (e) { - await handleError(e); - return controller.stream; + streamedResponse = await client.send(request); } + return streamedResponse; } diff --git a/packages/better_networking/lib/utils/auth/handle_auth.dart b/packages/better_networking/lib/utils/auth/handle_auth.dart index 795281ac..b8e2174c 100644 --- a/packages/better_networking/lib/utils/auth/handle_auth.dart +++ b/packages/better_networking/lib/utils/auth/handle_auth.dart @@ -114,7 +114,6 @@ Future handleAuth( final httpResult = await sendHttpRequest( "digest-${Random.secure()}", APIType.rest, - null, httpRequestModel, ); final httpResponse = httpResult.$1; diff --git a/packages/better_networking/lib/utils/http_response_utils.dart b/packages/better_networking/lib/utils/http_response_utils.dart index ac4a143a..c5ebfda1 100644 --- a/packages/better_networking/lib/utils/http_response_utils.dart +++ b/packages/better_networking/lib/utils/http_response_utils.dart @@ -50,20 +50,3 @@ Future convertStreamedResponse( return response; } - -Stream streamTextResponse( - http.StreamedResponse streamedResponse, -) async* { - try { - if (streamedResponse.statusCode != 200) { - final errorText = await streamedResponse.stream.bytesToString(); - throw Exception('${streamedResponse.statusCode}\n$errorText'); - } - final utf8Stream = streamedResponse.stream.transform(utf8.decoder); - await for (final chunk in utf8Stream) { - yield chunk; - } - } catch (e) { - rethrow; - } -} diff --git a/packages/better_networking/test/extensions/map_extensions_test.dart b/packages/better_networking/test/extensions/map_extensions_test.dart index bbc499ff..9a0a2034 100644 --- a/packages/better_networking/test/extensions/map_extensions_test.dart +++ b/packages/better_networking/test/extensions/map_extensions_test.dart @@ -124,4 +124,22 @@ void main() { }, ); }); + + group("Testing removeKeyContentType() function", () { + test('Removes lowercase content-type key', () { + Map header1 = { + "content-type": "application/json", + "authorization": "Bearer token", + }; + header1.removeKeyContentType(); + expect(header1.containsKey("content-type"), false); + expect(header1.containsKey("authorization"), true); + }); + + test('Preserves original map after mutation', () { + final header4 = {"Content-Type": "application/json", "X-Custom": "value"}; + final result = header4.removeKeyContentType(); + expect(identical(result, header4), true); + }); + }); } diff --git a/packages/better_networking/test/models/http_request_model_test.dart b/packages/better_networking/test/models/http_request_model_test.dart new file mode 100644 index 00000000..a89f90bc --- /dev/null +++ b/packages/better_networking/test/models/http_request_model_test.dart @@ -0,0 +1,87 @@ +import 'package:better_networking/better_networking.dart'; +import 'package:test/test.dart'; + +void main() { + group('HttpRequestModel', () { + test('should correctly convert to and from JSON', () { + const model = HttpRequestModel( + method: HTTPVerb.post, + url: 'https://api.example.com', + headers: [ + NameValueModel(name: 'Content-Type', value: 'application/json'), + ], + params: [NameValueModel(name: 'q', value: 'flutter')], + isHeaderEnabledList: [true], + isParamEnabledList: [true], + bodyContentType: ContentType.json, + body: '{"key": "value"}', + ); + + final json = model.toJson(); + final fromJson = HttpRequestModel.fromJson(json); + + expect(fromJson.method, HTTPVerb.post); + expect(fromJson.url, 'https://api.example.com'); + expect(fromJson.bodyContentType, ContentType.json); + expect(fromJson.headers?.first.name, 'Content-Type'); + }); + + test('enabled headers and params map', () { + const model = HttpRequestModel( + headers: [ + NameValueModel(name: 'Accept', value: 'application/json'), + NameValueModel(name: 'X-Test', value: 'test'), + ], + isHeaderEnabledList: [true, false], + params: [NameValueModel(name: 'search', value: 'dart')], + isParamEnabledList: [true], + ); + + expect(model.enabledHeadersMap.length, 1); + expect(model.enabledHeadersMap.containsKey('Accept'), true); + expect(model.enabledHeadersMap.containsKey('X-Test'), false); + + expect(model.enabledParamsMap.length, 1); + expect(model.enabledParamsMap.containsKey('search'), true); + }); + + test('content type checks', () { + const model = HttpRequestModel(bodyContentType: ContentType.formdata); + expect(model.hasFormDataContentType, true); + expect(model.hasJsonContentType, false); + }); + + test('hasBody logic', () { + const modelJson = HttpRequestModel( + method: HTTPVerb.post, + bodyContentType: ContentType.json, + body: '{"hello":"world"}', + ); + + expect(modelJson.hasBody, true); + expect(modelJson.hasJsonData, true); + expect(modelJson.hasTextData, false); + }); + + test('formData processing and file check', () { + const formData = [ + FormDataModel( + name: 'file1', + value: 'file.txt', + type: FormDataType.file, + ), + FormDataModel(name: 'username', value: 'test', type: FormDataType.text), + ]; + + const model = HttpRequestModel( + method: HTTPVerb.post, + bodyContentType: ContentType.formdata, + formData: formData, + ); + + expect(model.hasFormData, true); + expect(model.formDataMapList.length, 2); + expect(model.hasFileInFormData, true); + }); + }); +} diff --git a/packages/better_networking/test/models/http_response_model_test.dart b/packages/better_networking/test/models/http_response_model_test.dart new file mode 100644 index 00000000..7260b965 --- /dev/null +++ b/packages/better_networking/test/models/http_response_model_test.dart @@ -0,0 +1,96 @@ +import 'dart:typed_data'; +import 'package:better_networking/consts.dart'; +import 'package:better_networking/models/models.dart'; +import 'package:test/test.dart'; +import 'package:http/http.dart' as http; + +void main() { + group('HttpResponseModel', () { + test('should serialize and deserialize correctly', () { + final model = HttpResponseModel( + statusCode: 200, + headers: {'Content-Type': 'application/json'}, + requestHeaders: {'Accept': 'application/json'}, + body: '{"message":"ok"}', + formattedBody: '{"message":"ok"}', + bodyBytes: Uint8List.fromList([123, 34, 109, 101]), + time: const Duration(milliseconds: 300), + sseOutput: ['event1', 'event2'], + ); + + final json = model.toJson(); + final fromJson = HttpResponseModel.fromJson(json); + + expect(fromJson.statusCode, 200); + expect(fromJson.headers?['Content-Type'], 'application/json'); + expect(fromJson.body, '{"message":"ok"}'); + expect(fromJson.time?.inMilliseconds, 300); + expect(fromJson.bodyBytes, isNotNull); + expect(fromJson.sseOutput?.length, 2); + }); + + test('contentType and mediaType getters', () { + const model = HttpResponseModel( + headers: {'Content-Type': 'application/json; charset=utf-8'}, + ); + + expect(model.contentType, 'application/json; charset=utf-8'); + expect(model.mediaType?.mimeType, 'application/json'); + }); + + test('fromResponse returns valid model (JSON body)', () { + final http.Response response = http.Response( + '{"key":"value"}', + 200, + headers: {'content-type': 'application/json'}, + ); + + final model = const HttpResponseModel().fromResponse( + response: response, + time: const Duration(milliseconds: 150), + ); + + expect(model.statusCode, 200); + expect(model.body, '{"key":"value"}'); + expect(model.mediaType?.subtype, kSubTypeJson); + expect(model.time?.inMilliseconds, 150); + }); + + test('fromResponse returns valid model (plain text body)', () { + final http.Response response = http.Response( + 'This is plain text.', + 200, + headers: {'content-type': 'text/plain'}, + ); + + final model = const HttpResponseModel().fromResponse( + response: response, + time: const Duration(milliseconds: 50), + ); + + expect(model.statusCode, 200); + expect(model.body, 'This is plain text.'); + }); + + test('Uint8ListConverter converts correctly', () { + const converter = Uint8ListConverter(); + final input = Uint8List.fromList([1, 2, 3, 4]); + + final json = converter.toJson(input); + final output = converter.fromJson(json); + + expect(output, isA()); + expect(output, equals(input)); + }); + + test('DurationConverter converts correctly', () { + const converter = DurationConverter(); + const input = Duration(seconds: 1, microseconds: 500); + + final json = converter.toJson(input); + final output = converter.fromJson(json); + + expect(output?.inMicroseconds, input.inMicroseconds); + }); + }); +} diff --git a/packages/better_networking/test/services/http_client_manager_test.dart b/packages/better_networking/test/services/http_client_manager_test.dart new file mode 100644 index 00000000..d3a73d73 --- /dev/null +++ b/packages/better_networking/test/services/http_client_manager_test.dart @@ -0,0 +1,45 @@ +import 'package:better_networking/better_networking.dart'; +import 'package:flutter/foundation.dart'; +import 'package:test/test.dart'; +import 'package:http/io_client.dart'; + +void main() { + group('HttpClientManager', () { + late HttpClientManager manager; + + setUp(() { + manager = HttpClientManager(); + }); + + test('createHttpClientWithNoSSL: returns an IOClient when not on web', () { + if (!kIsWeb) { + final client = manager.createClient('req2', noSSL: true); + expect(client, isA()); + } + }); + + test('should track active client correctly', () { + const requestId = 'req1'; + manager.createClient(requestId); + expect(manager.hasActiveClient(requestId), isTrue); + + manager.closeClient(requestId); + expect(manager.hasActiveClient(requestId), isFalse); + }); + + test('should limit _cancelledRequests and remove oldest', () { + // Inject 101 cancelled request IDs + for (int i = 0; i < 101; i++) { + final requestId = 'cancel_$i'; + manager.createClient(requestId); + manager.cancelRequest(requestId); + } + + // Oldest should be 'cancel_0' and should have been removed + expect(manager.wasRequestCancelled('cancel_0'), isFalse); + + // The newest should still be in the cancelled set + expect(manager.wasRequestCancelled('cancel_100'), isTrue); + }); + }); +} diff --git a/packages/better_networking/test/services/http_service_test.dart b/packages/better_networking/test/services/http_service_test.dart new file mode 100644 index 00000000..8eb095d3 --- /dev/null +++ b/packages/better_networking/test/services/http_service_test.dart @@ -0,0 +1,9 @@ +// import '../services/request_types/gql_test.dart' as gql_test; +// import '../services/request_types/rest_test.dart' as rest_test; +// import '../services/request_types/sse_test.dart' as sse_test; + +void main() { + // gql_test.main(); + // rest_test.main(); + // sse_test.main(); +} diff --git a/packages/better_networking/test/services/request_types/gql_test.dart b/packages/better_networking/test/services/request_types/gql_test.dart new file mode 100644 index 00000000..e90a8d59 --- /dev/null +++ b/packages/better_networking/test/services/request_types/gql_test.dart @@ -0,0 +1,87 @@ +import 'dart:convert'; +import 'package:better_networking/consts.dart'; +import 'package:better_networking/models/http_request_model.dart'; +import 'package:better_networking/services/http_service.dart'; +import 'package:test/test.dart'; + +const kGQLquery = r'''query { + country(code: "IN") { + name + capital + emoji + } + } + '''; + +void main() { + group('streamHttpRequest: GraphQL Specific Tests', () { + test('GraphQL Test (Should get the correct data)', () async { + const model = HttpRequestModel( + url: 'https://countries.trevorblades.com/', + query: kGQLquery, + ); + + final stream = await streamHttpRequest( + 'graphql_test', + APIType.graphql, + model, + ); + + final response = await stream.first; + + expect(response, isNotNull, reason: 'No response from GraphQL server'); + final body = response!.$2?.body ?? '{}'; + + final decoded = jsonDecode(body); + final errors = decoded['errors']; + if (errors != null) { + fail('GraphQL Error: ${jsonEncode(errors)}'); + } + + final country = decoded['data']?['country']; + expect(country, isNotNull, reason: 'No country data found'); + expect(country['name'], equals('India')); + expect(country['capital'], isNotEmpty); + expect(country['emoji'], isNotEmpty); + }); + + test('GraphQL: Invalid Query should return error', () async { + const badQuery = r'''query { invalidField }'''; + const model = HttpRequestModel( + url: 'https://countries.trevorblades.com/', + query: badQuery, + ); + final stream = await streamHttpRequest( + 'graphql_bad', + APIType.graphql, + model, + ); + final output = await stream.first; + + final body = output!.$2?.body ?? '{}'; + final decoded = jsonDecode(body); + final errors = decoded['errors']; + expect( + errors, + isNotNull, + reason: 'Invalid GraphQL query must return error', + ); + }); + + test('GraphQL: Cancellation should work', () async { + const model = HttpRequestModel( + url: 'https://countries.trevorblades.com/', + query: kGQLquery, + ); + final stream = await streamHttpRequest( + 'graphql_test_cancellation', + APIType.graphql, + model, + ); + httpClientManager.cancelRequest('graphql_test_cancellation'); + final output = await stream.last; + final errMsg = output?.$4; + expect(errMsg, 'Request Cancelled'); + }); + }); +} diff --git a/packages/better_networking/test/services/request_types/rest_test.dart b/packages/better_networking/test/services/request_types/rest_test.dart new file mode 100644 index 00000000..d8e7492a --- /dev/null +++ b/packages/better_networking/test/services/request_types/rest_test.dart @@ -0,0 +1,222 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:better_networking/better_networking.dart'; +import 'package:flutter/material.dart'; +import 'package:test/test.dart'; + +void main() { + group('sendHttpRequest: REST Specific Tests', () { + test('GET (Regular)', () async { + const model = HttpRequestModel( + url: 'https://api.apidash.dev', + method: HTTPVerb.get, + headers: [ + NameValueModel(name: 'User-Agent', value: 'Dart/3.0 (dart:io)'), + NameValueModel(name: 'Accept', value: 'application/json'), + ], + ); + final (resp, dur, err) = await sendHttpRequest( + 'get_test', + APIType.rest, + model, + ); + final output = jsonDecode(resp?.body ?? '{}'); + expect(resp?.statusCode == 200, true, reason: 'Response must be 200'); + expect(jsonEncode(output), contains('doc')); + }); + }); + + group('streamHttpRequest: MULTIPART', () { + test('MULTIPART (Regular)', () async { + final tempDir = Directory.systemTemp.createTempSync(); + const fullName = 'temp_image.png'; + const token = 'xyz'; + final file = File('${tempDir.path}/$fullName'); + file.writeAsBytesSync( + Uint8List.fromList([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]), + ); + final model = HttpRequestModel( + method: HTTPVerb.post, + url: 'https://api.apidash.dev/io/img', + bodyContentType: ContentType.formdata, + body: r"""{ +"text": "I LOVE Flutter" +}""", + formData: [ + const FormDataModel( + name: "token", + value: token, + type: FormDataType.text, + ), + FormDataModel( + name: "imfile", + value: file.path, + type: FormDataType.file, + ), + ], + ); + final (resp, dur, err) = await sendHttpRequest( + 'mpreq', + APIType.rest, + model, + ); + final output = jsonDecode(resp?.body ?? '{}'); + expect(resp?.statusCode == 200, true, reason: 'Response must be 200'); + expect( + jsonEncode(output), + contains(fullName), + reason: 'Response must contain filename', + ); + expect( + jsonEncode(output), + contains(token), + reason: 'Response must contain token', + ); + }); + }); + + group('streamHttpRequest: REST Specific Tests', () { + test('GET (Regular)', () async { + const model = HttpRequestModel( + url: 'https://api.apidash.dev', + method: HTTPVerb.get, + headers: [ + NameValueModel(name: 'User-Agent', value: 'Dart/3.0 (dart:io)'), + NameValueModel(name: 'Accept', value: 'application/json'), + ], + ); + final stream = await streamHttpRequest('get_test', APIType.rest, model); + final output = await stream.first; + expect( + output?.$2?.statusCode == 200, + true, + reason: 'Response must be 200', + ); + expect(output?.$2?.body, contains('doc')); + }); + + test('POST (JSON Body)', () async { + const model = HttpRequestModel( + url: 'https://api.apidash.dev/case/lower', + method: HTTPVerb.post, + headers: [ + NameValueModel(name: 'Content-Type', value: 'application/json'), + ], + body: r"""{ +"text": "I LOVE Flutter" +}""", + ); + + final stream = await streamHttpRequest('post_test', APIType.rest, model); + final output = await stream.first; + + expect(output?.$2?.statusCode, equals(200), reason: 'Expected 200 Ok'); + final body = jsonDecode(output!.$2!.body); + expect(body['data'], equals('i love flutter')); + }); + + test('Empty URL should be handled', () async { + const model = HttpRequestModel(url: '', method: HTTPVerb.get); + final stream = await streamHttpRequest( + 'empty_url_test', + APIType.rest, + model, + ); + final output = await stream.first; + expect( + output?.$4, + 'URL is missing!', + reason: 'Should show that URL is missing', + ); + }); + + test('Should Show Error for invalid URL', () async { + const model = HttpRequestModel(url: '', method: HTTPVerb.get); + final stream = await streamHttpRequest( + 'invalid_url_test', + APIType.rest, + model, + ); + final output = await stream.first; + + expect( + output?.$4, + isNotNull, + reason: 'Should return error for invalid URL', + ); + }); + + test('REST: Large response body', () async { + const model = HttpRequestModel( + url: 'https://api.github.com/repos/foss42/apidash', + method: HTTPVerb.get, + ); + + final stream = await streamHttpRequest( + 'large_body_test', + APIType.rest, + model, + ); + final output = await stream.first; + + final body = output!.$2?.body; + expect(body, isNotNull); + expect( + body!.length, + greaterThan(500), + reason: 'Response should be large enough', + ); + }); + + test('REST: Cancellation', () async { + const model = HttpRequestModel( + url: 'https://api.apidash.dev/io/delay', + method: HTTPVerb.get, + params: [NameValueModel(name: 'wait', value: '5')], + headers: [ + NameValueModel(name: 'User-Agent', value: 'Dart/3.0 (dart:io)'), + NameValueModel(name: 'Accept', value: 'application/json'), + ], + ); + Future.delayed(const Duration(seconds: 2), () { + debugPrint("Stream canceled"); + cancelHttpRequest('get_test_c'); + }); + debugPrint("Stream start"); + final stream = await streamHttpRequest('get_test_c', APIType.rest, model); + debugPrint("Stream get output"); + final output = await stream.first; + final errMsg = output?.$4; + expect(errMsg, 'Request Cancelled'); + }); + }); + + group('Testing overrideContentType functionality', () { + test('overrideContentType is true', () async { + final request = prepareHttpRequest( + url: Uri.parse('https://www.example.com'), + method: 'POST', + body: 'Hello', + headers: {'content-type': 'application/json'}, + overrideContentType: true, + ); + expect(request.headers['content-type'], 'application/json'); + }); + + test('overrideContentType is false', () async { + final request = prepareHttpRequest( + url: Uri.parse('https://www.example.com'), + method: 'POST', + body: 'Hello', + headers: {'content-type': 'application/json'}, + overrideContentType: false, + ); + expect(request.headers['content-type'], isNot('application/json')); + expect( + request.headers['content-type'], + 'application/json; charset=utf-8', + ); + }); + }); +} diff --git a/packages/better_networking/test/services/request_types/sse_test.dart b/packages/better_networking/test/services/request_types/sse_test.dart new file mode 100644 index 00000000..b065e02b --- /dev/null +++ b/packages/better_networking/test/services/request_types/sse_test.dart @@ -0,0 +1,57 @@ +import 'package:better_networking/consts.dart'; +import 'package:better_networking/models/http_request_model.dart'; +import 'package:better_networking/services/http_service.dart'; +import 'package:test/test.dart'; + +void main() { + group('streamHttpRequest: SSE Specific Tests', () { + test( + 'SSE Stream - Should receive at least two events in 4 seconds', + () async { + const model = HttpRequestModel( + url: 'https://sse.dev/test', + method: HTTPVerb.get, + ); + + final stream = await streamHttpRequest('sse_test', APIType.rest, model); + + final outputs = []; + final subscription = stream.listen(outputs.add); + + await Future.delayed(const Duration(seconds: 4)); + await subscription.cancel(); + + final eventCount = outputs.where((e) => e?.$1 == true).length; + expect( + eventCount, + greaterThanOrEqualTo(2), + reason: 'Output -> $outputs', + ); + }, + timeout: const Timeout(Duration(seconds: 12)), + ); + + test( + 'SSE Stream - Cancellation should work', + () async { + const model = HttpRequestModel( + url: 'https://sse.dev/test', + method: HTTPVerb.get, + ); + + final stream = await streamHttpRequest('sse_test', APIType.rest, model); + final outputs = []; + final subscription = stream.listen(outputs.add); + + await Future.delayed(const Duration(seconds: 1)); + httpClientManager.cancelRequest('sse_test'); + await Future.delayed(const Duration(milliseconds: 300)); + await subscription.cancel(); + + final errMsg = outputs.lastOrNull?.$4; + expect(errMsg, 'Request Cancelled'); + }, + timeout: const Timeout(Duration(seconds: 12)), + ); + }); +} diff --git a/packages/better_networking/test/utils/auth/auth_handling_test.dart b/packages/better_networking/test/utils/auth/auth_handling_test.dart index 4448da5a..40f2cb2a 100644 --- a/packages/better_networking/test/utils/auth/auth_handling_test.dart +++ b/packages/better_networking/test/utils/auth/auth_handling_test.dart @@ -14,7 +14,6 @@ void main() { final result = await sendHttpRequest( 'test-request', APIType.rest, - null, httpRequestModel, ); diff --git a/packages/better_networking/test/utils/content_type_utils_test.dart b/packages/better_networking/test/utils/content_type_utils_test.dart new file mode 100644 index 00000000..de36b30c --- /dev/null +++ b/packages/better_networking/test/utils/content_type_utils_test.dart @@ -0,0 +1,101 @@ +import 'package:better_networking/better_networking.dart'; +import 'package:test/test.dart'; + +void main() { + group('getMediaTypeFromContentType', () { + test('Parses valid content type', () { + final mediaType = getMediaTypeFromContentType('application/json'); + expect(mediaType, isNotNull); + expect(mediaType!.type, 'application'); + expect(mediaType.subtype, 'json'); + }); + + test('Returns null on invalid content type', () { + final mediaType = getMediaTypeFromContentType('not-a-valid-header'); + expect(mediaType, isNull); + }); + + test('Returns null when input is null', () { + final mediaType = getMediaTypeFromContentType(null); + expect(mediaType, isNull); + }); + }); + + group('getContentTypeFromMediaType', () { + test('Returns json content type for application/json', () { + final mediaType = MediaType.parse('application/json'); + expect(getContentTypeFromMediaType(mediaType), ContentType.json); + }); + + test('Returns formdata for multipart/form-data', () { + final mediaType = MediaType.parse('multipart/form-data'); + expect(getContentTypeFromMediaType(mediaType), ContentType.formdata); + }); + + test('Returns text for other types', () { + final mediaType = MediaType.parse('text/html'); + expect(getContentTypeFromMediaType(mediaType), ContentType.text); + }); + + test('Returns null for null mediaType', () { + expect(getContentTypeFromMediaType(null), isNull); + }); + }); + + group('getMediaTypeFromHeaders', () { + test('Extracts MediaType from headers map', () { + final headers = {"Content-Type": "application/json"}; + final mediaType = getMediaTypeFromHeaders(headers); + expect(mediaType, isNotNull); + expect(mediaType!.type, 'application'); + expect(mediaType.subtype, 'json'); + }); + + test('Returns null if Content-Type is missing', () { + final headers = {"Accept": "application/json"}; + expect(getMediaTypeFromHeaders(headers), isNull); + }); + }); + + group('getContentTypeFromHeadersMap', () { + test('Returns ContentType.json from headers map', () { + final headers = {"content-type": "application/json"}; + expect(getContentTypeFromHeadersMap(headers), ContentType.json); + }); + + test('Returns ContentType.formdata from headers map', () { + final headers = {"Content-Type": "multipart/form-data"}; + expect(getContentTypeFromHeadersMap(headers), ContentType.formdata); + }); + + test('Returns null when headers map is null', () { + expect(getContentTypeFromHeadersMap(null), isNull); + }); + + test('Returns null when Content-Type is missing', () { + final headers = {"Accept": "application/json"}; + expect(getContentTypeFromHeadersMap(headers), isNull); + }); + }); + + group('getContentTypeFromContentTypeStr', () { + test('Correctly parses string into ContentType', () { + expect( + getContentTypeFromContentTypeStr('application/json'), + ContentType.json, + ); + expect( + getContentTypeFromContentTypeStr('multipart/form-data'), + ContentType.formdata, + ); + }); + + test('Returns null for invalid string', () { + expect(getContentTypeFromContentTypeStr('invalid-content-type'), isNull); + }); + + test('Returns null for null input', () { + expect(getContentTypeFromContentTypeStr(null), isNull); + }); + }); +} diff --git a/packages/better_networking/test/utils/http_request_utils.dart b/packages/better_networking/test/utils/http_request_utils.dart deleted file mode 100644 index f256795c..00000000 --- a/packages/better_networking/test/utils/http_request_utils.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:better_networking/better_networking.dart'; -import 'package:test/test.dart'; - -void main() { - group('Testing RemoveJsonComments', () { - test('Removes single-line comments', () { - String input = ''' - { - // This is a single-line comment - "key": "value" - } - '''; - - String expected = '''{ - "key": "value" -}'''; - expect(removeJsonComments(input), expected); - }); - - test('Removes multi-line comments', () { - String input = ''' - { - /* - This is a multi-line comment - */ - "key": "value" - } - '''; - - String expected = '''{ - "key": "value" -}'''; - expect(removeJsonComments(input), expected); - }); - - test('Handles valid JSON without comments', () { - String input = '{"key":"value"}'; - String expected = '''{ - "key": "value" -}'''; - expect(removeJsonComments(input), expected); - }); - - test('Returns original string if invalid JSON', () { - String input = '{key: value}'; - String expected = '{key: value}'; - expect(removeJsonComments(input), expected); - }); - - test('Removes trailing commas', () { - String input = ''' - { - "key1": "value1", - "key2": "value2", // trailing comma - } - '''; - - String expected = '''{ - "key1": "value1", - "key2": "value2" -}'''; - expect(removeJsonComments(input), expected); - }); - - test('Test blank json', () { - String input = ''' - {} - '''; - - String expected = '{}'; - expect(removeJsonComments(input), expected); - }); - }); -} diff --git a/packages/better_networking/test/utils/http_request_utils_test.dart b/packages/better_networking/test/utils/http_request_utils_test.dart new file mode 100644 index 00000000..298e8749 --- /dev/null +++ b/packages/better_networking/test/utils/http_request_utils_test.dart @@ -0,0 +1,280 @@ +import 'package:better_networking/better_networking.dart'; +import 'package:test/test.dart'; + +void main() { + group('Testing RemoveJsonComments', () { + test('Removes single-line comments', () { + String input = ''' + { + // This is a single-line comment + "key": "value" + } + '''; + + String expected = '''{ + "key": "value" +}'''; + expect(removeJsonComments(input), expected); + }); + + test('Removes multi-line comments', () { + String input = ''' + { + /* + This is a multi-line comment + */ + "key": "value" + } + '''; + + String expected = '''{ + "key": "value" +}'''; + expect(removeJsonComments(input), expected); + }); + + test('Handles valid JSON without comments', () { + String input = '{"key":"value"}'; + String expected = '''{ + "key": "value" +}'''; + expect(removeJsonComments(input), expected); + }); + + test('Returns original string if invalid JSON', () { + String input = '{key: value}'; + String expected = '{key: value}'; + expect(removeJsonComments(input), expected); + }); + + test('Removes trailing commas', () { + String input = ''' + { + "key1": "value1", + "key2": "value2", // trailing comma + } + '''; + + String expected = '''{ + "key1": "value1", + "key2": "value2" +}'''; + expect(removeJsonComments(input), expected); + }); + + test('Test blank json', () { + String input = ''' + {} + '''; + + String expected = '{}'; + expect(removeJsonComments(input), expected); + }); + }); + + group("Testing rowsToMap", () { + test('Testing for null', () { + expect(rowsToMap(null), null); + }); + test('Testing for string KVRow values', () { + const kvRow1 = NameValueModel(name: "code", value: "IN"); + expect(rowsToMap([kvRow1]), {"code": "IN"}); + }); + test('Testing when header is True', () { + const kvRow2 = NameValueModel(name: "Text", value: "ABC"); + expect(rowsToMap([kvRow2], isHeader: true), {"text": "ABC"}); + }); + test('Testing when header is false and key is in upper case', () { + const kvRow3 = [ + NameValueModel(name: "TEXT", value: "ABC"), + NameValueModel(name: "version", value: 0.1), + NameValueModel(name: "month", value: 4), + ]; + expect(rowsToMap(kvRow3), { + "TEXT": "ABC", + "version": "0.1", + "month": "4", + }); + }); + }); + + group("Testing mapToRows", () { + test('Testing for null', () { + expect(mapToRows(null), null); + }); + test('Testing with a map value', () { + Map value1 = {"text": "abc", "lang": "eng", "code": "1"}; + const result1Expected = [ + NameValueModel(name: "text", value: "abc"), + NameValueModel(name: "lang", value: "eng"), + NameValueModel(name: "code", value: "1"), + ]; + expect(mapToRows(value1), result1Expected); + }); + }); + + group("Testing rowsToFormDataMapList", () { + test('Testing for null', () { + expect(rowsToFormDataMapList(null), null); + }); + test('Testing with a map value', () { + const input = [ + FormDataModel(name: "text", value: "abc", type: FormDataType.file), + FormDataModel(name: "lang", value: "eng", type: FormDataType.file), + FormDataModel(name: "code", value: "1", type: FormDataType.text), + ]; + const expectedResult = [ + {"name": "text", "value": "abc", "type": "file"}, + {"name": "lang", "value": "eng", "type": "file"}, + {"name": "code", "value": "1", "type": "text"}, + ]; + expect(rowsToFormDataMapList(input), expectedResult); + }); + }); + + group("Testing mapListToFormDataModelRows", () { + test('Testing for null', () { + expect(mapListToFormDataModelRows(null), null); + }); + test('Testing with a map value', () { + const input = [ + {"name": "text", "value": "abc", "type": "file"}, + {"name": "lang", "value": "eng", "type": "file"}, + {"name": "code", "value": "1", "type": "text"}, + ]; + const expectedResult = [ + FormDataModel(name: "text", value: "abc", type: FormDataType.file), + FormDataModel(name: "lang", value: "eng", type: FormDataType.file), + FormDataModel(name: "code", value: "1", type: FormDataType.text), + ]; + expect(mapListToFormDataModelRows(input), expectedResult); + }); + }); + + group("Test getEnabledRows", () { + test('Testing for null', () { + expect(getEnabledRows(null, null), null); + }); + test('Testing for empty list', () { + expect(getEnabledRows([], []), []); + }); + const kvRow1 = NameValueModel(name: "code", value: "IN"); + const kvRow2 = NameValueModel(name: "lang", value: "eng"); + const kvRow3 = NameValueModel(name: "version", value: 0.1); + const kvRow4 = NameValueModel(name: "month", value: 4); + test('Testing with isRowEnabledList null', () { + expect(getEnabledRows([kvRow1, kvRow2, kvRow3, kvRow4], null), [ + kvRow1, + kvRow2, + kvRow3, + kvRow4, + ]); + }); + test('Testing for list with all enabled', () { + expect( + getEnabledRows( + [kvRow1, kvRow2, kvRow3, kvRow4], + [true, true, true, true], + ), + [kvRow1, kvRow2, kvRow3, kvRow4], + ); + }); + test('Testing for list with all disabled', () { + expect( + getEnabledRows( + [kvRow1, kvRow2, kvRow3, kvRow4], + [false, false, false, false], + ), + [], + ); + }); + test('Testing for list with some disabled', () { + expect( + getEnabledRows( + [kvRow1, kvRow2, kvRow3, kvRow4], + [true, false, true, false], + ), + [kvRow1, kvRow3], + ); + }); + }); + + group('Testing getRequestBody', () { + test('Returns body for REST when hasJsonData is true', () { + const model = HttpRequestModel( + body: '{"key":"value"}', + method: HTTPVerb.post, + ); + final result = getRequestBody(APIType.rest, model); + expect(result, '{"key":"value"}'); + }); + + test('Returns body for REST when hasTextData is true', () { + const model = HttpRequestModel(body: 'plain text', method: HTTPVerb.post); + final result = getRequestBody(APIType.rest, model); + expect(result, 'plain text'); + }); + + test('Returns null for REST when no data', () { + const model = HttpRequestModel(body: null); + final result = getRequestBody(APIType.rest, model); + expect(result, isNull); + }); + + test('Returns GraphQL body as JSON when query is present', () { + const model = HttpRequestModel( + query: '{ users { name } }', + method: HTTPVerb.post, + ); + final result = getRequestBody(APIType.graphql, model); + expect(result, '{\n "query": "{ users { name } }"\n}'); + }); + + test('Returns null for GraphQL when query is missing', () { + const model = HttpRequestModel(query: null); + final result = getRequestBody(APIType.graphql, model); + expect(result, isNull); + }); + + test('Returns null for GraphQL when query is empty', () { + const model = HttpRequestModel(query: ''); + final result = getRequestBody(APIType.graphql, model); + expect(result, isNull); + }); + }); + + group('getFormDataType', () { + test('Returns correct enum for valid type "text"', () { + expect(getFormDataType("text"), FormDataType.text); + }); + + test('Returns correct enum for valid type "file"', () { + expect(getFormDataType("file"), FormDataType.file); + }); + + test('Returns FormDataType.text for any other unknown type', () { + expect(getFormDataType("unknown_type"), FormDataType.text); + }); + }); + + group('convertStreamedResponse', () { + test('Converts StreamedResponse into Response correctly', () async { + final bodyBytes = "Hello".codeUnits; // "Hello" + final streamedResponse = StreamedResponse( + Stream.fromIterable([bodyBytes]), + 200, + headers: {'content-type': 'text/plain'}, + reasonPhrase: 'OK', + persistentConnection: true, + ); + + final response = await convertStreamedResponse(streamedResponse); + + expect(response.statusCode, 200); + expect(response.body, 'Hello'); + expect(response.headers['content-type'], 'text/plain'); + expect(response.reasonPhrase, 'OK'); + expect(response.persistentConnection, true); + }); + }); +} diff --git a/packages/better_networking/test/utils/http_response_utils.dart b/packages/better_networking/test/utils/http_response_utils_test.dart similarity index 100% rename from packages/better_networking/test/utils/http_response_utils.dart rename to packages/better_networking/test/utils/http_response_utils_test.dart diff --git a/packages/better_networking/test/utils/string_utils_test.dart b/packages/better_networking/test/utils/string_utils_test.dart new file mode 100644 index 00000000..548242d3 --- /dev/null +++ b/packages/better_networking/test/utils/string_utils_test.dart @@ -0,0 +1,37 @@ +import 'package:better_networking/utils/string_utils.dart'; +import 'package:test/test.dart'; + +void main() { + group('RandomStringGenerator', () { + test('getRandomString returns correct length', () { + final result = RandomStringGenerator.getRandomString(10); + expect(result.length, 10); + }); + + test('getRandomString returns only valid characters', () { + final result = RandomStringGenerator.getRandomString(100); + const _chars = + 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; + final validChars = _chars.split('').toSet(); + final isValid = result.split('').every(validChars.contains); + expect(isValid, true); + }); + + test('getRandomStringLines returns correct number of lines', () { + final result = RandomStringGenerator.getRandomStringLines(5, 8); + final lines = result.split('\n'); + expect(lines.length, 5); + expect(lines.every((line) => line.length == 8), true); + }); + + test('getRandomStringLines returns empty string for 0 lines', () { + final result = RandomStringGenerator.getRandomStringLines(0, 8); + expect(result, ''); + }); + + test('getRandomString returns empty string for length 0', () { + final result = RandomStringGenerator.getRandomString(0); + expect(result, ''); + }); + }); +} diff --git a/packages/better_networking/test/utils/uri_utils_test.dart b/packages/better_networking/test/utils/uri_utils_test.dart index a513ba18..03021375 100644 --- a/packages/better_networking/test/utils/uri_utils_test.dart +++ b/packages/better_networking/test/utils/uri_utils_test.dart @@ -176,4 +176,33 @@ void main() { expect(getValidRequestUri(url7, null), (uri7Expected, null)); }); }); + + group("Testing stripUriParams", () { + test('Removes query parameters from Uri with query', () { + final uri = Uri.parse( + "https://example.com/path/to/resource?param1=value1¶m2=value2", + ); + expect(stripUriParams(uri), "https://example.com/path/to/resource"); + }); + + test('Removes fragment and query from Uri', () { + final uri = Uri.parse("https://example.com/api#section?foo=bar"); + expect(stripUriParams(uri), "https://example.com/api"); + }); + + test('stripUrlParams removes query from URL string', () { + const url = "https://example.com/page?x=1&y=2"; + expect(stripUrlParams(url), "https://example.com/page"); + }); + + test('stripUrlParams handles URL with no query', () { + const url = "https://example.com/page"; + expect(stripUrlParams(url), "https://example.com/page"); + }); + + test('stripUrlParams with only ? and no query', () { + const url = "https://example.com/page?"; + expect(stripUrlParams(url), "https://example.com/page"); + }); + }); } diff --git a/test/models/http_response_models.dart b/test/models/http_response_models.dart index 5b04ce3b..a9dff3af 100644 --- a/test/models/http_response_models.dart +++ b/test/models/http_response_models.dart @@ -41,4 +41,5 @@ Map responseModelJson = { "formattedBody": formattedBody, "bodyBytes": bodyBytes, "time": 516000, + 'sseOutput': null, }; diff --git a/test/models/response_model_test.dart b/test/models/response_model_test.dart index 56ba1e2a..48ec8f13 100644 --- a/test/models/response_model_test.dart +++ b/test/models/response_model_test.dart @@ -17,7 +17,6 @@ void main() { var responseRec = await sendHttpRequest( requestModelGet1.id, requestModelGet1.apiType, - AuthModel(type: APIAuthType.none), requestModelGet1.httpRequestModel!, defaultUriScheme: kDefaultUriScheme, noSSL: false, @@ -36,7 +35,6 @@ void main() { var responseRec = await sendHttpRequest( requestModelGet13.id, requestModelGet13.apiType, - AuthModel(type: APIAuthType.none), requestModelGet13.httpRequestModel!, defaultUriScheme: kDefaultUriScheme, noSSL: false, @@ -54,7 +52,6 @@ void main() { var responseRec = await sendHttpRequest( requestModelPost11.id, requestModelPost11.apiType, - AuthModel(type: APIAuthType.none), requestModelPost11.httpRequestModel!, ); @@ -69,7 +66,6 @@ void main() { var responseRec = await sendHttpRequest( requestModelPost12.id, requestModelPost12.apiType, - AuthModel(type: APIAuthType.none), requestModelPost12.httpRequestModel!, ); @@ -83,7 +79,6 @@ void main() { var responseRec = await sendHttpRequest( requestModelPost13.id, requestModelPost13.apiType, - AuthModel(type: APIAuthType.none), requestModelPost13.httpRequestModel!, ); @@ -97,7 +92,6 @@ void main() { var responseRec = await sendHttpRequest( requestModelGetBadSSL.id, requestModelGetBadSSL.apiType, - AuthModel(type: APIAuthType.none), requestModelGetBadSSL.httpRequestModel!, defaultUriScheme: kDefaultUriScheme, noSSL: false, @@ -110,7 +104,6 @@ void main() { var responseRec = await sendHttpRequest( requestModelGetBadSSL.id, requestModelGetBadSSL.apiType, - AuthModel(type: APIAuthType.none), requestModelGetBadSSL.httpRequestModel!, defaultUriScheme: kDefaultUriScheme, noSSL: true, @@ -131,7 +124,6 @@ void main() { var responseRec = await sendHttpRequest( requestModelOptions1.id, requestModelOptions1.apiType, - AuthModel(type: APIAuthType.none), requestModelOptions1.httpRequestModel!, defaultUriScheme: kDefaultUriScheme, noSSL: false, diff --git a/test/providers/collection_providers_test.dart b/test/providers/collection_providers_test.dart index 82894391..f201f799 100644 --- a/test/providers/collection_providers_test.dart +++ b/test/providers/collection_providers_test.dart @@ -1,5 +1,7 @@ +import 'dart:io'; import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/request_body.dart'; import 'package:apidash/widgets/editor.dart'; +import 'package:apidash/widgets/response_body.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -51,6 +53,71 @@ void main() async { expect(find.text('Switched to POST method'), findsOneWidget); }, skip: true); + testWidgets('SSE Output is rendered correctly in UI', + (WidgetTester tester) async { + HttpOverrides.global = null; //enable networking in flutter_test + + final container = createContainer(); + final notifier = container.read(collectionStateNotifierProvider.notifier); + + const model = HttpRequestModel( + url: 'https://sse-demo.netlify.app/sse', + method: HTTPVerb.get, + ); + + notifier.addRequestModel(model, name: 'sseM'); + final id = notifier.state!.entries.last.key; + + //runAsync to enable user-code awaiting + await tester.runAsync(() async { + await notifier.sendRequest(); + await Future.delayed(const Duration(seconds: 3)); + }); + + final rm = notifier.getRequestModel(id)!; + cancelHttpRequest(rm.id); + + final sseOutput = (rm.httpResponseModel?.sseOutput ?? []) + .map((e) => e.trim()) + .where((e) => e.isNotEmpty) + .toList(); + + expect(sseOutput, isNotEmpty, reason: 'No SSE Output found'); + + // Render the widget + await tester.pumpWidget( + ProviderScope( + // ignore: deprecated_member_use + parent: container, + child: MaterialApp( + home: Scaffold( + body: ResponseBody(selectedRequestModel: rm), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + final textWidgets = tester.widgetList(find.byType(Text)); + final matchingTextCount = textWidgets + .where((text) => + text.data != null && + text.data!.startsWith('data') && + sseOutput.contains(text.data!.trim())) + .length; + + expect( + matchingTextCount, + sseOutput.length, + reason: 'UI does not match all SSE output lines', + ); + + // Waits for all provider actions to complete before exit + await tester.runAsync(() async { + await Future.delayed(const Duration(seconds: 2)); + }); + }); + group('CollectionStateNotifier Auth Tests', () { late ProviderContainer container; late CollectionStateNotifier notifier; diff --git a/test/screens/common_widgets/auth/api_key_auth_fields_test.dart b/test/screens/common_widgets/auth/api_key_auth_fields_test.dart index 1e60cbd6..dc7e9134 100644 --- a/test/screens/common_widgets/auth/api_key_auth_fields_test.dart +++ b/test/screens/common_widgets/auth/api_key_auth_fields_test.dart @@ -1,8 +1,9 @@ -import 'package:apidash/screens/common_widgets/auth/api_key_auth_fields.dart'; -import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/screens/common_widgets/common_widgets.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:extended_text_field/extended_text_field.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -23,11 +24,15 @@ void main() { mockAuthData = null; await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: ApiKeyAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: SingleChildScrollView( + child: ApiKeyAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), ), ), ), @@ -35,7 +40,7 @@ void main() { expect(find.text('Add to'), findsOneWidget); expect(find.byType(ADPopupMenu), findsOneWidget); - expect(find.byType(AuthTextField), findsNWidgets(2)); + expect(find.byType(EnvAuthField), findsNWidgets(2)); expect(find.text('Header'), findsOneWidget); }); @@ -43,20 +48,38 @@ void main() { 'updates auth data when authData is null and API key value is changed', (WidgetTester tester) async { await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: ApiKeyAuthFields( - authData: null, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: SingleChildScrollView( + child: ApiKeyAuthFields( + authData: null, + updateAuth: mockUpdateAuth, + ), + ), ), ), ), ); - // Find the key field (second AuthTextField) - final keyField = find.byType(AuthTextField).last; - await tester.tap(keyField); - await tester.enterText(keyField, 'new-api-key'); + // Wait for the widget to settle + await tester.pumpAndSettle(); + + // Find EnvAuthField widgets + final authFields = find.byType(EnvAuthField); + expect(authFields, findsNWidgets(2)); + + // Find ExtendedTextField widgets within the EnvAuthField widgets + final textFields = find.byType(ExtendedTextField); + expect(textFields, findsAtLeastNWidgets(2)); + + // Use testTextInput to directly input text + final lastField = textFields.last; + await tester.tap(lastField); + await tester.pumpAndSettle(); + + // Use tester.testTextInput to enter text directly + tester.testTextInput.enterText('new-api-key'); await tester.pumpAndSettle(); // Verify that updateAuth was called @@ -77,11 +100,15 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: ApiKeyAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: SingleChildScrollView( + child: ApiKeyAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), ), ), ), @@ -89,7 +116,7 @@ void main() { expect(find.text('Add to'), findsOneWidget); expect(find.text('Header'), findsOneWidget); - expect(find.byType(AuthTextField), findsNWidgets(2)); + expect(find.byType(EnvAuthField), findsNWidgets(2)); }); testWidgets('renders with query params location', @@ -104,11 +131,15 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: ApiKeyAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: SingleChildScrollView( + child: ApiKeyAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), ), ), ), @@ -130,11 +161,15 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: ApiKeyAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: SingleChildScrollView( + child: ApiKeyAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), ), ), ), @@ -166,20 +201,33 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: ApiKeyAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: SingleChildScrollView( + child: ApiKeyAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), ), ), ), ); - // Find the name field (first AuthTextField) - final nameField = find.byType(AuthTextField).first; - await tester.tap(nameField); - await tester.enterText(nameField, 'Authorization'); + // Wait for the widget to settle + await tester.pumpAndSettle(); + + // Find ExtendedTextField widgets + final textFields = find.byType(ExtendedTextField); + expect(textFields, findsAtLeastNWidgets(2)); + + // Tap and enter text in the name field (should be the first text field) + await tester.tap(textFields.first); + await tester.pumpAndSettle(); + + // Use tester.testTextInput to enter text directly + tester.testTextInput.enterText('Authorization'); await tester.pumpAndSettle(); // Verify that updateAuth was called @@ -200,20 +248,37 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: ApiKeyAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: SingleChildScrollView( + child: ApiKeyAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), ), ), ), ); - // Find the key field (second AuthTextField) - final keyField = find.byType(AuthTextField).last; - await tester.tap(keyField); - await tester.enterText(keyField, 'new-api-key'); + // Wait for the widget to settle + await tester.pumpAndSettle(); + + // Find EnvAuthField widgets + final textFields = find.byType(EnvAuthField); + expect(textFields, findsNWidgets(2)); + + // Find the underlying ExtendedTextField widgets + final extendedTextFields = find.byType(ExtendedTextField); + expect(extendedTextFields, findsAtLeastNWidgets(2)); + + // Tap and enter text in the key field (should be the last text field) + await tester.tap(extendedTextFields.last); + await tester.pumpAndSettle(); + + // Use tester.testTextInput to enter text directly + tester.testTextInput.enterText('new-api-key'); await tester.pumpAndSettle(); // Verify that updateAuth was called @@ -233,21 +298,25 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: ApiKeyAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, - readOnly: true, + Portal( + child: MaterialApp( + home: Scaffold( + body: SingleChildScrollView( + child: ApiKeyAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + readOnly: true, + ), + ), ), ), ), ); - // Verify that AuthTextField widgets are rendered - expect(find.byType(AuthTextField), findsNWidgets(2)); + // Verify that EnvAuthField widgets are rendered + expect(find.byType(EnvAuthField), findsNWidgets(2)); - // The readOnly property should be passed to AuthTextField widgets + // The readOnly property should be passed to EnvAuthField widgets // This is verified by the widget structure itself }); @@ -255,11 +324,15 @@ void main() { mockAuthData = null; await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: ApiKeyAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: SingleChildScrollView( + child: ApiKeyAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), ), ), ), @@ -267,28 +340,40 @@ void main() { expect(find.text('Add to'), findsOneWidget); // Check for the existence of the auth text fields - expect(find.byType(AuthTextField), findsNWidgets(2)); + expect(find.byType(EnvAuthField), findsNWidgets(2)); }); testWidgets('initializes with correct default values', (WidgetTester tester) async { mockAuthData = null; await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: ApiKeyAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: SingleChildScrollView( + child: ApiKeyAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), ), ), ), ); + // Wait for the widget to settle + await tester.pumpAndSettle(); + // Default location should be header expect(find.text('Header'), findsOneWidget); - // Default name should be 'x-api-key' in the text field - expect(find.text('x-api-key'), findsOneWidget); + // Check for the existence of text fields with default values + final textFields = find.byType(EnvAuthField); + expect(textFields, findsNWidgets(2)); + + // Verify the first text field (name) has the default value in its controller + final nameTextField = tester.widget(textFields.first); + expect(nameTextField.initialValue, 'x-api-key'); }); }); } diff --git a/test/screens/common_widgets/auth/basic_auth_fields_test.dart b/test/screens/common_widgets/auth/basic_auth_fields_test.dart index 8a5d8814..6712a761 100644 --- a/test/screens/common_widgets/auth/basic_auth_fields_test.dart +++ b/test/screens/common_widgets/auth/basic_auth_fields_test.dart @@ -1,7 +1,8 @@ -import 'package:apidash/screens/common_widgets/auth/basic_auth_fields.dart'; -import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/screens/common_widgets/common_widgets.dart'; import 'package:apidash_core/apidash_core.dart'; +import 'package:extended_text_field/extended_text_field.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -22,17 +23,19 @@ void main() { mockAuthData = null; await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: BasicAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: BasicAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); - expect(find.byType(AuthTextField), findsNWidgets(2)); + expect(find.byType(EnvAuthField), findsNWidgets(2)); expect(find.text('Username'), findsNWidgets(2)); expect(find.text('Password'), findsNWidgets(2)); }); @@ -48,17 +51,19 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: BasicAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: BasicAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); - expect(find.byType(AuthTextField), findsNWidgets(2)); + expect(find.byType(EnvAuthField), findsNWidgets(2)); expect(find.text('Username'), findsExactly(2)); expect(find.text('Password'), findsExactly(2)); }); @@ -74,20 +79,28 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: BasicAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: BasicAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); - // Find the username field (first AuthTextField) - final usernameField = find.byType(AuthTextField).first; + // Find the username field (first ExtendedTextField) + final textFields = find.byType(ExtendedTextField); + expect(textFields, findsAtLeastNWidgets(2)); + + final usernameField = textFields.first; await tester.tap(usernameField); - await tester.enterText(usernameField, 'newuser'); + await tester.pumpAndSettle(); + + // Use tester.testTextInput to enter text directly + tester.testTextInput.enterText('newuser'); await tester.pumpAndSettle(); // Verify that updateAuth was called @@ -108,20 +121,28 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: BasicAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: BasicAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); - // Find the password field (second AuthTextField) - final passwordField = find.byType(AuthTextField).last; + // Find the password field (second ExtendedTextField) + final textFields = find.byType(ExtendedTextField); + expect(textFields, findsAtLeastNWidgets(2)); + + final passwordField = textFields.last; await tester.tap(passwordField); - await tester.enterText(passwordField, 'newpass'); + await tester.pumpAndSettle(); + + // Use tester.testTextInput to enter text directly + tester.testTextInput.enterText('newpass'); await tester.pumpAndSettle(); // Verify that updateAuth was called @@ -141,21 +162,23 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: BasicAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, - readOnly: true, + Portal( + child: MaterialApp( + home: Scaffold( + body: BasicAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + readOnly: true, + ), ), ), ), ); - // Verify that AuthTextField widgets are rendered - expect(find.byType(AuthTextField), findsNWidgets(2)); + // Verify that EnvAuthField widgets are rendered + expect(find.byType(EnvAuthField), findsNWidgets(2)); - // The readOnly property should be passed to AuthTextField widgets + // The readOnly property should be passed to EnvAuthField widgets // This is verified by the widget structure itself }); @@ -163,17 +186,19 @@ void main() { mockAuthData = null; await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: BasicAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: BasicAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); - expect(find.byType(AuthTextField), findsNWidgets(2)); + expect(find.byType(EnvAuthField), findsNWidgets(2)); }); testWidgets('handles empty auth data gracefully', @@ -187,17 +212,19 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: BasicAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: BasicAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); - expect(find.byType(AuthTextField), findsNWidgets(2)); + expect(find.byType(EnvAuthField), findsNWidgets(2)); }); testWidgets('creates proper AuthModel on field changes', @@ -205,20 +232,28 @@ void main() { mockAuthData = null; await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: BasicAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: BasicAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); // Enter username - final usernameField = find.byType(AuthTextField).first; + final textFields = find.byType(ExtendedTextField); + expect(textFields, findsAtLeastNWidgets(2)); + + final usernameField = textFields.first; await tester.tap(usernameField); - await tester.enterText(usernameField, 'testuser'); + await tester.pumpAndSettle(); + + // Use tester.testTextInput to enter text directly + tester.testTextInput.enterText('testuser'); await tester.pumpAndSettle(); // Verify that updateAuth was called with correct structure diff --git a/test/screens/common_widgets/auth/bearer_auth_fields_test.dart b/test/screens/common_widgets/auth/bearer_auth_fields_test.dart index c8dc1336..cd01c22b 100644 --- a/test/screens/common_widgets/auth/bearer_auth_fields_test.dart +++ b/test/screens/common_widgets/auth/bearer_auth_fields_test.dart @@ -1,7 +1,8 @@ -import 'package:apidash/screens/common_widgets/auth/bearer_auth_fields.dart'; -import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/screens/common_widgets/common_widgets.dart'; import 'package:apidash_core/apidash_core.dart'; +import 'package:extended_text_field/extended_text_field.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -22,17 +23,19 @@ void main() { mockAuthData = null; await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: BearerAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: BearerAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); - expect(find.byType(AuthTextField), findsOneWidget); + expect(find.byType(EnvAuthField), findsOneWidget); expect(find.text('Token'), findsNWidgets(2)); }); @@ -46,17 +49,19 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: BearerAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: BearerAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); - expect(find.byType(AuthTextField), findsOneWidget); + expect(find.byType(EnvAuthField), findsOneWidget); expect(find.text('Token'), findsNWidgets(2)); }); @@ -70,20 +75,28 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: BearerAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: BearerAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); // Find the token field - final tokenField = find.byType(AuthTextField); + final textFields = find.byType(ExtendedTextField); + expect(textFields, findsAtLeastNWidgets(1)); + + final tokenField = textFields.first; await tester.tap(tokenField); - await tester.enterText(tokenField, 'new-bearer-token'); + await tester.pumpAndSettle(); + + // Use tester.testTextInput to enter text directly + tester.testTextInput.enterText('new-bearer-token'); await tester.pumpAndSettle(); // Verify that updateAuth was called @@ -102,21 +115,23 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: BearerAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, - readOnly: true, + Portal( + child: MaterialApp( + home: Scaffold( + body: BearerAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + readOnly: true, + ), ), ), ), ); - // Verify that AuthTextField widget is rendered - expect(find.byType(AuthTextField), findsOneWidget); + // Verify that EnvAuthField widget is rendered + expect(find.byType(EnvAuthField), findsOneWidget); - // The readOnly property should be passed to AuthTextField widget + // The readOnly property should be passed to EnvAuthField widget // This is verified by the widget structure itself }); @@ -124,17 +139,19 @@ void main() { mockAuthData = null; await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: BearerAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: BearerAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); - expect(find.byType(AuthTextField), findsOneWidget); + expect(find.byType(EnvAuthField), findsOneWidget); }); testWidgets('handles empty auth data gracefully', @@ -147,17 +164,19 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: BearerAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: BearerAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); - expect(find.byType(AuthTextField), findsOneWidget); + expect(find.byType(EnvAuthField), findsOneWidget); }); testWidgets('creates proper AuthModel on token change', @@ -165,20 +184,28 @@ void main() { mockAuthData = null; await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: BearerAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: BearerAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); // Enter token - final tokenField = find.byType(AuthTextField); + final textFields = find.byType(ExtendedTextField); + expect(textFields, findsAtLeastNWidgets(1)); + + final tokenField = textFields.first; await tester.tap(tokenField); - await tester.enterText(tokenField, 'test-bearer-token'); + await tester.pumpAndSettle(); + + // Use tester.testTextInput to enter text directly + tester.testTextInput.enterText('test-bearer-token'); await tester.pumpAndSettle(); // Verify that updateAuth was called with correct structure @@ -193,18 +220,20 @@ void main() { mockAuthData = null; await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: BearerAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: BearerAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); // The token field should be empty initially - expect(find.byType(AuthTextField), findsOneWidget); + expect(find.byType(EnvAuthField), findsOneWidget); }); testWidgets('trims whitespace from token input', @@ -212,20 +241,28 @@ void main() { mockAuthData = null; await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: BearerAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: BearerAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); // Enter token with whitespace - final tokenField = find.byType(AuthTextField); + final textFields = find.byType(ExtendedTextField); + expect(textFields, findsAtLeastNWidgets(1)); + + final tokenField = textFields.first; await tester.tap(tokenField); - await tester.enterText(tokenField, ' test-token '); + await tester.pumpAndSettle(); + + // Use tester.testTextInput to enter text directly + tester.testTextInput.enterText(' test-token '); await tester.pumpAndSettle(); // Verify that updateAuth was called with trimmed token diff --git a/test/screens/common_widgets/auth/digest_auth_fields_test.dart b/test/screens/common_widgets/auth/digest_auth_fields_test.dart index 2eb26a04..170fe075 100644 --- a/test/screens/common_widgets/auth/digest_auth_fields_test.dart +++ b/test/screens/common_widgets/auth/digest_auth_fields_test.dart @@ -1,8 +1,9 @@ -import 'package:apidash/screens/common_widgets/auth/digest_auth_fields.dart'; -import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/screens/common_widgets/common_widgets.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/widgets/widgets.dart'; +import 'package:extended_text_field/extended_text_field.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -23,19 +24,21 @@ void main() { mockAuthData = null; await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: DigestAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); - expect(find.byType(AuthTextField), findsNWidgets(6)); + expect(find.byType(EnvAuthField), findsNWidgets(6)); expect(find.byType(ADPopupMenu), findsOneWidget); - // Check for field labels (each AuthTextField creates a Text widget for label) + // Check for field labels (each EnvAuthField creates a Text widget for label) expect(find.text('Username'), findsNWidgets(2)); expect(find.text('Password'), findsNWidgets(2)); expect(find.text('Realm'), findsNWidgets(2)); @@ -61,17 +64,19 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: DigestAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); - expect(find.byType(AuthTextField), findsNWidgets(6)); + expect(find.byType(EnvAuthField), findsNWidgets(6)); expect(find.byType(ADPopupMenu), findsOneWidget); expect(find.text('MD5'), findsOneWidget); }); @@ -92,20 +97,28 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: DigestAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); - // Find the username field (first AuthTextField) - final usernameField = find.byType(AuthTextField).first; + // Find the username field (first ExtendedTextField) + final textFields = find.byType(ExtendedTextField); + expect(textFields, findsAtLeastNWidgets(6)); + + final usernameField = textFields.first; await tester.tap(usernameField); - await tester.enterText(usernameField, 'newuser'); + await tester.pumpAndSettle(); + + // Use tester.testTextInput to enter text directly + tester.testTextInput.enterText('newuser'); await tester.pumpAndSettle(); // Verify that updateAuth was called @@ -131,20 +144,28 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: DigestAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); - // Find the password field (second AuthTextField) - final passwordField = find.byType(AuthTextField).at(1); + // Find the password field (second ExtendedTextField) + final textFields = find.byType(ExtendedTextField); + expect(textFields, findsAtLeastNWidgets(6)); + + final passwordField = textFields.at(1); await tester.tap(passwordField); - await tester.enterText(passwordField, 'newpass'); + await tester.pumpAndSettle(); + + // Use tester.testTextInput to enter text directly + tester.testTextInput.enterText('newpass'); await tester.pumpAndSettle(); // Verify that updateAuth was called @@ -170,11 +191,13 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: DigestAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), @@ -210,20 +233,28 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: DigestAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); - // Find the realm field (third AuthTextField) - final realmField = find.byType(AuthTextField).at(2); + // Find the realm field (third ExtendedTextField) + final textFields = find.byType(ExtendedTextField); + expect(textFields, findsAtLeastNWidgets(6)); + + final realmField = textFields.at(2); await tester.tap(realmField); - await tester.enterText(realmField, 'newrealm'); + await tester.pumpAndSettle(); + + // Use tester.testTextInput to enter text directly + tester.testTextInput.enterText('newrealm'); await tester.pumpAndSettle(); // Verify that updateAuth was called @@ -247,46 +278,51 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: DigestAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, - readOnly: true, + Portal( + child: MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + readOnly: true, + ), ), ), ), ); - final usernameFieldFinder = find.byType(AuthTextField).first; + final usernameFieldFinder = find.byType(ExtendedTextField).first; - // Try to enter text - await tester.enterText(usernameFieldFinder, 'testuser'); - await tester.pumpAndSettle(); + // Verify the field is readOnly + final usernameField = + tester.widget(usernameFieldFinder); + expect(usernameField.readOnly, isTrue); // Ensure updateAuth was not called expect(capturedAuthUpdates, isEmpty); // Check the field still shows original value - final textField = tester.widget(usernameFieldFinder); - expect(textField.controller.text, equals('user')); + final textField = tester.widget(usernameFieldFinder); + expect(textField.controller?.text, equals('user')); }); testWidgets('displays correct hint texts', (WidgetTester tester) async { mockAuthData = null; await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: DigestAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); - expect(find.byType(AuthTextField), findsNWidgets(6)); + expect(find.byType(EnvAuthField), findsNWidgets(6)); expect(find.byType(ADPopupMenu), findsOneWidget); expect(find.text('Algorithm'), findsOneWidget); }); @@ -296,11 +332,13 @@ void main() { mockAuthData = null; await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: DigestAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), @@ -311,7 +349,7 @@ void main() { // Default QOP should be 'auth' - but this is in the TextFormField value, not visible text // We need to check the controller value instead - expect(find.byType(AuthTextField), findsNWidgets(6)); + expect(find.byType(EnvAuthField), findsNWidgets(6)); }); testWidgets('creates proper AuthModel on field changes', @@ -319,20 +357,22 @@ void main() { mockAuthData = null; await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: DigestAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); // Enter username - final usernameField = find.byType(AuthTextField).first; + final usernameField = find.byType(ExtendedTextField).first; await tester.tap(usernameField); - await tester.enterText(usernameField, 'testuser'); + tester.testTextInput.enterText('testuser'); await tester.pumpAndSettle(); // Verify that updateAuth was called with correct structure @@ -348,11 +388,13 @@ void main() { mockAuthData = null; await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: DigestAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), @@ -380,20 +422,28 @@ void main() { mockAuthData = null; await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: DigestAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); // Enter username with whitespace - final usernameField = find.byType(AuthTextField).first; + final textFields = find.byType(ExtendedTextField); + expect(textFields, findsAtLeastNWidgets(6)); + + final usernameField = textFields.first; await tester.tap(usernameField); - await tester.enterText(usernameField, ' testuser '); + await tester.pumpAndSettle(); + + // Use tester.testTextInput to enter text directly + tester.testTextInput.enterText(' testuser '); await tester.pumpAndSettle(); // Verify that updateAuth was called with trimmed values diff --git a/test/screens/common_widgets/auth/jwt_auth_fields_test.dart b/test/screens/common_widgets/auth/jwt_auth_fields_test.dart index ff02166b..206d0ba8 100644 --- a/test/screens/common_widgets/auth/jwt_auth_fields_test.dart +++ b/test/screens/common_widgets/auth/jwt_auth_fields_test.dart @@ -1,9 +1,10 @@ -import 'package:apidash/screens/common_widgets/auth/jwt_auth_fields.dart'; -import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/screens/common_widgets/common_widgets.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:extended_text_field/extended_text_field.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_portal/flutter_portal.dart'; void main() { group('JwtAuthFields Widget Tests', () { @@ -23,11 +24,13 @@ void main() { mockAuthData = null; await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: JwtAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: JwtAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), @@ -59,11 +62,13 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: JwtAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: JwtAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), @@ -93,11 +98,13 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: JwtAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: JwtAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), @@ -105,7 +112,7 @@ void main() { expect(find.text('Secret Key'), findsExactly(2)); expect(find.text('Secret is Base64 encoded'), findsOneWidget); - expect(find.byType(AuthTextField), findsOneWidget); + expect(find.byType(EnvAuthField), findsOneWidget); expect(find.byType(CheckboxListTile), findsOneWidget); }); @@ -127,11 +134,13 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: JwtAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: JwtAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), @@ -160,11 +169,13 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: JwtAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: JwtAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), @@ -202,11 +213,13 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: JwtAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: JwtAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), @@ -244,20 +257,22 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: JwtAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: JwtAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), ); // Find the secret field - final secretField = find.byType(AuthTextField).first; + final secretField = find.byType(ExtendedTextField).first; await tester.tap(secretField); - await tester.enterText(secretField, 'new-secret'); + tester.testTextInput.enterText('new-secret'); await tester.pumpAndSettle(); // Verify that updateAuth was called @@ -284,11 +299,13 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: JwtAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: JwtAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), @@ -297,7 +314,7 @@ void main() { // Find the payload field (TextField) final payloadField = find.byType(TextField).last; await tester.tap(payloadField); - await tester.enterText(payloadField, '{"sub": "1234567890"}'); + tester.testTextInput.enterText('{"sub": "1234567890"}'); await tester.pumpAndSettle(); // Verify that updateAuth was called @@ -324,11 +341,13 @@ void main() { ); await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: JwtAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: JwtAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), @@ -349,11 +368,13 @@ void main() { mockAuthData = null; await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: JwtAuthFields( - authData: mockAuthData, - updateAuth: mockUpdateAuth, + Portal( + child: MaterialApp( + home: Scaffold( + body: JwtAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), ), ), ), diff --git a/test/utils/convert_utils_test.dart b/test/utils/convert_utils_test.dart index 45ac5556..21c3f523 100644 --- a/test/utils/convert_utils_test.dart +++ b/test/utils/convert_utils_test.dart @@ -94,81 +94,6 @@ void main() { expect(formatHeaderCase(headerText2), headerText2Expected); }); }); - group("Testing rowsToMap", () { - test('Testing for null', () { - expect(rowsToMap(null), null); - }); - test('Testing for string KVRow values', () { - const kvRow1 = NameValueModel(name: "code", value: "IN"); - expect(rowsToMap([kvRow1]), {"code": "IN"}); - }); - test('Testing when header is True', () { - const kvRow2 = NameValueModel(name: "Text", value: "ABC"); - expect(rowsToMap([kvRow2], isHeader: true), {"text": "ABC"}); - }); - test('Testing when header is false and key is in upper case', () { - const kvRow3 = [ - NameValueModel(name: "TEXT", value: "ABC"), - NameValueModel(name: "version", value: 0.1), - NameValueModel(name: "month", value: 4), - ]; - expect( - rowsToMap(kvRow3), {"TEXT": "ABC", "version": "0.1", "month": "4"}); - }); - }); - - group("Testing mapToRows", () { - test('Testing for null', () { - expect(mapToRows(null), null); - }); - test('Testing with a map value', () { - Map value1 = {"text": "abc", "lang": "eng", "code": "1"}; - const result1Expected = [ - NameValueModel(name: "text", value: "abc"), - NameValueModel(name: "lang", value: "eng"), - NameValueModel(name: "code", value: "1") - ]; - expect(mapToRows(value1), result1Expected); - }); - }); - - group("Testing rowsToFormDataMapList", () { - test('Testing for null', () { - expect(rowsToFormDataMapList(null), null); - }); - test('Testing with a map value', () { - const input = [ - FormDataModel(name: "text", value: "abc", type: FormDataType.file), - FormDataModel(name: "lang", value: "eng", type: FormDataType.file), - FormDataModel(name: "code", value: "1", type: FormDataType.text) - ]; - const expectedResult = [ - {"name": "text", "value": "abc", "type": "file"}, - {"name": "lang", "value": "eng", "type": "file"}, - {"name": "code", "value": "1", "type": "text"} - ]; - expect(rowsToFormDataMapList(input), expectedResult); - }); - }); - - group("Testing mapListToFormDataModelRows", () { - test('Testing for null', () { - expect(mapListToFormDataModelRows(null), null); - }); - test('Testing with a map value', () { - const input = [ - {"name": "text", "value": "abc", "type": "file"}, - {"name": "lang", "value": "eng", "type": "file"}, - {"name": "code", "value": "1", "type": "text"} - ]; - const expectedResult = [ - FormDataModel(name: "text", value: "abc", type: FormDataType.file), - FormDataModel(name: "lang", value: "eng", type: FormDataType.file), - FormDataModel(name: "code", value: "1", type: FormDataType.text) - ]; - expect(mapListToFormDataModelRows(input), expectedResult); - }); - }); group("Testing getFormDataType", () { test('Testing for null', () { @@ -240,41 +165,6 @@ Easily manipulate and play around with request inputs like headers, query parame }); }); - group("Test getEnabledRows", () { - test('Testing for null', () { - expect(getEnabledRows(null, null), null); - }); - test('Testing for empty list', () { - expect(getEnabledRows([], []), []); - }); - const kvRow1 = NameValueModel(name: "code", value: "IN"); - const kvRow2 = NameValueModel(name: "lang", value: "eng"); - const kvRow3 = NameValueModel(name: "version", value: 0.1); - const kvRow4 = NameValueModel(name: "month", value: 4); - test('Testing with isRowEnabledList null', () { - expect(getEnabledRows([kvRow1, kvRow2, kvRow3, kvRow4], null), - [kvRow1, kvRow2, kvRow3, kvRow4]); - }); - test('Testing for list with all enabled', () { - expect( - getEnabledRows( - [kvRow1, kvRow2, kvRow3, kvRow4], [true, true, true, true]), - [kvRow1, kvRow2, kvRow3, kvRow4]); - }); - test('Testing for list with all disabled', () { - expect( - getEnabledRows( - [kvRow1, kvRow2, kvRow3, kvRow4], [false, false, false, false]), - []); - }); - test('Testing for list with some disabled', () { - expect( - getEnabledRows( - [kvRow1, kvRow2, kvRow3, kvRow4], [true, false, true, false]), - [kvRow1, kvRow3]); - }); - }); - group("Testing audioPosition function", () { test('Testing using dur1', () { Duration dur1 = const Duration(minutes: 1, seconds: 3); diff --git a/test/utils/envvar_utils_test.dart b/test/utils/envvar_utils_test.dart index 48fc7735..409d2aa1 100644 --- a/test/utils/envvar_utils_test.dart +++ b/test/utils/envvar_utils_test.dart @@ -357,4 +357,98 @@ void main() { expect(getVariableStatus(query, envMap, activeEnvironmentId), expected); }); }); + + group("Testing auth model environment variable substitution", () { + test("Testing basic auth with environment variables", () { + const httpRequestModel = HttpRequestModel( + url: "{{url}}/test", + authModel: AuthModel( + type: APIAuthType.basic, + basic: AuthBasicAuthModel( + username: "{{basic_username}}admin", + password: "{{token}}pass", + ), + ), + ); + + Map> envMap = { + kGlobalEnvironmentId: [ + EnvironmentVariableModel(key: "url", value: "api.apidash.dev"), + EnvironmentVariableModel(key: "basic_username", value: "testuser"), + EnvironmentVariableModel(key: "token", value: "secret"), + ], + }; + + const activeEnvironmentId = null; + final result = substituteHttpRequestModel( + httpRequestModel, + envMap, + activeEnvironmentId, + ); + + expect(result.authModel?.basic?.username, "testuseradmin"); + expect(result.authModel?.basic?.password, "secretpass"); + expect(result.url, "api.apidash.dev/test"); + }); + + test("Testing bearer auth with environment variables", () { + const httpRequestModel = HttpRequestModel( + url: "{{url}}/test", + authModel: AuthModel( + type: APIAuthType.bearer, + bearer: AuthBearerModel( + token: "{{bearer_token}}", + ), + ), + ); + + Map> envMap = { + kGlobalEnvironmentId: [ + EnvironmentVariableModel(key: "url", value: "api.apidash.dev"), + EnvironmentVariableModel(key: "bearer_token", value: "secret123"), + ], + }; + + const activeEnvironmentId = null; + final result = substituteHttpRequestModel( + httpRequestModel, + envMap, + activeEnvironmentId, + ); + + expect(result.authModel?.bearer?.token, "secret123"); + }); + + test("Testing API key auth with environment variables", () { + const httpRequestModel = HttpRequestModel( + url: "{{url}}/test", + authModel: AuthModel( + type: APIAuthType.apiKey, + apikey: AuthApiKeyModel( + key: "{{api_key}}", + name: "{{header_name}}", + location: "header", + ), + ), + ); + + Map> envMap = { + kGlobalEnvironmentId: [ + EnvironmentVariableModel(key: "url", value: "api.apidash.dev"), + EnvironmentVariableModel(key: "api_key", value: "key123"), + EnvironmentVariableModel(key: "header_name", value: "X-API-Key"), + ], + }; + + const activeEnvironmentId = null; + final result = substituteHttpRequestModel( + httpRequestModel, + envMap, + activeEnvironmentId, + ); + + expect(result.authModel?.apikey?.key, "key123"); + expect(result.authModel?.apikey?.name, "X-API-Key"); + }); + }); } diff --git a/test/utils/ui_utils_test.dart b/test/utils/ui_utils_test.dart index 28b90df9..9fc4a2fa 100644 --- a/test/utils/ui_utils_test.dart +++ b/test/utils/ui_utils_test.dart @@ -13,8 +13,7 @@ void main() { expect(getResponseStatusCodeColor(statusCode1), kColorStatusCode200); }); - Color colStatusCode1DarkModeExpected = - getDarkModeColor(kColorStatusCode200); + Color colStatusCode1DarkModeExpected = kColorStatusCode200.toDark; test('Testing getResponseStatusCodeColor for statusCode 200 dark mode', () { expect(getResponseStatusCodeColor(statusCode1, brightness: dark), colStatusCode1DarkModeExpected); @@ -25,8 +24,7 @@ void main() { expect(getResponseStatusCodeColor(statusCode2), kColorStatusCode300); }); - Color colStatusCode2DarkModeExpected = - getDarkModeColor(kColorStatusCode300); + Color colStatusCode2DarkModeExpected = kColorStatusCode300.toDark; test('Testing getResponseStatusCodeColor for statusCode 300 dark mode', () { expect(getResponseStatusCodeColor(statusCode2, brightness: dark), colStatusCode2DarkModeExpected); @@ -37,8 +35,7 @@ void main() { expect(getResponseStatusCodeColor(statusCode3), kColorStatusCode400); }); - Color colStatusCode3DarkModeExpected = - getDarkModeColor(kColorStatusCode400); + Color colStatusCode3DarkModeExpected = kColorStatusCode400.toDark; test('Testing getResponseStatusCodeColor for statusCode 404 dark mode', () { expect(getResponseStatusCodeColor(statusCode3, brightness: dark), colStatusCode3DarkModeExpected); @@ -50,8 +47,7 @@ void main() { expect(getResponseStatusCodeColor(statusCode4), kColorStatusCode500); }); - Color colStatusCode4DarkModeExpected = - getDarkModeColor(kColorStatusCode500); + Color colStatusCode4DarkModeExpected = kColorStatusCode500.toDark; test('Testing getResponseStatusCodeColor for statusCode 503 dark mode', () { expect(getResponseStatusCodeColor(statusCode4, brightness: dark), colStatusCode4DarkModeExpected); @@ -63,8 +59,7 @@ void main() { expect(getResponseStatusCodeColor(statusCode5), kColorStatusCodeDefault); }); - Color colStatusCode5DarkModeExpected = - getDarkModeColor(kColorStatusCodeDefault); + Color colStatusCode5DarkModeExpected = kColorStatusCodeDefault.toDark; test('Testing getResponseStatusCodeColor for statusCode 101 dark mode', () { expect(getResponseStatusCodeColor(statusCode5, brightness: dark), colStatusCode5DarkModeExpected); @@ -73,7 +68,7 @@ void main() { group("Testing getAPIColor function", () { HTTPVerb methodGet = HTTPVerb.get; - Color colMethodGetDarkModeExpected = getDarkModeColor(kColorHttpMethodGet); + Color colMethodGetDarkModeExpected = kColorHttpMethodGet.toDark; test('Test getAPIColor for GET method dark mode', () { expect( getAPIColor( @@ -85,8 +80,7 @@ void main() { }); HTTPVerb methodHead = HTTPVerb.head; - Color colMethodHeadDarkModeExpected = - getDarkModeColor(kColorHttpMethodHead); + Color colMethodHeadDarkModeExpected = kColorHttpMethodHead.toDark; test('Test getHTTPMethodColor for HEAD Method dark mode', () { expect( getAPIColor( @@ -98,8 +92,7 @@ void main() { }); HTTPVerb methodPatch = HTTPVerb.patch; - Color colMethodPatchDarkModeExpected = - getDarkModeColor(kColorHttpMethodPatch); + Color colMethodPatchDarkModeExpected = kColorHttpMethodPatch.toDark; test('Test getHTTPMethodColor for PATCH Method dark mode', () { expect( getAPIColor( @@ -111,7 +104,7 @@ void main() { }); HTTPVerb methodPut = HTTPVerb.put; - Color colMethodPutDarkModeExpected = getDarkModeColor(kColorHttpMethodPut); + Color colMethodPutDarkModeExpected = kColorHttpMethodPut.toDark; test('Test getHTTPMethodColor for PUT Method dark mode', () { expect( getAPIColor( @@ -123,8 +116,7 @@ void main() { }); HTTPVerb methodPost = HTTPVerb.post; - Color colMethodPostDarkModeExpected = - getDarkModeColor(kColorHttpMethodPost); + Color colMethodPostDarkModeExpected = kColorHttpMethodPost.toDark; test('Test getHTTPMethodColor for POST Method dark mode', () { expect( getAPIColor( @@ -136,8 +128,7 @@ void main() { }); HTTPVerb methodDelete = HTTPVerb.delete; - Color colMethodDeleteDarkModeExpected = - getDarkModeColor(kColorHttpMethodDelete); + Color colMethodDeleteDarkModeExpected = kColorHttpMethodDelete.toDark; test('Test getHTTPMethodColor for DELETE Method dark mode', () { expect( getAPIColor( diff --git a/test/widgets/button_send_test.dart b/test/widgets/button_send_test.dart index c9bee3c7..46f954de 100644 --- a/test/widgets/button_send_test.dart +++ b/test/widgets/button_send_test.dart @@ -16,6 +16,7 @@ void main() { theme: kThemeDataLight, home: Scaffold( body: SendButton( + isStreaming: false, isWorking: false, onTap: () => sendPressed = true, onCancel: () => cancelPressed = true, @@ -45,6 +46,7 @@ void main() { theme: kThemeDataLight, home: Scaffold( body: SendButton( + isStreaming: false, isWorking: true, onTap: () => sendPressed = true, onCancel: () => cancelPressed = true, @@ -74,6 +76,7 @@ void main() { builder: (context, setState) { return Scaffold( body: SendButton( + isStreaming: false, isWorking: isWorking, onTap: () => setState(() => isWorking = true), onCancel: () => setState(() => isWorking = false), diff --git a/test/widgets/texts_test.dart b/test/widgets/texts_test.dart index 79107fe1..5b1cb222 100644 --- a/test/widgets/texts_test.dart +++ b/test/widgets/texts_test.dart @@ -2,7 +2,6 @@ import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:apidash/utils/utils.dart' show getDarkModeColor; import 'package:apidash/widgets/texts.dart'; void main() { @@ -49,7 +48,7 @@ void main() { expect(find.byType(SizedBox), findsOneWidget); expect(find.text('DEL'), findsOneWidget); expect(find.text('GET'), findsNothing); - Color colDelDarkMode = getDarkModeColor(kColorHttpMethodDelete); + Color colDelDarkMode = kColorHttpMethodDelete.toDark; final delTextWithColor = find.byWidgetPredicate( (widget) => widget is Text && widget.style!.color == colDelDarkMode); expect(delTextWithColor, findsOneWidget);