diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 047b5b09e..12768e35b 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:apidash_core/apidash_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_riverpod/legacy.dart'; import 'package:apidash/consts.dart'; import 'package:apidash/terminal/terminal.dart'; import 'providers.dart'; @@ -24,19 +23,19 @@ final selectedRequestModelProvider = StateProvider((ref) { final selectedSubstitutedHttpRequestModelProvider = StateProvider((ref) { - final selectedRequestModel = ref.watch(selectedRequestModelProvider); - final envMap = ref.read(availableEnvironmentVariablesStateProvider); - final activeEnvId = ref.read(activeEnvironmentIdStateProvider); - if (selectedRequestModel?.httpRequestModel == null) { - return null; - } else { - return substituteHttpRequestModel( - selectedRequestModel!.httpRequestModel!, - envMap, - activeEnvId, - ); - } - }); + final selectedRequestModel = ref.watch(selectedRequestModelProvider); + final envMap = ref.read(availableEnvironmentVariablesStateProvider); + final activeEnvId = ref.read(activeEnvironmentIdStateProvider); + if (selectedRequestModel?.httpRequestModel == null) { + return null; + } else { + return substituteHttpRequestModel( + selectedRequestModel!.httpRequestModel!, + envMap, + activeEnvId, + ); + } +}); final requestSequenceProvider = StateProvider>((ref) { var ids = hiveHandler.getIds(); @@ -44,27 +43,36 @@ final requestSequenceProvider = StateProvider>((ref) { }); final StateNotifierProvider?> -collectionStateNotifierProvider = StateNotifierProvider( - (ref) => CollectionStateNotifier(ref, hiveHandler), -); + collectionStateNotifierProvider = + StateNotifierProvider((ref) => CollectionStateNotifier( + ref, + hiveHandler, + )); class CollectionStateNotifier extends StateNotifier?> { - CollectionStateNotifier(this.ref, this.hiveHandler) : super(null) { + CollectionStateNotifier( + this.ref, + this.hiveHandler, + ) : super(null) { var status = loadData(); Future.microtask(() { if (status) { - ref.read(requestSequenceProvider.notifier).state = [state!.keys.first]; + ref.read(requestSequenceProvider.notifier).state = [ + state!.keys.first, + ]; } - ref.read(selectedIdStateProvider.notifier).state = ref.read( - requestSequenceProvider, - )[0]; + ref.read(selectedIdStateProvider.notifier).state = + ref.read(requestSequenceProvider)[0]; }); } final Ref ref; final HiveHandler hiveHandler; final baseHttpResponseModel = const HttpResponseModel(); + RequestModel? _lastDeletedModel; + String? _lastDeletedId; + int? _lastDeletedIndex; bool hasId(String id) => state?.keys.contains(id) ?? false; @@ -92,7 +100,10 @@ class CollectionStateNotifier unsave(); } - void addRequestModel(HttpRequestModel httpRequestModel, {String? name}) { + void addRequestModel( + HttpRequestModel httpRequestModel, { + String? name, + }) { final id = getNewUuid(); final newRequestModel = RequestModel( id: id, @@ -110,7 +121,7 @@ class CollectionStateNotifier } void reorder(int oldIdx, int newIdx) { - var itemIds = ref.read(requestSequenceProvider); + var itemIds = [...ref.read(requestSequenceProvider)]; final itemId = itemIds.removeAt(oldIdx); itemIds.insert(newIdx, itemId); ref.read(requestSequenceProvider.notifier).state = [...itemIds]; @@ -121,6 +132,9 @@ class CollectionStateNotifier final rId = id ?? ref.read(selectedIdStateProvider); var itemIds = ref.read(requestSequenceProvider); int idx = itemIds.indexOf(rId!); + _lastDeletedModel = state![rId]; + _lastDeletedId = rId; + _lastDeletedIndex = idx; cancelHttpRequest(rId); itemIds.remove(rId); ref.read(requestSequenceProvider.notifier).state = [...itemIds]; @@ -142,6 +156,31 @@ class CollectionStateNotifier unsave(); } + void undoDelete() { + if (_lastDeletedModel == null || + _lastDeletedId == null || + _lastDeletedIndex == null) { + return; + } + + var itemIds = [...ref.read(requestSequenceProvider)]; + final insertIdx = _lastDeletedIndex!.clamp(0, itemIds.length); + itemIds.insert(insertIdx, _lastDeletedId!); + + var map = {...state!}; + map[_lastDeletedId!] = _lastDeletedModel!; + state = map; + + ref.read(requestSequenceProvider.notifier).state = itemIds; + ref.read(selectedIdStateProvider.notifier).state = _lastDeletedId; + + _lastDeletedModel = null; + _lastDeletedId = null; + _lastDeletedIndex = null; + + unsave(); +} + void clearResponse({String? id}) { final rId = id ?? ref.read(selectedIdStateProvider); if (rId == null || state?[rId] == null) return; @@ -162,8 +201,7 @@ class CollectionStateNotifier void duplicate({String? id}) { final rId = id ?? ref.read(selectedIdStateProvider); final newId = getNewUuid(); - - var itemIds = ref.read(requestSequenceProvider); + var itemIds = [...ref.read(requestSequenceProvider)]; int idx = itemIds.indexOf(rId!); var currentModel = state![rId]!; final newModel = currentModel.copyWith( @@ -257,23 +295,22 @@ class CollectionStateNotifier final defaultModel = ref.read(settingsProvider).defaultAIModel; newModel = switch (apiType) { APIType.rest || APIType.graphql => currentModel.copyWith( - apiType: apiType, - requestTabIndex: 0, - name: name ?? currentModel.name, - description: description ?? currentModel.description, - httpRequestModel: const HttpRequestModel(), - aiRequestModel: null, - ), + apiType: apiType, + requestTabIndex: 0, + name: name ?? currentModel.name, + description: description ?? currentModel.description, + httpRequestModel: const HttpRequestModel(), + aiRequestModel: null, + ), APIType.ai => currentModel.copyWith( - apiType: apiType, - requestTabIndex: 0, - name: name ?? currentModel.name, - description: description ?? currentModel.description, - httpRequestModel: null, - aiRequestModel: defaultModel == null - ? const AIRequestModel() - : AIRequestModel.fromJson(defaultModel), - ), + apiType: apiType, + requestTabIndex: 0, + name: name ?? currentModel.name, + description: description ?? currentModel.description, + httpRequestModel: null, + aiRequestModel: defaultModel == null + ? const AIRequestModel() + : AIRequestModel.fromJson(defaultModel)), }; } else { newModel = currentModel.copyWith( @@ -287,8 +324,7 @@ class CollectionStateNotifier headers: headers ?? currentHttpRequestModel.headers, params: params ?? currentHttpRequestModel.params, authModel: authModel ?? currentHttpRequestModel.authModel, - isHeaderEnabledList: - isHeaderEnabledList ?? + isHeaderEnabledList: isHeaderEnabledList ?? currentHttpRequestModel.isHeaderEnabledList, isParamEnabledList: isParamEnabledList ?? currentHttpRequestModel.isParamEnabledList, @@ -328,9 +364,8 @@ class CollectionStateNotifier } final defaultUriScheme = ref.read(settingsProvider).defaultUriScheme; - final EnvironmentModel? originalEnvironmentModel = ref.read( - activeEnvironmentModelProvider, - ); + final EnvironmentModel? originalEnvironmentModel = + ref.read(activeEnvironmentModelProvider); RequestModel executionRequestModel = requestModel!.copyWith(); @@ -338,18 +373,18 @@ class CollectionStateNotifier executionRequestModel = await ref .read(jsRuntimeNotifierProvider.notifier) .handlePreRequestScript( - executionRequestModel, - originalEnvironmentModel, - (envModel, updatedValues) { - ref - .read(environmentsStateNotifierProvider.notifier) - .updateEnvironment( - envModel.id, - name: envModel.name, - values: updatedValues, - ); - }, - ); + executionRequestModel, + originalEnvironmentModel, + (envModel, updatedValues) { + ref + .read(environmentsStateNotifierProvider.notifier) + .updateEnvironment( + envModel.id, + name: envModel.name, + values: updatedValues, + ); + }, + ); } APIType apiType = executionRequestModel.apiType; @@ -358,12 +393,10 @@ class CollectionStateNotifier if (apiType == APIType.ai) { substitutedHttpRequestModel = getSubstitutedHttpRequestModel( - executionRequestModel.aiRequestModel!.httpRequestModel!, - ); + executionRequestModel.aiRequestModel!.httpRequestModel!); } else { substitutedHttpRequestModel = getSubstitutedHttpRequestModel( - executionRequestModel.httpRequestModel!, - ); + executionRequestModel.httpRequestModel!); } // Terminal @@ -416,73 +449,71 @@ class CollectionStateNotifier 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}; - // Terminal: append chunk preview - if (response != null && response.body.isNotEmpty) { - terminal.addNetworkChunk( - logId, - BodyChunk( - ts: DateTime.now(), - text: response.body, - sizeBytes: response.body.codeUnits.length, - ), - ); - } - unsave(); - - if (historyModel != null && httpResponseModel != null) { - historyModel = historyModel!.copyWith( - httpResponseModel: httpResponseModel!, - ); - ref - .read(historyMetaStateNotifier.notifier) - .editHistoryRequest(historyModel!); - } - } else { - streamingMode = false; - } + 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, + ], + ); - if (!completer.isCompleted) { - completer.complete((response, duration, errorMessage)); - } - }, - onDone: () { - sub?.cancel(); + newRequestModel = newRequestModel.copyWith( + httpResponseModel: httpResponseModel, + isStreaming: true, + ); state = { ...state!, - requestId: newRequestModel.copyWith(isStreaming: false), + requestId: newRequestModel, }; + // Terminal: append chunk preview + if (response != null && response.body.isNotEmpty) { + terminal.addNetworkChunk( + logId, + BodyChunk( + ts: DateTime.now(), + text: response.body, + sizeBytes: response.body.codeUnits.length, + ), + ); + } unsave(); - }, - onError: (e) { - if (!completer.isCompleted) { - completer.complete((null, null, 'StreamError: $e')); + + if (historyModel != null && httpResponseModel != null) { + historyModel = + historyModel!.copyWith(httpResponseModel: httpResponseModel!); + ref + .read(historyMetaStateNotifier.notifier) + .editHistoryRequest(historyModel!); } - terminal.failNetwork(logId, 'StreamError: $e'); - }, - ); + } else { + streamingMode = false; + } + + 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')); + } + terminal.failNetwork(logId, 'StreamError: $e'); + }); final (response, duration, errorMessage) = await completer.future; @@ -502,13 +533,13 @@ class CollectionStateNotifier isStreamingResponse: isStreamingResponse, ); - //AI-FORMATTING for Non Streaming Variant + //AI-FORMATTING for Non Streaming Varaint if (!streamingMode && apiType == APIType.ai && response.statusCode == 200) { final fb = executionRequestModel.aiRequestModel?.getFormattedOutput( - kJsonDecoder.convert(httpResponseModel?.body ?? "Error parsing body"), - ); + kJsonDecoder + .convert(httpResponseModel?.body ?? "Error parsing body")); httpResponseModel = httpResponseModel?.copyWith(formattedBody: fb); } @@ -556,23 +587,26 @@ class CollectionStateNotifier newRequestModel = await ref .read(jsRuntimeNotifierProvider.notifier) .handlePostResponseScript( - newRequestModel, - originalEnvironmentModel, - (envModel, updatedValues) { - ref - .read(environmentsStateNotifierProvider.notifier) - .updateEnvironment( - envModel.id, - name: envModel.name, - values: updatedValues, - ); - }, - ); + newRequestModel, + originalEnvironmentModel, + (envModel, updatedValues) { + ref + .read(environmentsStateNotifierProvider.notifier) + .updateEnvironment( + envModel.id, + name: envModel.name, + values: updatedValues, + ); + }, + ); } } // Final state update - state = {...state!, requestId: newRequestModel}; + state = { + ...state!, + requestId: newRequestModel, + }; unsave(); } @@ -652,11 +686,14 @@ class CollectionStateNotifier } HttpRequestModel getSubstitutedHttpRequestModel( - HttpRequestModel httpRequestModel, - ) { + HttpRequestModel httpRequestModel) { var envMap = ref.read(availableEnvironmentVariablesStateProvider); var activeEnvId = ref.read(activeEnvironmentIdStateProvider); - return substituteHttpRequestModel(httpRequestModel, envMap, activeEnvId); + return substituteHttpRequestModel( + httpRequestModel, + envMap, + activeEnvId, + ); } } diff --git a/lib/screens/home_page/collection_pane.dart b/lib/screens/home_page/collection_pane.dart index c62e3ce96..f184333ca 100644 --- a/lib/screens/home_page/collection_pane.dart +++ b/lib/screens/home_page/collection_pane.dart @@ -9,18 +9,21 @@ import 'package:apidash/consts.dart'; import '../common_widgets/common_widgets.dart'; class CollectionPane extends ConsumerWidget { - const CollectionPane({super.key}); + const CollectionPane({ + super.key, + }); @override Widget build(BuildContext context, WidgetRef ref) { final collection = ref.watch(collectionStateNotifierProvider); var sm = ScaffoldMessenger.of(context); if (collection == null) { - return const Center(child: CircularProgressIndicator()); + return const Center( + child: CircularProgressIndicator(), + ); } return Padding( - padding: - (!context.isMediumWindow && kIsMacOS ? kPt24l4 : kPt8l4) + + padding: (!context.isMediumWindow && kIsMacOS ? kPt24l4 : kPt8l4) + (context.isMediumWindow ? kPb70 : EdgeInsets.zero), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -35,18 +38,23 @@ class CollectionPane extends ConsumerWidget { ), if (context.isMediumWindow) kVSpacer6, if (context.isMediumWindow) - Padding(padding: kPh8, child: EnvironmentDropdown()), + Padding( + padding: kPh8, + child: EnvironmentDropdown(), + ), kVSpacer10, SidebarFilter( - filterHintText: kHintFilterByNameOrUrl, + filterHintText: "Filter by name or url", onFilterFieldChanged: (value) { - ref.read(collectionSearchQueryProvider.notifier).state = value - .toLowerCase(); + ref.read(collectionSearchQueryProvider.notifier).state = + value.toLowerCase(); }, ), kVSpacer10, - const Expanded(child: RequestList()), - kVSpacer5, + const Expanded( + child: RequestList(), + ), + kVSpacer5 ], ), ); @@ -54,7 +62,9 @@ class CollectionPane extends ConsumerWidget { } class RequestList extends ConsumerStatefulWidget { - const RequestList({super.key}); + const RequestList({ + super.key, + }); @override ConsumerState createState() => _RequestListState(); @@ -79,11 +89,8 @@ class _RequestListState extends ConsumerState { Widget build(BuildContext context) { final requestSequence = ref.watch(requestSequenceProvider); final requestItems = ref.watch(collectionStateNotifierProvider)!; - final alwaysShowCollectionPaneScrollbar = ref.watch( - settingsProvider.select( - (value) => value.alwaysShowCollectionPaneScrollbar, - ), - ); + final alwaysShowCollectionPaneScrollbar = ref.watch(settingsProvider + .select((value) => value.alwaysShowCollectionPaneScrollbar)); final filterQuery = ref.watch(collectionSearchQueryProvider).trim(); return Scrollbar( @@ -131,7 +138,10 @@ class _RequestListState extends ConsumerState { index: index, child: Padding( padding: kP1, - child: RequestItem(id: id, requestModel: requestItems[id]!), + child: RequestItem( + id: id, + requestModel: requestItems[id]!, + ), ), ); }, @@ -146,13 +156,16 @@ class _RequestListState extends ConsumerState { controller: controller, children: requestSequence.map((id) { var item = requestItems[id]!; - if (item.httpRequestModel!.url.toLowerCase().contains( - filterQuery, - ) || + if (item.httpRequestModel!.url + .toLowerCase() + .contains(filterQuery) || item.name.toLowerCase().contains(filterQuery)) { return Padding( padding: kP1, - child: RequestItem(id: id, requestModel: item), + child: RequestItem( + id: id, + requestModel: item, + ), ); } return kSizedBoxEmpty; @@ -163,7 +176,11 @@ class _RequestListState extends ConsumerState { } class RequestItem extends ConsumerWidget { - const RequestItem({super.key, required this.id, required this.requestModel}); + const RequestItem({ + super.key, + required this.id, + required this.requestModel, + }); final String id; final RequestModel requestModel; @@ -221,7 +238,22 @@ class RequestItem extends ConsumerWidget { ); } if (item == ItemMenuOption.delete) { - ref.read(collectionStateNotifierProvider.notifier).remove(id: id); + final deletedName = + requestModel.name.isEmpty ? kUntitled : requestModel.name; + final notifier = ref.read(collectionStateNotifierProvider.notifier); + notifier.remove(id: id); + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + getSnackBar( + '"$deletedName" deleted', + small: false, + duration: const Duration(seconds: 5), + action: SnackBarAction( + label: 'Undo', + onPressed: () => notifier.undoDelete(), + ), + ), + ); } if (item == ItemMenuOption.duplicate) { ref.read(collectionStateNotifierProvider.notifier).duplicate(id: id); diff --git a/lib/screens/home_page/editor_pane/request_editor_top_bar.dart b/lib/screens/home_page/editor_pane/request_editor_top_bar.dart index 1bd31c06d..256894de4 100644 --- a/lib/screens/home_page/editor_pane/request_editor_top_bar.dart +++ b/lib/screens/home_page/editor_pane/request_editor_top_bar.dart @@ -13,9 +13,8 @@ class RequestEditorTopBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { ref.watch(selectedIdStateProvider); - final name = ref.watch( - selectedRequestModelProvider.select((value) => value?.name), - ); + final name = + ref.watch(selectedRequestModelProvider.select((value) => value?.name)); return Padding( padding: kP4, child: Row( @@ -33,7 +32,7 @@ class RequestEditorTopBar extends ConsumerWidget { kHSpacer10, EditorTitleActions( onRenamePressed: () { - showRenameDialog(context, kLabelRenameRequest, name, (val) { + showRenameDialog(context, "Rename Request", name, (val) { ref .read(collectionStateNotifierProvider.notifier) .update(name: val); @@ -41,8 +40,24 @@ class RequestEditorTopBar extends ConsumerWidget { }, onDuplicatePressed: () => ref.read(collectionStateNotifierProvider.notifier).duplicate(), - onDeletePressed: () => - ref.read(collectionStateNotifierProvider.notifier).remove(), + onDeletePressed: () { + final deletedName = name.isNullOrEmpty() ? kUntitled : name!; + final notifier = + ref.read(collectionStateNotifierProvider.notifier); + notifier.remove(); + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + getSnackBar( + '"$deletedName" deleted', + small: false, + duration: const Duration(seconds: 5), + action: SnackBarAction( + label: 'Undo', + onPressed: () => notifier.undoDelete(), + ), + ), + ); + }, ), kHSpacer10, const EnvironmentDropdown(), diff --git a/packages/apidash_design_system/lib/widgets/snackbar.dart b/packages/apidash_design_system/lib/widgets/snackbar.dart index 434d74733..e4f23b738 100644 --- a/packages/apidash_design_system/lib/widgets/snackbar.dart +++ b/packages/apidash_design_system/lib/widgets/snackbar.dart @@ -4,16 +4,20 @@ SnackBar getSnackBar( String text, { bool small = true, Color? color, + SnackBarAction? action, + Duration duration = const Duration(seconds: 4), }) { return SnackBar( width: small ? 300 : 500, backgroundColor: color, behavior: SnackBarBehavior.floating, + duration: duration, content: Text( text, softWrap: true, textAlign: TextAlign.center, ), + action: action, showCloseIcon: true, ); } diff --git a/test/providers/collection_providers_test.dart b/test/providers/collection_providers_test.dart index 8fc177998..8cf8ab5ec 100644 --- a/test/providers/collection_providers_test.dart +++ b/test/providers/collection_providers_test.dart @@ -16,47 +16,45 @@ void main() async { }); testWidgets( - 'Request method changes from GET to POST when body is added and Snackbar is shown', - (WidgetTester tester) async { - // Set up the test environment - final container = createContainer(); - final notifier = container.read(collectionStateNotifierProvider.notifier); + 'Request method changes from GET to POST when body is added and Snackbar is shown', + (WidgetTester tester) async { + // Set up the test environment + final container = createContainer(); + final notifier = container.read(collectionStateNotifierProvider.notifier); - // Ensure the initial request is a GET request with no body - final id = notifier.state!.entries.first.key; - expect( - notifier.getRequestModel(id)!.httpRequestModel!.method, - HTTPVerb.get, - ); - expect(notifier.getRequestModel(id)!.httpRequestModel!.body, isNull); + // Ensure the initial request is a GET request with no body + final id = notifier.state!.entries.first.key; + expect( + notifier.getRequestModel(id)!.httpRequestModel!.method, HTTPVerb.get); + expect(notifier.getRequestModel(id)!.httpRequestModel!.body, isNull); - // Build the EditRequestBody widget - await tester.pumpWidget( - UncontrolledProviderScope( - container: container, - child: const MaterialApp(home: Scaffold(body: EditRequestBody())), + // Build the EditRequestBody widget + await tester.pumpWidget( + ProviderScope( + // ignore: deprecated_member_use + parent: container, + child: const MaterialApp( + home: Scaffold( + body: EditRequestBody(), + ), ), - ); + ), + ); - // Add a body to the request, which should trigger the method change - await tester.enterText(find.byType(TextFieldEditor), 'new body added'); - await tester.pump(); // Process the state change + // Add a body to the request, which should trigger the method change + await tester.enterText(find.byType(TextFieldEditor), 'new body added'); + await tester.pump(); // Process the state change - // Verify that the request method changed to POST - expect( - notifier.getRequestModel(id)!.httpRequestModel!.method, - HTTPVerb.post, - ); + // Verify that the request method changed to POST + expect( + notifier.getRequestModel(id)!.httpRequestModel!.method, HTTPVerb.post); - // Verify that the Snackbar is shown - expect(find.text('Switched to POST method'), findsOneWidget); - }, - skip: true, - ); + // Verify that the Snackbar is shown + expect(find.text('Switched to POST method'), findsOneWidget); + }, skip: true); - testWidgets('SSE Output is rendered correctly in UI', ( - WidgetTester tester, - ) async { + testWidgets('SSE Output is rendered correctly in UI', + (WidgetTester tester) async { HttpOverrides.global = null; //enable networking in flutter_test final container = createContainer(); @@ -88,10 +86,13 @@ void main() async { // Render the widget await tester.pumpWidget( - UncontrolledProviderScope( - container: container, + ProviderScope( + // ignore: deprecated_member_use + parent: container, child: MaterialApp( - home: Scaffold(body: ResponseBody(selectedRequestModel: rm)), + home: Scaffold( + body: ResponseBody(selectedRequestModel: rm), + ), ), ), ); @@ -99,12 +100,10 @@ void main() async { final textWidgets = tester.widgetList(find.byType(Text)); final matchingTextCount = textWidgets - .where( - (text) => - text.data != null && - text.data!.startsWith('data') && - sseOutput.contains(text.data!.trim()), - ) + .where((text) => + text.data != null && + text.data!.startsWith('data') && + sseOutput.contains(text.data!.trim())) .length; expect( @@ -134,41 +133,37 @@ void main() async { username: 'testuser', password: 'testpass', ); - const authModel = AuthModel(type: APIAuthType.basic, basic: basicAuth); + const authModel = AuthModel( + type: APIAuthType.basic, + basic: basicAuth, + ); notifier.update(id: id, authModel: authModel); final updatedRequest = notifier.getRequestModel(id); expect( - updatedRequest?.httpRequestModel?.authModel?.type, - APIAuthType.basic, - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.basic?.username, - 'testuser', - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.basic?.password, - 'testpass', - ); + updatedRequest?.httpRequestModel?.authModel?.type, APIAuthType.basic); + expect(updatedRequest?.httpRequestModel?.authModel?.basic?.username, + 'testuser'); + expect(updatedRequest?.httpRequestModel?.authModel?.basic?.password, + 'testpass'); }); test('should update request with bearer authentication', () { final id = notifier.state!.entries.first.key; const bearerAuth = AuthBearerModel(token: 'bearer-token-123'); - const authModel = AuthModel(type: APIAuthType.bearer, bearer: bearerAuth); + const authModel = AuthModel( + type: APIAuthType.bearer, + bearer: bearerAuth, + ); notifier.update(id: id, authModel: authModel); final updatedRequest = notifier.getRequestModel(id); - expect( - updatedRequest?.httpRequestModel?.authModel?.type, - APIAuthType.bearer, - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.bearer?.token, - 'bearer-token-123', - ); + expect(updatedRequest?.httpRequestModel?.authModel?.type, + APIAuthType.bearer); + expect(updatedRequest?.httpRequestModel?.authModel?.bearer?.token, + 'bearer-token-123'); }); test('should update request with API key authentication', () { @@ -178,27 +173,22 @@ void main() async { location: 'header', name: 'X-API-Key', ); - const authModel = AuthModel(type: APIAuthType.apiKey, apikey: apiKeyAuth); + const authModel = AuthModel( + type: APIAuthType.apiKey, + apikey: apiKeyAuth, + ); notifier.update(id: id, authModel: authModel); final updatedRequest = notifier.getRequestModel(id); - expect( - updatedRequest?.httpRequestModel?.authModel?.type, - APIAuthType.apiKey, - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.apikey?.key, - 'api-key-123', - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.apikey?.location, - 'header', - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.apikey?.name, - 'X-API-Key', - ); + expect(updatedRequest?.httpRequestModel?.authModel?.type, + APIAuthType.apiKey); + expect(updatedRequest?.httpRequestModel?.authModel?.apikey?.key, + 'api-key-123'); + expect(updatedRequest?.httpRequestModel?.authModel?.apikey?.location, + 'header'); + expect(updatedRequest?.httpRequestModel?.authModel?.apikey?.name, + 'X-API-Key'); }); test('should update request with JWT authentication', () { @@ -213,27 +203,24 @@ void main() async { queryParamKey: 'token', header: 'Authorization', ); - const authModel = AuthModel(type: APIAuthType.jwt, jwt: jwtAuth); + const authModel = AuthModel( + type: APIAuthType.jwt, + jwt: jwtAuth, + ); notifier.update(id: id, authModel: authModel); final updatedRequest = notifier.getRequestModel(id); expect( - updatedRequest?.httpRequestModel?.authModel?.type, - APIAuthType.jwt, - ); + updatedRequest?.httpRequestModel?.authModel?.type, APIAuthType.jwt); + expect(updatedRequest?.httpRequestModel?.authModel?.jwt?.secret, + 'jwt-secret'); expect( - updatedRequest?.httpRequestModel?.authModel?.jwt?.secret, - 'jwt-secret', - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.jwt?.algorithm, - 'HS256', - ); + updatedRequest?.httpRequestModel?.authModel?.jwt?.algorithm, 'HS256'); expect( - updatedRequest?.httpRequestModel?.authModel?.jwt?.isSecretBase64Encoded, - false, - ); + updatedRequest + ?.httpRequestModel?.authModel?.jwt?.isSecretBase64Encoded, + false); }); test('should update request with digest authentication', () { @@ -247,27 +234,22 @@ void main() async { qop: 'auth', opaque: 'test-opaque', ); - const authModel = AuthModel(type: APIAuthType.digest, digest: digestAuth); + const authModel = AuthModel( + type: APIAuthType.digest, + digest: digestAuth, + ); notifier.update(id: id, authModel: authModel); final updatedRequest = notifier.getRequestModel(id); - expect( - updatedRequest?.httpRequestModel?.authModel?.type, - APIAuthType.digest, - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.digest?.username, - 'digestuser', - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.digest?.realm, - 'test-realm', - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.digest?.algorithm, - 'MD5', - ); + expect(updatedRequest?.httpRequestModel?.authModel?.type, + APIAuthType.digest); + expect(updatedRequest?.httpRequestModel?.authModel?.digest?.username, + 'digestuser'); + expect(updatedRequest?.httpRequestModel?.authModel?.digest?.realm, + 'test-realm'); + expect(updatedRequest?.httpRequestModel?.authModel?.digest?.algorithm, + 'MD5'); }); test('should remove authentication when set to none', () { @@ -278,7 +260,10 @@ void main() async { username: 'testuser', password: 'testpass', ); - const authModel = AuthModel(type: APIAuthType.basic, basic: basicAuth); + const authModel = AuthModel( + type: APIAuthType.basic, + basic: basicAuth, + ); notifier.update(id: id, authModel: authModel); // Then remove auth @@ -287,9 +272,7 @@ void main() async { final updatedRequest = notifier.getRequestModel(id); expect( - updatedRequest?.httpRequestModel?.authModel?.type, - APIAuthType.none, - ); + updatedRequest?.httpRequestModel?.authModel?.type, APIAuthType.none); expect(updatedRequest?.httpRequestModel?.authModel?.basic, isNull); }); @@ -299,7 +282,10 @@ void main() async { username: 'testuser', password: 'testpass', ); - const authModel = AuthModel(type: APIAuthType.basic, basic: basicAuth); + const authModel = AuthModel( + type: APIAuthType.basic, + basic: basicAuth, + ); notifier.update(id: id, authModel: authModel); notifier.duplicate(id: id); @@ -308,38 +294,31 @@ void main() async { final duplicatedId = sequence.firstWhere((element) => element != id); final duplicatedRequest = notifier.getRequestModel(duplicatedId); - expect( - duplicatedRequest?.httpRequestModel?.authModel?.type, - APIAuthType.basic, - ); - expect( - duplicatedRequest?.httpRequestModel?.authModel?.basic?.username, - 'testuser', - ); - expect( - duplicatedRequest?.httpRequestModel?.authModel?.basic?.password, - 'testpass', - ); + expect(duplicatedRequest?.httpRequestModel?.authModel?.type, + APIAuthType.basic); + expect(duplicatedRequest?.httpRequestModel?.authModel?.basic?.username, + 'testuser'); + expect(duplicatedRequest?.httpRequestModel?.authModel?.basic?.password, + 'testpass'); }); test('should not clear auth when clearing response', () { final id = notifier.state!.entries.first.key; const bearerAuth = AuthBearerModel(token: 'bearer-token-123'); - const authModel = AuthModel(type: APIAuthType.bearer, bearer: bearerAuth); + const authModel = AuthModel( + type: APIAuthType.bearer, + bearer: bearerAuth, + ); notifier.update(id: id, authModel: authModel); notifier.clearResponse(id: id); final updatedRequest = notifier.getRequestModel(id); // Auth should be preserved when clearing response - expect( - updatedRequest?.httpRequestModel?.authModel?.type, - APIAuthType.bearer, - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.bearer?.token, - 'bearer-token-123', - ); + expect(updatedRequest?.httpRequestModel?.authModel?.type, + APIAuthType.bearer); + expect(updatedRequest?.httpRequestModel?.authModel?.bearer?.token, + 'bearer-token-123'); }); test('should handle auth with special characters', () { @@ -348,19 +327,18 @@ void main() async { username: 'user@domain.com', password: r'P@ssw0rd!@#$%^&*()', ); - const authModel = AuthModel(type: APIAuthType.basic, basic: basicAuth); + const authModel = AuthModel( + type: APIAuthType.basic, + basic: basicAuth, + ); notifier.update(id: id, authModel: authModel); final updatedRequest = notifier.getRequestModel(id); - expect( - updatedRequest?.httpRequestModel?.authModel?.basic?.username, - 'user@domain.com', - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.basic?.password, - r'P@ssw0rd!@#$%^&*()', - ); + expect(updatedRequest?.httpRequestModel?.authModel?.basic?.username, + 'user@domain.com'); + expect(updatedRequest?.httpRequestModel?.authModel?.basic?.password, + r'P@ssw0rd!@#$%^&*()'); }); test('should handle multiple auth type changes', () { @@ -398,36 +376,32 @@ void main() async { notifier.update(id: id, authModel: apiKeyAuthModel); final updatedRequest = notifier.getRequestModel(id); + expect(updatedRequest?.httpRequestModel?.authModel?.type, + APIAuthType.apiKey); + expect(updatedRequest?.httpRequestModel?.authModel?.apikey?.key, + 'api-key-123'); + expect(updatedRequest?.httpRequestModel?.authModel?.apikey?.location, + 'query'); expect( - updatedRequest?.httpRequestModel?.authModel?.type, - APIAuthType.apiKey, - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.apikey?.key, - 'api-key-123', - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.apikey?.location, - 'query', - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.apikey?.name, - 'apikey', - ); + updatedRequest?.httpRequestModel?.authModel?.apikey?.name, 'apikey'); }); test('should handle empty auth values', () { final id = notifier.state!.entries.first.key; - const basicAuth = AuthBasicAuthModel(username: '', password: ''); - const authModel = AuthModel(type: APIAuthType.basic, basic: basicAuth); + const basicAuth = AuthBasicAuthModel( + username: '', + password: '', + ); + const authModel = AuthModel( + type: APIAuthType.basic, + basic: basicAuth, + ); notifier.update(id: id, authModel: authModel); final updatedRequest = notifier.getRequestModel(id); expect( - updatedRequest?.httpRequestModel?.authModel?.type, - APIAuthType.basic, - ); + updatedRequest?.httpRequestModel?.authModel?.type, APIAuthType.basic); expect(updatedRequest?.httpRequestModel?.authModel?.basic?.username, ''); expect(updatedRequest?.httpRequestModel?.authModel?.basic?.password, ''); }); @@ -446,7 +420,10 @@ void main() async { queryParamKey: 'token', header: 'Authorization', ); - const authModel = AuthModel(type: APIAuthType.jwt, jwt: jwtAuth); + const authModel = AuthModel( + type: APIAuthType.jwt, + jwt: jwtAuth, + ); notifier.update(id: id, authModel: authModel); await notifier.saveData(); @@ -457,9 +434,8 @@ void main() async { newContainer = ProviderContainer(); // Wait for the container to initialize by accessing the provider - final newNotifier = newContainer.read( - collectionStateNotifierProvider.notifier, - ); + final newNotifier = + newContainer.read(collectionStateNotifierProvider.notifier); // Give some time for the microtask in the constructor to complete await Future.delayed(const Duration(milliseconds: 10)); @@ -467,17 +443,11 @@ void main() async { final loadedRequest = newNotifier.getRequestModel(id); expect( - loadedRequest?.httpRequestModel?.authModel?.type, - APIAuthType.jwt, - ); - expect( - loadedRequest?.httpRequestModel?.authModel?.jwt?.secret, - 'jwt-secret', - ); - expect( - loadedRequest?.httpRequestModel?.authModel?.jwt?.algorithm, - 'HS256', - ); + loadedRequest?.httpRequestModel?.authModel?.type, APIAuthType.jwt); + expect(loadedRequest?.httpRequestModel?.authModel?.jwt?.secret, + 'jwt-secret'); + expect(loadedRequest?.httpRequestModel?.authModel?.jwt?.algorithm, + 'HS256'); } finally { newContainer.dispose(); } @@ -488,7 +458,10 @@ void main() async { username: 'testuser', password: 'testpass', ); - const authModel = AuthModel(type: APIAuthType.basic, basic: basicAuth); + const authModel = AuthModel( + type: APIAuthType.basic, + basic: basicAuth, + ); final httpRequestModel = HttpRequestModel( method: HTTPVerb.get, @@ -502,17 +475,11 @@ void main() async { final addedRequest = notifier.getRequestModel(sequence.first); expect( - addedRequest?.httpRequestModel?.authModel?.type, - APIAuthType.basic, - ); - expect( - addedRequest?.httpRequestModel?.authModel?.basic?.username, - 'testuser', - ); - expect( - addedRequest?.httpRequestModel?.authModel?.basic?.password, - 'testpass', - ); + addedRequest?.httpRequestModel?.authModel?.type, APIAuthType.basic); + expect(addedRequest?.httpRequestModel?.authModel?.basic?.username, + 'testuser'); + expect(addedRequest?.httpRequestModel?.authModel?.basic?.password, + 'testpass'); }); test('should handle complex JWT configuration', () { @@ -542,35 +509,28 @@ void main() async { queryParamKey: 'jwt_token', header: 'X-JWT-Token', ); - const authModel = AuthModel(type: APIAuthType.jwt, jwt: jwtAuth); + const authModel = AuthModel( + type: APIAuthType.jwt, + jwt: jwtAuth, + ); notifier.update(id: id, authModel: authModel); final updatedRequest = notifier.getRequestModel(id); expect( - updatedRequest?.httpRequestModel?.authModel?.type, - APIAuthType.jwt, - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.jwt?.payload, - complexPayload, - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.jwt?.privateKey, - 'private-key-content', - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.jwt?.algorithm, - 'RS256', - ); + updatedRequest?.httpRequestModel?.authModel?.type, APIAuthType.jwt); + expect(updatedRequest?.httpRequestModel?.authModel?.jwt?.payload, + complexPayload); + expect(updatedRequest?.httpRequestModel?.authModel?.jwt?.privateKey, + 'private-key-content'); expect( - updatedRequest?.httpRequestModel?.authModel?.jwt?.isSecretBase64Encoded, - true, - ); + updatedRequest?.httpRequestModel?.authModel?.jwt?.algorithm, 'RS256'); expect( - updatedRequest?.httpRequestModel?.authModel?.jwt?.addTokenTo, - 'query', - ); + updatedRequest + ?.httpRequestModel?.authModel?.jwt?.isSecretBase64Encoded, + true); + expect(updatedRequest?.httpRequestModel?.authModel?.jwt?.addTokenTo, + 'query'); }); test('should handle API key in different locations', () { @@ -589,14 +549,10 @@ void main() async { notifier.update(id: id, authModel: headerAuthModel); var updatedRequest = notifier.getRequestModel(id); - expect( - updatedRequest?.httpRequestModel?.authModel?.apikey?.location, - 'header', - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.apikey?.name, - 'X-API-Key', - ); + expect(updatedRequest?.httpRequestModel?.authModel?.apikey?.location, + 'header'); + expect(updatedRequest?.httpRequestModel?.authModel?.apikey?.name, + 'X-API-Key'); // Test query location const queryApiKey = AuthApiKeyModel( @@ -611,14 +567,10 @@ void main() async { notifier.update(id: id, authModel: queryAuthModel); updatedRequest = notifier.getRequestModel(id); + expect(updatedRequest?.httpRequestModel?.authModel?.apikey?.location, + 'query'); expect( - updatedRequest?.httpRequestModel?.authModel?.apikey?.location, - 'query', - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.apikey?.name, - 'apikey', - ); + updatedRequest?.httpRequestModel?.authModel?.apikey?.name, 'apikey'); }); test('should handle digest auth with different algorithms', () { @@ -641,10 +593,8 @@ void main() async { notifier.update(id: id, authModel: md5AuthModel); var updatedRequest = notifier.getRequestModel(id); - expect( - updatedRequest?.httpRequestModel?.authModel?.digest?.algorithm, - 'MD5', - ); + expect(updatedRequest?.httpRequestModel?.authModel?.digest?.algorithm, + 'MD5'); // Test SHA-256 algorithm const sha256DigestAuth = AuthDigestModel( @@ -663,14 +613,10 @@ void main() async { notifier.update(id: id, authModel: sha256AuthModel); updatedRequest = notifier.getRequestModel(id); + expect(updatedRequest?.httpRequestModel?.authModel?.digest?.algorithm, + 'SHA-256'); expect( - updatedRequest?.httpRequestModel?.authModel?.digest?.algorithm, - 'SHA-256', - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.digest?.qop, - 'auth-int', - ); + updatedRequest?.httpRequestModel?.authModel?.digest?.qop, 'auth-int'); }); test('should handle auth model copyWith functionality', () { @@ -691,19 +637,17 @@ void main() async { username: 'updated', password: 'updated', ); - final updatedAuthModel = originalAuthModel.copyWith(basic: updatedAuth); + final updatedAuthModel = originalAuthModel.copyWith( + basic: updatedAuth, + ); notifier.update(id: id, authModel: updatedAuthModel); final updatedRequest = notifier.getRequestModel(id); - expect( - updatedRequest?.httpRequestModel?.authModel?.basic?.username, - 'updated', - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.basic?.password, - 'updated', - ); + expect(updatedRequest?.httpRequestModel?.authModel?.basic?.username, + 'updated'); + expect(updatedRequest?.httpRequestModel?.authModel?.basic?.password, + 'updated'); }); test('should handle auth with very long tokens', () { @@ -711,19 +655,18 @@ void main() async { final longToken = 'a' * 5000; // Very long token final bearerAuth = AuthBearerModel(token: longToken); - final authModel = AuthModel(type: APIAuthType.bearer, bearer: bearerAuth); + final authModel = AuthModel( + type: APIAuthType.bearer, + bearer: bearerAuth, + ); notifier.update(id: id, authModel: authModel); final updatedRequest = notifier.getRequestModel(id); - expect( - updatedRequest?.httpRequestModel?.authModel?.bearer?.token, - longToken, - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.bearer?.token.length, - 5000, - ); + expect(updatedRequest?.httpRequestModel?.authModel?.bearer?.token, + longToken); + expect(updatedRequest?.httpRequestModel?.authModel?.bearer?.token.length, + 5000); }); test('should handle auth with Unicode characters', () { @@ -732,19 +675,18 @@ void main() async { username: 'user_测试_тест_テスト', password: 'password_🔑_🚀_💻', ); - const authModel = AuthModel(type: APIAuthType.basic, basic: basicAuth); + const authModel = AuthModel( + type: APIAuthType.basic, + basic: basicAuth, + ); notifier.update(id: id, authModel: authModel); final updatedRequest = notifier.getRequestModel(id); - expect( - updatedRequest?.httpRequestModel?.authModel?.basic?.username, - 'user_测试_тест_テスト', - ); - expect( - updatedRequest?.httpRequestModel?.authModel?.basic?.password, - 'password_🔑_🚀_💻', - ); + expect(updatedRequest?.httpRequestModel?.authModel?.basic?.username, + 'user_测试_тест_テスト'); + expect(updatedRequest?.httpRequestModel?.authModel?.basic?.password, + 'password_🔑_🚀_💻'); }); tearDown(() { @@ -824,7 +766,11 @@ void main() async { ); // Then clear scripts - notifier.update(id: id, preRequestScript: '', postRequestScript: ''); + notifier.update( + id: id, + preRequestScript: '', + postRequestScript: '', + ); final updatedRequest = notifier.getRequestModel(id); expect(updatedRequest?.preRequestScript, equals('')); @@ -902,7 +848,11 @@ void main() async { final id = notifier.state!.entries.first.key; // Test with empty strings - notifier.update(id: id, preRequestScript: '', postRequestScript: ''); + notifier.update( + id: id, + preRequestScript: '', + postRequestScript: '', + ); var updatedRequest = notifier.getRequestModel(id); expect(updatedRequest?.preRequestScript, equals('')); @@ -917,13 +867,9 @@ void main() async { updatedRequest = notifier.getRequestModel(id); expect( - updatedRequest?.preRequestScript, - equals('ad.console.log("test");'), - ); + updatedRequest?.preRequestScript, equals('ad.console.log("test");')); expect( - updatedRequest?.postRequestScript, - equals('ad.console.log("test");'), - ); + updatedRequest?.postRequestScript, equals('ad.console.log("test");')); }); test('should save and load scripts correctly', () async { @@ -950,9 +896,8 @@ void main() async { late ProviderContainer newContainer; try { newContainer = ProviderContainer(); - final newNotifier = newContainer.read( - collectionStateNotifierProvider.notifier, - ); + final newNotifier = + newContainer.read(collectionStateNotifierProvider.notifier); // Give some time for the microtask in the constructor to complete await Future.delayed(const Duration(milliseconds: 10)); @@ -1105,70 +1050,61 @@ void main() async { }); test( - 'should handle script updates without affecting other request properties', - () { - final id = notifier.state!.entries.first.key; - - // First set up a complete request - notifier.update( - id: id, - method: HTTPVerb.post, - url: 'https://api.apidash.dev/test', - headers: const [ - NameValueModel(name: 'Content-Type', value: 'application/json'), - NameValueModel(name: 'Accept', value: 'application/json'), - ], - body: '{"test": "data"}', - name: 'Test Request', - description: 'A test request with scripts', - ); - - final beforeRequest = notifier.getRequestModel(id); - - // Now update only scripts - const newPreScript = 'ad.console.log("Updated pre-script");'; - const newPostScript = 'ad.console.log("Updated post-script");'; + 'should handle script updates without affecting other request properties', + () { + final id = notifier.state!.entries.first.key; - notifier.update( - id: id, - preRequestScript: newPreScript, - postRequestScript: newPostScript, - ); + // First set up a complete request + notifier.update( + id: id, + method: HTTPVerb.post, + url: 'https://api.apidash.dev/test', + headers: const [ + NameValueModel(name: 'Content-Type', value: 'application/json'), + NameValueModel(name: 'Accept', value: 'application/json'), + ], + body: '{"test": "data"}', + name: 'Test Request', + description: 'A test request with scripts', + ); - final afterRequest = notifier.getRequestModel(id); + final beforeRequest = notifier.getRequestModel(id); - // Verify scripts were updated - expect(afterRequest?.preRequestScript, equals(newPreScript)); - expect(afterRequest?.postRequestScript, equals(newPostScript)); + // Now update only scripts + const newPreScript = 'ad.console.log("Updated pre-script");'; + const newPostScript = 'ad.console.log("Updated post-script");'; - // Verify other properties were preserved - expect( - afterRequest?.httpRequestModel?.method, - equals(beforeRequest?.httpRequestModel?.method), - ); - expect( - afterRequest?.httpRequestModel?.url, - equals(beforeRequest?.httpRequestModel?.url), - ); - expect( - afterRequest?.httpRequestModel?.headers, - equals(beforeRequest?.httpRequestModel?.headers), - ); - expect( - afterRequest?.httpRequestModel?.body, - equals(beforeRequest?.httpRequestModel?.body), - ); - expect(afterRequest?.name, equals(beforeRequest?.name)); - expect(afterRequest?.description, equals(beforeRequest?.description)); - }, - ); + notifier.update( + id: id, + preRequestScript: newPreScript, + postRequestScript: newPostScript, + ); + + final afterRequest = notifier.getRequestModel(id); + + // Verify scripts were updated + expect(afterRequest?.preRequestScript, equals(newPreScript)); + expect(afterRequest?.postRequestScript, equals(newPostScript)); + + // Verify other properties were preserved + expect(afterRequest?.httpRequestModel?.method, + equals(beforeRequest?.httpRequestModel?.method)); + expect(afterRequest?.httpRequestModel?.url, + equals(beforeRequest?.httpRequestModel?.url)); + expect(afterRequest?.httpRequestModel?.headers, + equals(beforeRequest?.httpRequestModel?.headers)); + expect(afterRequest?.httpRequestModel?.body, + equals(beforeRequest?.httpRequestModel?.body)); + expect(afterRequest?.name, equals(beforeRequest?.name)); + expect(afterRequest?.description, equals(beforeRequest?.description)); + }); test( - 'should not modify original state during script execution - only execution copy', - () { - final id = notifier.state!.entries.first.key; + 'should not modify original state during script execution - only execution copy', + () { + final id = notifier.state!.entries.first.key; - const preRequestScript = r''' + const preRequestScript = r''' // Script that modifies request properties ad.request.headers.set('X-Script-Modified', 'true'); ad.request.headers.set('Authorization', 'Bearer script-token'); @@ -1178,154 +1114,195 @@ void main() async { ad.console.log('Pre-request script executed and modified request'); '''; - // Set up initial request properties - notifier.update( - id: id, - method: HTTPVerb.get, - url: 'https://api.apidash.dev/api', - headers: const [ - NameValueModel(name: 'Content-Type', value: 'application/json'), - NameValueModel(name: 'Accept', value: 'application/json'), + // Set up initial request properties + notifier.update( + id: id, + method: HTTPVerb.get, + url: 'https://api.apidash.dev/api', + headers: const [ + NameValueModel(name: 'Content-Type', value: 'application/json'), + NameValueModel(name: 'Accept', value: 'application/json'), + ], + params: const [ + NameValueModel(name: 'originalParam', value: 'originalValue'), + ], + preRequestScript: preRequestScript, + ); + + // Capture the original state before script execution simulation + final originalRequest = notifier.getRequestModel(id); + final originalHttpRequestModel = originalRequest!.httpRequestModel!; + + // Test the script execution isolation by simulating the copyWith pattern used in sendRequest + final executionRequestModel = originalRequest.copyWith(); + + // Verify that the execution copy is separate from original + expect(executionRequestModel.id, equals(originalRequest.id)); + expect(executionRequestModel.httpRequestModel?.url, + equals(originalRequest.httpRequestModel?.url)); + expect(executionRequestModel.httpRequestModel?.headers, + equals(originalRequest.httpRequestModel?.headers)); + expect(executionRequestModel.httpRequestModel?.params, + equals(originalRequest.httpRequestModel?.params)); + + // Simulate script modifications on the execution copy + final modifiedExecutionModel = executionRequestModel.copyWith( + httpRequestModel: executionRequestModel.httpRequestModel?.copyWith( + url: 'https://api.apidash.dev/', + headers: [ + ...originalHttpRequestModel.headers ?? [], + const NameValueModel(name: 'X-Script-Modified', value: 'true'), + const NameValueModel( + name: 'Authorization', value: 'Bearer script-token'), ], - params: const [ - NameValueModel(name: 'originalParam', value: 'originalValue'), + params: [ + ...originalHttpRequestModel.params ?? [], + const NameValueModel(name: 'scriptParam', value: 'scriptValue'), ], - preRequestScript: preRequestScript, - ); + ), + ); - // Capture the original state before script execution simulation - final originalRequest = notifier.getRequestModel(id); - final originalHttpRequestModel = originalRequest!.httpRequestModel!; + // Verify the execution copy has been modified + expect(modifiedExecutionModel.httpRequestModel?.url, + equals('https://api.apidash.dev/')); + expect( + modifiedExecutionModel.httpRequestModel?.headers?.length, equals(4)); + + final hasScriptModifiedHeader = modifiedExecutionModel + .httpRequestModel?.headers + ?.any((header) => header.name == 'X-Script-Modified') ?? + false; + expect(hasScriptModifiedHeader, isTrue); + + final hasAuthHeader = modifiedExecutionModel.httpRequestModel?.headers + ?.any((header) => header.name == 'Authorization') ?? + false; + expect(hasAuthHeader, isTrue); + + final hasScriptParam = modifiedExecutionModel.httpRequestModel?.params + ?.any((param) => param.name == 'scriptParam') ?? + false; + expect(hasScriptParam, isTrue); + + // Verify that the original request in the state remains completely unchanged + final currentRequest = notifier.getRequestModel(id); + + expect(currentRequest?.httpRequestModel?.url, + equals('https://api.apidash.dev/api')); + expect(currentRequest?.httpRequestModel?.headers?.length, equals(2)); + expect(currentRequest?.httpRequestModel?.headers?[0].name, + equals('Content-Type')); + expect(currentRequest?.httpRequestModel?.headers?[0].value, + equals('application/json')); + expect( + currentRequest?.httpRequestModel?.headers?[1].name, equals('Accept')); + expect(currentRequest?.httpRequestModel?.headers?[1].value, + equals('application/json')); + expect(currentRequest?.httpRequestModel?.params?.length, equals(1)); + expect(currentRequest?.httpRequestModel?.params?[0].name, + equals('originalParam')); + expect(currentRequest?.httpRequestModel?.params?[0].value, + equals('originalValue')); + + // Verify no script-modified headers are present in the original state + final hasScriptModifiedHeaderInOriginal = currentRequest + ?.httpRequestModel?.headers + ?.any((header) => header.name == 'X-Script-Modified') ?? + false; + expect(hasScriptModifiedHeaderInOriginal, isFalse); + + final hasAuthHeaderInOriginal = currentRequest?.httpRequestModel?.headers + ?.any((header) => header.name == 'Authorization') ?? + false; + expect(hasAuthHeaderInOriginal, isFalse); + + // Verify no script-modified params are present in the original state + final hasScriptParamInOriginal = currentRequest?.httpRequestModel?.params + ?.any((param) => param.name == 'scriptParam') ?? + false; + expect(hasScriptParamInOriginal, isFalse); + + // Verify the script is preserved in the original + expect(currentRequest?.preRequestScript, equals(preRequestScript)); + }); - // Test the script execution isolation by simulating the copyWith pattern used in sendRequest - final executionRequestModel = originalRequest.copyWith(); + tearDown(() { + container.dispose(); + }); + }); - // Verify that the execution copy is separate from original - expect(executionRequestModel.id, equals(originalRequest.id)); - expect( - executionRequestModel.httpRequestModel?.url, - equals(originalRequest.httpRequestModel?.url), - ); - expect( - executionRequestModel.httpRequestModel?.headers, - equals(originalRequest.httpRequestModel?.headers), - ); - expect( - executionRequestModel.httpRequestModel?.params, - equals(originalRequest.httpRequestModel?.params), - ); - - // Simulate script modifications on the execution copy - final modifiedExecutionModel = executionRequestModel.copyWith( - httpRequestModel: executionRequestModel.httpRequestModel?.copyWith( - url: 'https://api.apidash.dev/', - headers: [ - ...originalHttpRequestModel.headers ?? [], - const NameValueModel(name: 'X-Script-Modified', value: 'true'), - const NameValueModel( - name: 'Authorization', - value: 'Bearer script-token', - ), - ], - params: [ - ...originalHttpRequestModel.params ?? [], - const NameValueModel(name: 'scriptParam', value: 'scriptValue'), - ], - ), - ); + group('CollectionStateNotifier Delete & Undo Tests', () { + late ProviderContainer container; + late CollectionStateNotifier notifier; - // Verify the execution copy has been modified - expect( - modifiedExecutionModel.httpRequestModel?.url, - equals('https://api.apidash.dev/'), - ); - expect( - modifiedExecutionModel.httpRequestModel?.headers?.length, - equals(4), - ); - - final hasScriptModifiedHeader = - modifiedExecutionModel.httpRequestModel?.headers?.any( - (header) => header.name == 'X-Script-Modified', - ) ?? - false; - expect(hasScriptModifiedHeader, isTrue); - - final hasAuthHeader = - modifiedExecutionModel.httpRequestModel?.headers?.any( - (header) => header.name == 'Authorization', - ) ?? - false; - expect(hasAuthHeader, isTrue); - - final hasScriptParam = - modifiedExecutionModel.httpRequestModel?.params?.any( - (param) => param.name == 'scriptParam', - ) ?? - false; - expect(hasScriptParam, isTrue); - - // Verify that the original request in the state remains completely unchanged - final currentRequest = notifier.getRequestModel(id); + setUp(() { + container = createContainer(); + notifier = container.read(collectionStateNotifierProvider.notifier); + }); - expect( - currentRequest?.httpRequestModel?.url, - equals('https://api.apidash.dev/api'), - ); - expect(currentRequest?.httpRequestModel?.headers?.length, equals(2)); - expect( - currentRequest?.httpRequestModel?.headers?[0].name, - equals('Content-Type'), - ); - expect( - currentRequest?.httpRequestModel?.headers?[0].value, - equals('application/json'), - ); - expect( - currentRequest?.httpRequestModel?.headers?[1].name, - equals('Accept'), - ); - expect( - currentRequest?.httpRequestModel?.headers?[1].value, - equals('application/json'), - ); - expect(currentRequest?.httpRequestModel?.params?.length, equals(1)); - expect( - currentRequest?.httpRequestModel?.params?[0].name, - equals('originalParam'), - ); - expect( - currentRequest?.httpRequestModel?.params?[0].value, - equals('originalValue'), - ); - - // Verify no script-modified headers are present in the original state - final hasScriptModifiedHeaderInOriginal = - currentRequest?.httpRequestModel?.headers?.any( - (header) => header.name == 'X-Script-Modified', - ) ?? - false; - expect(hasScriptModifiedHeaderInOriginal, isFalse); - - final hasAuthHeaderInOriginal = - currentRequest?.httpRequestModel?.headers?.any( - (header) => header.name == 'Authorization', - ) ?? - false; - expect(hasAuthHeaderInOriginal, isFalse); - - // Verify no script-modified params are present in the original state - final hasScriptParamInOriginal = - currentRequest?.httpRequestModel?.params?.any( - (param) => param.name == 'scriptParam', - ) ?? - false; - expect(hasScriptParamInOriginal, isFalse); - - // Verify the script is preserved in the original - expect(currentRequest?.preRequestScript, equals(preRequestScript)); - }, - ); + test('should remove request from state on delete', () { + final id = notifier.state!.entries.first.key; + notifier.remove(id: id); + expect(notifier.state!.containsKey(id), isFalse); + }); + + test('should restore deleted request after undoDelete', () { + notifier.add(); + final id = container.read(requestSequenceProvider).first; + notifier.remove(id: id); + notifier.undoDelete(); + expect(notifier.state!.containsKey(id), isTrue); + }); + + test('should restore deleted request at original index after undo', () { + notifier.add(); + notifier.add(); + notifier.add(); + final sequenceBefore = [...container.read(requestSequenceProvider)]; + final idToDelete = sequenceBefore[1]; + notifier.remove(id: idToDelete); + notifier.undoDelete(); + final sequenceAfter = container.read(requestSequenceProvider); + expect(sequenceAfter[1], equals(idToDelete)); + }); + + test('should select restored request after undo', () { + notifier.add(); + final id = container.read(requestSequenceProvider).first; + notifier.remove(id: id); + notifier.undoDelete(); + final selectedId = container.read(selectedIdStateProvider); + expect(selectedId, equals(id)); + }); + + test('should do nothing if undoDelete called without prior delete', () { + final keysBefore = notifier.state!.keys.toList(); + notifier.undoDelete(); + expect(notifier.state!.keys.toList(), equals(keysBefore)); + }); + + test('should only undo last delete not multiple', () { + notifier.add(); + notifier.add(); + final sequence = container.read(requestSequenceProvider); + final firstId = sequence[0]; + final secondId = sequence[1]; + notifier.remove(id: firstId); + notifier.remove(id: secondId); + notifier.undoDelete(); + expect(notifier.state!.containsKey(secondId), isTrue); + expect(notifier.state!.containsKey(firstId), isFalse); + }); + + test('should clear undo state after undoDelete is called', () { + notifier.add(); + final id = container.read(requestSequenceProvider).first; + notifier.remove(id: id); + notifier.undoDelete(); + notifier.undoDelete(); + final count = notifier.state!.keys.where((k) => k == id).length; + expect(count, equals(1)); + }); tearDown(() { container.dispose();