diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 211eaf06..32f79e5c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -154,4 +154,16 @@ ClientException with SocketException: Connection failed (OS Error: Operation not You can read more [here](https://docs.flutter.dev/platform-integration/macos/building#setting-up-entitlements) +### Android (Work in Progress) +Add the `multiDexEnabled true` line to the `defaultConfig` section at `android/app/build.gradle file` + +``` +android { + ... + defaultConfig { + ... + multiDexEnabled true + } +} +``` diff --git a/lib/codegen/codegen.dart b/lib/codegen/codegen.dart index 7c66b88f..f3f606ae 100644 --- a/lib/codegen/codegen.dart +++ b/lib/codegen/codegen.dart @@ -9,6 +9,7 @@ import 'php/guzzle.dart'; import 'python/http_client.dart'; import 'python/requests.dart'; import 'rust/actix.dart'; +import 'rust/curl_rust.dart'; import 'rust/reqwest.dart'; import 'rust/ureq.dart'; import 'js/axios.dart'; @@ -73,6 +74,8 @@ class Codegen { return PythonRequestsCodeGen().getCode(rM, boundary: boundary); case CodegenLanguage.rustActix: return RustActixCodeGen().getCode(rM, boundary: boundary); + case CodegenLanguage.rustCurl: + return RustCurlCodeGen().getCode(rM); case CodegenLanguage.rustReqwest: return RustReqwestCodeGen().getCode(rM); case CodegenLanguage.rustUreq: diff --git a/lib/codegen/rust/curl_rust.dart b/lib/codegen/rust/curl_rust.dart new file mode 100644 index 00000000..9299554f --- /dev/null +++ b/lib/codegen/rust/curl_rust.dart @@ -0,0 +1,147 @@ +import 'package:jinja/jinja.dart' as jj; +import 'package:apidash/utils/utils.dart' + show getValidRequestUri, requestModelToHARJsonRequest; +import 'package:apidash/models/models.dart' show RequestModel; +import 'package:apidash/consts.dart'; + +class RustCurlCodeGen { + final String kTemplateStart = """use curl::easy::Easy; +{% if hasJsonBody %}use serde_json::json; +{% endif %}{% if hasHeaders %}use curl::easy::List; +{% endif %} +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + +"""; + + String kTemplateUrl = """ + easy.url("{{url}}").unwrap(); +"""; + + String kTemplateMethod = """ +{% if method == 'get' or method == 'post' or method == 'put' %} + easy.{{ method }}(true).unwrap(); +{% elif method == 'delete' %} + easy.custom_request("DELETE").unwrap(); +{% elif method == 'patch' %} + easy.custom_request("PATCH").unwrap(); +{% elif method == 'head' %} + easy.nobody(true).unwrap(); +{% endif %} + +"""; + + String kTemplateRawBody = """ + easy.post_fields_copy(r#"{{body}}"#.as_bytes()).unwrap(); + + +"""; + + String kTemplateJsonBody = """ + easy.post_fields_copy(json!({{body}}).to_string().as_bytes()).unwrap(); + + +"""; + + String kTemplateFormData = """ + let mut form = curl::easy::Form::new(); + {% for field in fields %} + form.part("{{field.name}}") + {% if field.type == "file" %}.file("{{field.value}}"){% else %}.contents(b"{{field.value}}"){% endif %} + .add().unwrap(); + {% endfor %} + easy.httppost(form).unwrap(); + """; + + String kTemplateHeader = """ + {% if headers %}let mut list = List::new();{% for header, value in headers %} + list.append("{{header}}: {{value}}").unwrap();{% endfor %} + easy.http_headers(list).unwrap(); + {% endif %} + +"""; + + final String kTemplateEnd = """ + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + + String? getCode(RequestModel requestModel) { + try { + String result = ""; + var requestBody = requestModel.requestBody; + + String url = requestModel.url; + + result += jj.Template(kTemplateStart).render({ + "hasJsonBody": requestModel.hasJsonData, + "hasHeaders": (requestModel.enabledRequestHeaders != null && + requestModel.enabledRequestHeaders!.isNotEmpty) || + (requestModel.hasJsonData || requestModel.hasTextData) + }); + + var rec = getValidRequestUri( + url, + requestModel.enabledRequestParams, + ); + + Uri? uri = rec.$1; + var harJson = + requestModelToHARJsonRequest(requestModel, useEnabled: true); + + var templateUrl = jj.Template(kTemplateUrl); + result += templateUrl.render({"url": harJson["url"]}); + + var methodTemplate = jj.Template(kTemplateMethod); + result += methodTemplate.render({"method": requestModel.method.name}); + + if (uri != null) { + if (requestModel.hasTextData) { + var templateBody = jj.Template(kTemplateRawBody); + result += templateBody.render({"body": requestBody}); + } else if (requestModel.hasJsonData) { + var templateBody = jj.Template(kTemplateJsonBody); + result += templateBody.render({"body": requestBody}); + } else if (requestModel.hasFormData) { + var templateFormData = jj.Template(kTemplateFormData); + result += templateFormData.render({ + "fields": requestModel.formDataMapList, + }); + } + + var headersList = requestModel.enabledRequestHeaders; + if (headersList != null || requestModel.hasBody) { + var headers = requestModel.enabledHeadersMap; + if (requestModel.hasJsonData || requestModel.hasTextData) { + headers.putIfAbsent(kHeaderContentType, + () => requestModel.requestBodyContentType.header); + } + if (headers.isNotEmpty) { + var templateHeader = jj.Template(kTemplateHeader); + result += templateHeader.render({ + "headers": headers, + }); + } + } + + result += kTemplateEnd; + } + + return result; + } catch (e) { + return null; + } + } +} diff --git a/lib/consts.dart b/lib/consts.dart index a8828cf8..2ad9d185 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -56,6 +56,7 @@ const kForegroundOpacity = 0.05; const kOverlayBackgroundOpacity = 0.5; const kTextStyleButton = TextStyle(fontWeight: FontWeight.bold); +const kTextStyleTab = TextStyle(fontSize: 14); const kTextStyleButtonSmall = TextStyle(fontSize: 12); const kFormDataButtonLabelTextStyle = TextStyle( fontSize: 12, @@ -71,9 +72,13 @@ const kP5 = EdgeInsets.all(5); const kP8 = EdgeInsets.all(8); const kPs8 = EdgeInsets.only(left: 8); const kPs2 = EdgeInsets.only(left: 2); +const kPe8 = EdgeInsets.only(right: 8.0); const kPh20v5 = EdgeInsets.symmetric(horizontal: 20, vertical: 5); const kPh20v10 = EdgeInsets.symmetric(horizontal: 20, vertical: 10); const kP10 = EdgeInsets.all(10); +const kPv8 = EdgeInsets.symmetric(vertical: 8); +const kPv2 = EdgeInsets.symmetric(vertical: 2); +const kPh2 = EdgeInsets.symmetric(horizontal: 2); const kPt24o8 = EdgeInsets.only(top: 24, left: 8.0, right: 8.0, bottom: 8.0); const kPt5o10 = EdgeInsets.only(left: 10.0, right: 10.0, top: 5.0, bottom: 10.0); @@ -102,8 +107,9 @@ const kP8CollectionPane = EdgeInsets.only( const kPb10 = EdgeInsets.only( bottom: 10, ); -const kPr8CollectionPane = EdgeInsets.only(right: 8.0); -const kpsV5 = EdgeInsets.symmetric(vertical: 2); +const kPb15 = EdgeInsets.only( + bottom: 15, +); const kHSpacer4 = SizedBox(width: 4); const kHSpacer5 = SizedBox(width: 5); const kHSpacer10 = SizedBox(width: 10); @@ -112,9 +118,10 @@ const kVSpacer5 = SizedBox(height: 5); const kVSpacer8 = SizedBox(height: 8); const kVSpacer10 = SizedBox(height: 10); const kVSpacer20 = SizedBox(height: 20); +const kVSpacer40 = SizedBox(height: 40); const kTabAnimationDuration = Duration(milliseconds: 200); -const kTabHeight = 45.0; +const kTabHeight = 32.0; const kHeaderHeight = 32.0; const kSegmentHeight = 24.0; const kTextButtonMinWidth = 44.0; @@ -284,6 +291,7 @@ enum CodegenLanguage { pythonHttpClient("Python (http.client)", "python", "py"), rustActix("Rust (Actix Client)", "rust", "rs"), rustReqwest("Rust (reqwest)", "rust", "rs"), + rustCurl("Rust (curl-rust)", "rust", "rs"), rustUreq("Rust (ureq)", "rust", "rs"), javaOkHttp("Java (okhttp3)", "java", 'java'), javaAsyncHttpClient("Java (asynchttpclient)", "java", "java"), @@ -532,3 +540,36 @@ const kLabelSave = "Save"; const kLabelDownload = "Download"; const kLabelSaving = "Saving"; const kLabelSaved = "Saved"; +// Request Pane +const kLabelRequest = "Request"; +const kLabelHideCode = "Hide Code"; +const kLabelViewCode = "View Code"; +const kLabelURLParams = "URL Params"; +const kLabelHeaders = "Headers"; +const kLabelBody = "Body"; +const kNameCheckbox = "Checkbox"; +const kNameURLParam = "URL Parameter"; +const kNameHeader = "Header Name"; +const kNameValue = "Value"; +const kNameField = "Field"; +const kHintAddURLParam = "Add URL Parameter"; +const kHintAddValue = "Add Value"; +const kHintAddName = "Add Name"; +const kHintAddFieldName = "Add Field Name"; +const kLabelAddParam = "Add Param"; +const kLabelAddHeader = "Add Header"; +const kLabelSelectFile = "Select File"; +const kLabelAddFormField = "Add Form Field"; +// Response Pane +const kLabelNotSent = "Not Sent"; +const kLabelResponse = "Response"; +const kLabelResponseBody = "Response Body"; +const kTooltipClearResponse = "Clear Response"; +const kHeaderRow = ["Header Name", "Header Value"]; +const kLabelRequestHeaders = "Request Headers"; +const kLabelResponseHeaders = "Response Headers"; +const kLabelItems = "items"; +const kMsgError = "Error: Response data does not exist."; +const kMsgNullBody = "Response body is missing (null)."; +const kMsgNoContent = "No content"; +const kMsgUnknowContentType = "Unknown Response Content-Type"; diff --git a/lib/models/request_model.dart b/lib/models/request_model.dart index 3a04c0eb..d8b9d6af 100644 --- a/lib/models/request_model.dart +++ b/lib/models/request_model.dart @@ -97,6 +97,7 @@ class RequestModel { RequestModel duplicate({ required String id, String? name, + int? requestTabIndex, }) { return RequestModel( id: id, @@ -104,6 +105,7 @@ class RequestModel { url: url, name: name ?? "${this.name} (copy)", description: description, + requestTabIndex: requestTabIndex ?? 0, requestHeaders: requestHeaders != null ? [...requestHeaders!] : null, requestParams: requestParams != null ? [...requestParams!] : null, isHeaderEnabledList: diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 07e26822..5068f4ce 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -102,6 +102,7 @@ class CollectionStateNotifier final newModel = currentModel.duplicate( id: id, name: currentModel.name, + requestTabIndex: currentModel.requestTabIndex, ); var map = {...state!}; map[id] = newModel; diff --git a/lib/providers/ui_providers.dart b/lib/providers/ui_providers.dart index 2fddb2ad..62f83f70 100644 --- a/lib/providers/ui_providers.dart +++ b/lib/providers/ui_providers.dart @@ -23,3 +23,5 @@ final nameTextFieldFocusNodeProvider = }); return focusNode; }); + +final searchQueryProvider = StateProvider((ref) => ''); diff --git a/lib/screens/home_page/collection_pane.dart b/lib/screens/home_page/collection_pane.dart index fd0b518a..0ede2021 100644 --- a/lib/screens/home_page/collection_pane.dart +++ b/lib/screens/home_page/collection_pane.dart @@ -26,7 +26,7 @@ class CollectionPane extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( - padding: kPr8CollectionPane, + padding: kPe8, child: Wrap( alignment: WrapAlignment.spaceBetween, children: [ @@ -69,7 +69,39 @@ class CollectionPane extends ConsumerWidget { ], ), ), - kVSpacer8, + kVSpacer10, + Container( + height: 30, + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + borderRadius: kBorderRadius8, + border: Border.all( + color: Theme.of(context).colorScheme.surfaceVariant, + ), + ), + child: Row( + children: [ + kHSpacer5, + Icon( + Icons.filter_alt, + size: 18, + color: Theme.of(context).colorScheme.secondary, + ), + kHSpacer5, + Expanded( + child: RawTextField( + style: Theme.of(context).textTheme.bodyMedium, + hintText: "Filter by name or URL", + onChanged: (value) { + ref.read(searchQueryProvider.notifier).state = + value.toLowerCase(); + }, + ), + ), + ], + ), + ), + kVSpacer10, const Expanded( child: RequestList(), ), @@ -109,41 +141,61 @@ class _RequestListState extends ConsumerState { final requestItems = ref.watch(collectionStateNotifierProvider)!; final alwaysShowCollectionPaneScrollbar = ref.watch(settingsProvider .select((value) => value.alwaysShowCollectionPaneScrollbar)); + final filterQuery = ref.watch(searchQueryProvider).trim(); return Scrollbar( controller: controller, thumbVisibility: alwaysShowCollectionPaneScrollbar ? true : null, radius: const Radius.circular(12), - child: ReorderableListView.builder( - padding: kPr8CollectionPane, - scrollController: controller, - buildDefaultDragHandles: false, - itemCount: requestSequence.length, - onReorder: (int oldIndex, int newIndex) { - if (oldIndex < newIndex) { - newIndex -= 1; - } - if (oldIndex != newIndex) { - ref - .read(collectionStateNotifierProvider.notifier) - .reorder(oldIndex, newIndex); - } - }, - itemBuilder: (context, index) { - var id = requestSequence[index]; - return ReorderableDragStartListener( - key: ValueKey(id), - index: index, - child: Padding( - padding: kP1, - child: RequestItem( - id: id, - requestModel: requestItems[id]!, - ), + child: filterQuery.isEmpty + ? ReorderableListView.builder( + padding: kPe8, + scrollController: controller, + buildDefaultDragHandles: false, + itemCount: requestSequence.length, + onReorder: (int oldIndex, int newIndex) { + if (oldIndex < newIndex) { + newIndex -= 1; + } + if (oldIndex != newIndex) { + ref + .read(collectionStateNotifierProvider.notifier) + .reorder(oldIndex, newIndex); + } + }, + itemBuilder: (context, index) { + var id = requestSequence[index]; + return ReorderableDragStartListener( + key: ValueKey(id), + index: index, + child: Padding( + padding: kP1, + child: RequestItem( + id: id, + requestModel: requestItems[id]!, + ), + ), + ); + }, + ) + : ListView( + padding: kPe8, + controller: controller, + children: requestSequence.map((id) { + var item = requestItems[id]!; + if (item.url.toLowerCase().contains(filterQuery) || + item.name.toLowerCase().contains(filterQuery)) { + return Padding( + padding: kP1, + child: RequestItem( + id: id, + requestModel: item, + ), + ); + } + return const SizedBox(); + }).toList(), ), - ); - }, - ), ); } } diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_form_data.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_form_data.dart index 5d518e22..a6bafd6e 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_form_data.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_form_data.dart @@ -17,48 +17,72 @@ class FormDataWidget extends ConsumerStatefulWidget { class _FormDataBodyState extends ConsumerState { late int seed; final random = Random.secure(); - late List rows; + late List formRows; + bool isAddingRow = false; + @override void initState() { super.initState(); seed = random.nextInt(kRandMax); } + void _onFieldChange(String selectedId) { + ref.read(collectionStateNotifierProvider.notifier).update( + selectedId, + requestFormDataList: formRows.sublist(0, formRows.length - 1), + ); + } + @override Widget build(BuildContext context) { final selectedId = ref.watch(selectedIdStateProvider); - var formRows = ref.read(selectedRequestModelProvider)?.requestFormDataList; - rows = - formRows == null || formRows.isEmpty ? [kFormDataEmptyModel] : formRows; + ref.watch(selectedRequestModelProvider + .select((value) => value?.requestFormDataList?.length)); + var rF = ref.read(selectedRequestModelProvider)?.requestFormDataList; + bool isFormDataEmpty = rF == null || rF.isEmpty; + List rows = (isFormDataEmpty) + ? [ + kFormDataEmptyModel, + ] + : rF; + formRows = isFormDataEmpty ? rows : rows + [kFormDataEmptyModel]; + isAddingRow = false; DaviModel daviModelRows = DaviModel( - rows: rows, + rows: formRows, columns: [ DaviColumn( - cellPadding: kpsV5, - name: 'Key', + cellPadding: kPv2, + name: kNameField, grow: 4, cellBuilder: (_, row) { int idx = row.index; + bool isLast = idx + 1 == formRows.length; return Theme( data: Theme.of(context), child: FormDataField( keyId: "$selectedId-$idx-form-v-$seed", - initialValue: rows[idx].name, - hintText: " Add Key", + initialValue: formRows[idx].name, + hintText: kHintAddFieldName, onChanged: (value) { - rows[idx] = rows[idx].copyWith( - name: value, - ); + formRows[idx] = formRows[idx].copyWith(name: value); + if (isLast && !isAddingRow) { + isAddingRow = true; + formRows.add(kFormDataEmptyModel); + } _onFieldChange(selectedId!); }, colorScheme: Theme.of(context).colorScheme, - formDataType: rows[idx].type, + formDataType: formRows[idx].type, onFormDataTypeChanged: (value) { - rows[idx] = rows[idx].copyWith( + bool hasChanged = formRows[idx].type != value; + formRows[idx] = formRows[idx].copyWith( type: value ?? FormDataType.text, ); - rows[idx] = rows[idx].copyWith(value: ""); + formRows[idx] = formRows[idx].copyWith(value: ""); + if (isLast && hasChanged) { + formRows.add(kFormDataEmptyModel); + } setState(() {}); _onFieldChange(selectedId!); }, @@ -69,7 +93,7 @@ class _FormDataBodyState extends ConsumerState { ), DaviColumn( width: 40, - cellPadding: kpsV5, + cellPadding: kPv2, cellAlignment: Alignment.center, cellBuilder: (_, row) { return Text( @@ -79,12 +103,13 @@ class _FormDataBodyState extends ConsumerState { }, ), DaviColumn( - name: 'Value', + name: kNameValue, grow: 4, - cellPadding: kpsV5, + cellPadding: kPv2, cellBuilder: (_, row) { int idx = row.index; - return rows[idx].type == FormDataType.file + bool isLast = idx + 1 == formRows.length; + return formRows[idx].type == FormDataType.file ? Align( alignment: Alignment.centerLeft, child: Row( @@ -109,7 +134,7 @@ class _FormDataBodyState extends ConsumerState { if (pickedResult != null && pickedResult.files.isNotEmpty && pickedResult.files.first.path != null) { - rows[idx] = rows[idx].copyWith( + formRows[idx] = formRows[idx].copyWith( value: pickedResult.files.first.path!, ); setState(() {}); @@ -117,10 +142,10 @@ class _FormDataBodyState extends ConsumerState { } }, label: Text( - (rows[idx].type == FormDataType.file && - rows[idx].value.isNotEmpty) - ? rows[idx].value.toString() - : "Select File", + (formRows[idx].type == FormDataType.file && + formRows[idx].value.isNotEmpty) + ? formRows[idx].value.toString() + : kLabelSelectFile, textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, style: kFormDataButtonLabelTextStyle, @@ -133,10 +158,14 @@ class _FormDataBodyState extends ConsumerState { ) : CellField( keyId: "$selectedId-$idx-form-v-$seed", - initialValue: rows[idx].value, - hintText: " Add Value", + initialValue: formRows[idx].value, + hintText: kHintAddValue, onChanged: (value) { - rows[idx] = rows[idx].copyWith(value: value); + formRows[idx] = formRows[idx].copyWith(value: value); + if (isLast && !isAddingRow) { + isAddingRow = true; + formRows.add(kFormDataEmptyModel); + } _onFieldChange(selectedId!); }, colorScheme: Theme.of(context).colorScheme, @@ -148,22 +177,24 @@ class _FormDataBodyState extends ConsumerState { pinStatus: PinStatus.none, width: 30, cellBuilder: (_, row) { + bool isLast = row.index + 1 == formRows.length; return InkWell( + onTap: isLast + ? null + : () { + seed = random.nextInt(kRandMax); + if (formRows.length == 2) { + setState(() { + formRows = [kFormDataEmptyModel]; + }); + } else { + formRows.removeAt(row.index); + } + _onFieldChange(selectedId!); + }, child: Theme.of(context).brightness == Brightness.dark ? kIconRemoveDark : kIconRemoveLight, - onTap: () { - seed = random.nextInt(kRandMax); - if (rows.length == 1) { - setState(() { - rows = [kFormDataEmptyModel]; - }); - } else { - rows.removeAt(row.index); - } - _onFieldChange(selectedId!); - setState(() {}); - }, ); }, ), @@ -185,23 +216,22 @@ class _FormDataBodyState extends ConsumerState { child: Davi(daviModelRows), ), ), + kVSpacer20, ], ), ), Align( alignment: Alignment.bottomCenter, child: Padding( - padding: const EdgeInsets.only(bottom: 30), + padding: const EdgeInsets.only(bottom: 5), child: ElevatedButton.icon( onPressed: () { - setState(() { - rows.add(kFormDataEmptyModel); - }); + formRows.add(kFormDataEmptyModel); _onFieldChange(selectedId!); }, icon: const Icon(Icons.add), label: const Text( - "Add Form Data", + kLabelAddFormField, style: kTextStyleButton, ), ), @@ -210,11 +240,4 @@ class _FormDataBodyState extends ConsumerState { ], ); } - - void _onFieldChange(String selectedId) { - ref.read(collectionStateNotifierProvider.notifier).update( - selectedId, - requestFormDataList: rows, - ); - } } diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_headers.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_headers.dart index 503355f8..ee5c574c 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_headers.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_headers.dart @@ -15,10 +15,11 @@ class EditRequestHeaders extends ConsumerStatefulWidget { } class EditRequestHeadersState extends ConsumerState { - final random = Random.secure(); - late List rows; - late List isRowEnabledList; late int seed; + final random = Random.secure(); + late List headerRows; + late List isRowEnabledList; + bool isAddingRow = false; @override void initState() { @@ -29,8 +30,9 @@ class EditRequestHeadersState extends ConsumerState { void _onFieldChange(String selectedId) { ref.read(collectionStateNotifierProvider.notifier).update( selectedId, - requestHeaders: rows, - isHeaderEnabledList: isRowEnabledList, + requestHeaders: headerRows.sublist(0, headerRows.length - 1), + isHeaderEnabledList: + isRowEnabledList.sublist(0, headerRows.length - 1), ); } @@ -40,48 +42,62 @@ class EditRequestHeadersState extends ConsumerState { ref.watch(selectedRequestModelProvider .select((value) => value?.requestHeaders?.length)); var rH = ref.read(selectedRequestModelProvider)?.requestHeaders; - rows = (rH == null || rH.isEmpty) + bool isHeadersEmpty = rH == null || rH.isEmpty; + List rows = (isHeadersEmpty) ? [ kNameValueEmptyModel, ] : rH; + headerRows = isHeadersEmpty ? rows : rows + [kNameValueEmptyModel]; isRowEnabledList = ref.read(selectedRequestModelProvider)?.isHeaderEnabledList ?? - List.filled(rows.length, true, growable: true); + List.filled(rH?.length ?? 0, true, growable: true); + isRowEnabledList.add(false); + isAddingRow = false; DaviModel model = DaviModel( - rows: rows, + rows: headerRows, columns: [ DaviColumn( - name: 'Checkbox', + name: kNameCheckbox, width: 30, cellBuilder: (_, row) { int idx = row.index; + bool isLast = idx + 1 == headerRows.length; return CheckBox( keyId: "$selectedId-$idx-headers-c-$seed", value: isRowEnabledList[idx], - onChanged: (value) { - setState(() { - isRowEnabledList[idx] = value!; - }); - _onFieldChange(selectedId!); - }, + onChanged: isLast + ? null + : (value) { + setState(() { + isRowEnabledList[idx] = value!; + }); + _onFieldChange(selectedId!); + }, colorScheme: Theme.of(context).colorScheme, ); }, ), DaviColumn( - name: 'Header Name', + name: kNameHeader, width: 70, grow: 1, cellBuilder: (_, row) { int idx = row.index; + bool isLast = idx + 1 == headerRows.length; return HeaderField( keyId: "$selectedId-$idx-headers-k-$seed", - initialValue: rows[idx].name, - hintText: "Add Header Name", + initialValue: headerRows[idx].name, + hintText: kHintAddName, onChanged: (value) { - rows[idx] = rows[idx].copyWith(name: value); + headerRows[idx] = headerRows[idx].copyWith(name: value); + if (isLast && !isAddingRow) { + isAddingRow = true; + isRowEnabledList[idx] = true; + headerRows.add(kNameValueEmptyModel); + isRowEnabledList.add(false); + } _onFieldChange(selectedId!); }, colorScheme: Theme.of(context).colorScheme, @@ -99,16 +115,23 @@ class EditRequestHeadersState extends ConsumerState { }, ), DaviColumn( - name: 'Header Value', + name: kNameValue, grow: 1, cellBuilder: (_, row) { int idx = row.index; + bool isLast = idx + 1 == headerRows.length; return CellField( keyId: "$selectedId-$idx-headers-v-$seed", - initialValue: rows[idx].value, - hintText: " Add Header Value", + initialValue: headerRows[idx].value, + hintText: kHintAddValue, onChanged: (value) { - rows[idx] = rows[idx].copyWith(value: value); + headerRows[idx] = headerRows[idx].copyWith(value: value); + if (isLast && !isAddingRow) { + isAddingRow = true; + isRowEnabledList[idx] = true; + headerRows.add(kNameValueEmptyModel); + isRowEnabledList.add(false); + } _onFieldChange(selectedId!); }, colorScheme: Theme.of(context).colorScheme, @@ -120,25 +143,28 @@ class EditRequestHeadersState extends ConsumerState { pinStatus: PinStatus.none, width: 30, cellBuilder: (_, row) { + bool isLast = row.index + 1 == headerRows.length; return InkWell( + onTap: isLast + ? null + : () { + seed = random.nextInt(kRandMax); + if (headerRows.length == 2) { + setState(() { + headerRows = [ + kNameValueEmptyModel, + ]; + isRowEnabledList = [false]; + }); + } else { + headerRows.removeAt(row.index); + isRowEnabledList.removeAt(row.index); + } + _onFieldChange(selectedId!); + }, child: Theme.of(context).brightness == Brightness.dark ? kIconRemoveDark : kIconRemoveLight, - onTap: () { - seed = random.nextInt(kRandMax); - if (rows.length == 1) { - setState(() { - rows = [ - kNameValueEmptyModel, - ]; - isRowEnabledList = [true]; - }); - } else { - rows.removeAt(row.index); - isRowEnabledList.removeAt(row.index); - } - _onFieldChange(selectedId!); - }, ); }, ), @@ -160,22 +186,23 @@ class EditRequestHeadersState extends ConsumerState { child: Davi(model), ), ), + kVSpacer40, ], ), ), Align( alignment: Alignment.bottomCenter, child: Padding( - padding: const EdgeInsets.only(bottom: 30), + padding: kPb15, child: ElevatedButton.icon( onPressed: () { - rows.add(kNameValueEmptyModel); - isRowEnabledList.add(true); + headerRows.add(kNameValueEmptyModel); + isRowEnabledList.add(false); _onFieldChange(selectedId!); }, icon: const Icon(Icons.add), label: const Text( - "Add Header", + kLabelAddHeader, style: kTextStyleButton, ), ), diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_params.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_params.dart index 6e3aca4b..47dca1a6 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_params.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_params.dart @@ -16,10 +16,11 @@ class EditRequestURLParams extends ConsumerStatefulWidget { } class EditRequestURLParamsState extends ConsumerState { - final random = Random.secure(); - late List rows; - late List isRowEnabledList; late int seed; + final random = Random.secure(); + late List paramRows; + late List isRowEnabledList; + bool isAddingRow = false; @override void initState() { @@ -30,8 +31,8 @@ class EditRequestURLParamsState extends ConsumerState { void _onFieldChange(String selectedId) { ref.read(collectionStateNotifierProvider.notifier).update( selectedId, - requestParams: rows, - isParamEnabledList: isRowEnabledList, + requestParams: paramRows.sublist(0, paramRows.length - 1), + isParamEnabledList: isRowEnabledList.sublist(0, paramRows.length - 1), ); } @@ -41,48 +42,62 @@ class EditRequestURLParamsState extends ConsumerState { ref.watch(selectedRequestModelProvider .select((value) => value?.requestParams?.length)); var rP = ref.read(selectedRequestModelProvider)?.requestParams; - rows = (rP == null || rP.isEmpty) + bool isParamsEmpty = rP == null || rP.isEmpty; + List rows = (isParamsEmpty) ? [ kNameValueEmptyModel, ] : rP; + paramRows = isParamsEmpty ? rows : rows + [kNameValueEmptyModel]; isRowEnabledList = ref.read(selectedRequestModelProvider)?.isParamEnabledList ?? - List.filled(rows.length, true, growable: true); + List.filled(rP?.length ?? 0, true, growable: true); + isRowEnabledList.add(false); + isAddingRow = false; DaviModel model = DaviModel( - rows: rows, + rows: paramRows, columns: [ DaviColumn( - name: 'Checkbox', + name: kNameCheckbox, width: 30, cellBuilder: (_, row) { int idx = row.index; + bool isLast = idx + 1 == paramRows.length; return CheckBox( keyId: "$selectedId-$idx-params-c-$seed", value: isRowEnabledList[idx], - onChanged: (value) { - setState(() { - isRowEnabledList[idx] = value!; - }); - _onFieldChange(selectedId!); - }, + onChanged: isLast + ? null + : (value) { + setState(() { + isRowEnabledList[idx] = value!; + }); + _onFieldChange(selectedId!); + }, colorScheme: Theme.of(context).colorScheme, ); }, ), DaviColumn( - name: 'URL Parameter', + name: kNameURLParam, width: 70, grow: 1, cellBuilder: (_, row) { int idx = row.index; + bool isLast = idx + 1 == paramRows.length; return CellField( keyId: "$selectedId-$idx-params-k-$seed", - initialValue: rows[idx].name, - hintText: "Add URL Parameter", + initialValue: paramRows[idx].name, + hintText: kHintAddURLParam, onChanged: (value) { - rows[idx] = rows[idx].copyWith(name: value); + paramRows[idx] = paramRows[idx].copyWith(name: value); + if (isLast && !isAddingRow) { + isAddingRow = true; + isRowEnabledList[idx] = true; + paramRows.add(kNameValueEmptyModel); + isRowEnabledList.add(false); + } _onFieldChange(selectedId!); }, colorScheme: Theme.of(context).colorScheme, @@ -100,16 +115,23 @@ class EditRequestURLParamsState extends ConsumerState { }, ), DaviColumn( - name: 'Value', + name: kNameValue, grow: 1, cellBuilder: (_, row) { int idx = row.index; + bool isLast = idx + 1 == paramRows.length; return CellField( keyId: "$selectedId-$idx-params-v-$seed", - initialValue: rows[idx].value, - hintText: "Add Value", + initialValue: paramRows[idx].value, + hintText: kHintAddValue, onChanged: (value) { - rows[idx] = rows[idx].copyWith(value: value); + paramRows[idx] = paramRows[idx].copyWith(value: value); + if (isLast && !isAddingRow) { + isAddingRow = true; + isRowEnabledList[idx] = true; + paramRows.add(kNameValueEmptyModel); + isRowEnabledList.add(false); + } _onFieldChange(selectedId!); }, colorScheme: Theme.of(context).colorScheme, @@ -121,25 +143,28 @@ class EditRequestURLParamsState extends ConsumerState { pinStatus: PinStatus.none, width: 30, cellBuilder: (_, row) { + bool isLast = row.index + 1 == paramRows.length; return InkWell( + onTap: isLast + ? null + : () { + seed = random.nextInt(kRandMax); + if (paramRows.length == 2) { + setState(() { + paramRows = [ + kNameValueEmptyModel, + ]; + isRowEnabledList = [false]; + }); + } else { + paramRows.removeAt(row.index); + isRowEnabledList.removeAt(row.index); + } + _onFieldChange(selectedId!); + }, child: Theme.of(context).brightness == Brightness.dark ? kIconRemoveDark : kIconRemoveLight, - onTap: () { - seed = random.nextInt(kRandMax); - if (rows.length == 1) { - setState(() { - rows = [ - kNameValueEmptyModel, - ]; - isRowEnabledList = [true]; - }); - } else { - rows.removeAt(row.index); - isRowEnabledList.removeAt(row.index); - } - _onFieldChange(selectedId!); - }, ); }, ), @@ -161,22 +186,23 @@ class EditRequestURLParamsState extends ConsumerState { child: Davi(model), ), ), + kVSpacer40, ], ), ), Align( alignment: Alignment.bottomCenter, child: Padding( - padding: const EdgeInsets.only(bottom: 30), + padding: kPb15, child: ElevatedButton.icon( onPressed: () { - rows.add(kNameValueEmptyModel); - isRowEnabledList.add(true); + paramRows.add(kNameValueEmptyModel); + isRowEnabledList.add(false); _onFieldChange(selectedId!); }, icon: const Icon(Icons.add), label: const Text( - "Add Param", + kLabelAddParam, style: kTextStyleButton, ), ), diff --git a/lib/screens/home_page/editor_pane/details_card/response_pane.dart b/lib/screens/home_page/editor_pane/details_card/response_pane.dart index 4a240932..17a03ea7 100644 --- a/lib/screens/home_page/editor_pane/details_card/response_pane.dart +++ b/lib/screens/home_page/editor_pane/details_card/response_pane.dart @@ -39,7 +39,6 @@ class ResponseDetails extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - var sm = ScaffoldMessenger.of(context); final responseStatus = ref.watch( selectedRequestModelProvider.select((value) => value?.responseStatus)); final message = ref @@ -57,8 +56,6 @@ class ResponseDetails extends ConsumerWidget { ref .read(collectionStateNotifierProvider.notifier) .clearResponse(selectedRequest?.id); - sm.hideCurrentSnackBar(); - sm.showSnackBar(getSnackBar('Response cleared')); }, ), const Expanded( diff --git a/lib/widgets/buttons.dart b/lib/widgets/buttons.dart index f4280380..41bcddb9 100644 --- a/lib/widgets/buttons.dart +++ b/lib/widgets/buttons.dart @@ -246,15 +246,12 @@ class ClearResponseButton extends StatelessWidget { @override Widget build(BuildContext context) { - return Tooltip( - message: 'Clear response', - child: TextButton( - style: TextButton.styleFrom(minimumSize: const Size(40, 40)), - onPressed: onPressed, - child: const Icon( - Icons.delete, - size: 20, - ), + return IconButton( + tooltip: kTooltipClearResponse, + onPressed: onPressed, + icon: const Icon( + Icons.delete, + size: 16, ), ); } diff --git a/lib/widgets/checkbox.dart b/lib/widgets/checkbox.dart index 22c3869f..53adf496 100644 --- a/lib/widgets/checkbox.dart +++ b/lib/widgets/checkbox.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; class CheckBox extends StatelessWidget { final String keyId; final bool value; - final ValueChanged onChanged; + final ValueChanged? onChanged; final ColorScheme? colorScheme; const CheckBox({ super.key, diff --git a/lib/widgets/dropdowns.dart b/lib/widgets/dropdowns.dart index 69008a6b..1be902e9 100644 --- a/lib/widgets/dropdowns.dart +++ b/lib/widgets/dropdowns.dart @@ -157,6 +157,7 @@ class DropdownButtonCodegenLanguage extends StatelessWidget { Widget build(BuildContext context) { final surfaceColor = Theme.of(context).colorScheme.surface; return DropdownButton( + isExpanded: true, focusColor: surfaceColor, value: codegenLanguage, icon: const Icon( @@ -181,6 +182,8 @@ class DropdownButtonCodegenLanguage extends StatelessWidget { child: Text( value.label, style: kTextStyleButton, + overflow: TextOverflow.ellipsis, + maxLines: 1, ), ), ); diff --git a/lib/widgets/request_widgets.dart b/lib/widgets/request_widgets.dart index ffac504e..7375a3b4 100644 --- a/lib/widgets/request_widgets.dart +++ b/lib/widgets/request_widgets.dart @@ -48,16 +48,12 @@ class _RequestPaneState extends State return Column( children: [ Padding( - padding: kPh20v10, + padding: kP8, child: SizedBox( height: kHeaderHeight, child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.end, children: [ - Text( - "Request", - style: Theme.of(context).textTheme.titleMedium, - ), FilledButton.tonalIcon( onPressed: widget.onPressedCodeButton, icon: Icon( @@ -68,7 +64,8 @@ class _RequestPaneState extends State label: SizedBox( width: 75, child: Text( - widget.codePaneVisible ? "Hide Code" : "View Code"), + widget.codePaneVisible ? kLabelHideCode : kLabelViewCode, + ), ), ), ], @@ -79,18 +76,19 @@ class _RequestPaneState extends State key: Key(widget.selectedId!), controller: _controller, overlayColor: kColorTransparentState, + labelPadding: kPh2, onTap: widget.onTapTabBar, tabs: [ TabLabel( - text: 'URL Params', + text: kLabelURLParams, showIndicator: widget.showIndicators[0], ), TabLabel( - text: 'Headers', + text: kLabelHeaders, showIndicator: widget.showIndicators[1], ), TabLabel( - text: 'Body', + text: kLabelBody, showIndicator: widget.showIndicators[2], ), ], diff --git a/lib/widgets/response_widgets.dart b/lib/widgets/response_widgets.dart index a2f2183a..44117d4e 100644 --- a/lib/widgets/response_widgets.dart +++ b/lib/widgets/response_widgets.dart @@ -25,11 +25,9 @@ class NotSentWidget extends StatelessWidget { color: color, ), Text( - 'Not Sent', - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith(color: color), + kLabelNotSent, + style: + Theme.of(context).textTheme.titleMedium?.copyWith(color: color), ), ], ), @@ -123,42 +121,19 @@ class ResponsePaneHeader extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: kPh20v10, + padding: kPv8, child: SizedBox( height: kHeaderHeight, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text.rich( - TextSpan( - children: [ - const TextSpan( - text: "Response (", - ), - TextSpan( - text: "$responseStatus", - style: TextStyle( - color: getResponseStatusCodeColor( - responseStatus, - brightness: Theme.of(context).brightness, - ), - fontFamily: kCodeStyle.fontFamily, - ), - ), - const TextSpan( - text: ")", - ), - ], - style: Theme.of(context).textTheme.titleMedium, - ), - ), - kHSpacer20, + kHSpacer10, Expanded( child: Text( - message ?? "", + "$responseStatus: ${message ?? '-'}", softWrap: false, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleMedium!.copyWith( + style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontFamily: kCodeStyle.fontFamily, color: getResponseStatusCodeColor( responseStatus, @@ -167,10 +142,10 @@ class ResponsePaneHeader extends StatelessWidget { ), ), ), - kHSpacer20, + kHSpacer10, Text( humanizeDuration(time), - style: Theme.of(context).textTheme.titleMedium!.copyWith( + style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontFamily: kCodeStyle.fontFamily, color: Theme.of(context).colorScheme.secondary, ), @@ -220,31 +195,15 @@ class _ResponseTabViewState extends State TabBar( key: Key(widget.selectedId!), controller: _controller, + labelPadding: kPh2, overlayColor: kColorTransparentState, onTap: (index) {}, tabs: const [ - SizedBox( - height: kTabHeight, - child: Center( - child: Text( - 'Body', - textAlign: TextAlign.center, - overflow: TextOverflow.fade, - softWrap: false, - style: kTextStyleButton, - ), - ), + TabLabel( + text: kLabelResponseBody, ), - SizedBox( - height: kTabHeight, - child: Center( - child: Text( - 'Headers', - textAlign: TextAlign.center, - overflow: TextOverflow.fade, - style: kTextStyleButton, - ), - ), + TabLabel( + text: kLabelHeaders, ), ], ), @@ -284,8 +243,8 @@ class ResponseHeadersHeader extends StatelessWidget { children: [ Expanded( child: Text( - "$name (${map.length} items)", - style: Theme.of(context).textTheme.labelLarge!.copyWith( + "$name (${map.length} $kLabelItems)", + style: Theme.of(context).textTheme.labelMedium?.copyWith( fontWeight: FontWeight.bold, ), ), @@ -300,8 +259,6 @@ class ResponseHeadersHeader extends StatelessWidget { } } -const kHeaderRow = ["Header Name", "Header Value"]; - class ResponseHeaders extends StatelessWidget { const ResponseHeaders({ super.key, @@ -320,7 +277,7 @@ class ResponseHeaders extends StatelessWidget { children: [ ResponseHeadersHeader( map: responseHeaders, - name: "Response Headers", + name: kLabelResponseHeaders, ), if (responseHeaders.isNotEmpty) kVSpacer5, if (responseHeaders.isNotEmpty) @@ -332,7 +289,7 @@ class ResponseHeaders extends StatelessWidget { kVSpacer10, ResponseHeadersHeader( map: requestHeaders, - name: "Request Headers", + name: kLabelRequestHeaders, ), if (requestHeaders.isNotEmpty) kVSpacer5, if (requestHeaders.isNotEmpty) @@ -359,20 +316,18 @@ class ResponseBody extends StatelessWidget { Widget build(BuildContext context) { final responseModel = selectedRequestModel?.responseModel; if (responseModel == null) { - return const ErrorMessage( - message: - 'Error: Response data does not exist. $kUnexpectedRaiseIssue'); + return const ErrorMessage(message: '$kMsgError $kUnexpectedRaiseIssue'); } var body = responseModel.body; var formattedBody = responseModel.formattedBody; if (body == null) { return const ErrorMessage( - message: 'Response body is missing (null). $kUnexpectedRaiseIssue'); + message: '$kMsgNullBody $kUnexpectedRaiseIssue'); } if (body.isEmpty) { return const ErrorMessage( - message: 'No content', + message: kMsgNoContent, showIcon: false, showIssueButton: false, ); @@ -382,7 +337,7 @@ class ResponseBody extends StatelessWidget { if (mediaType == null) { return ErrorMessage( message: - 'Unknown Response Content-Type - ${responseModel.contentType}. $kUnexpectedRaiseIssue'); + '$kMsgUnknowContentType - ${responseModel.contentType}. $kUnexpectedRaiseIssue'); } var responseBodyView = getResponseBodyViewOptions(mediaType); diff --git a/lib/widgets/tabs.dart b/lib/widgets/tabs.dart index eabf36c8..c2542a26 100644 --- a/lib/widgets/tabs.dart +++ b/lib/widgets/tabs.dart @@ -2,7 +2,11 @@ import 'package:flutter/material.dart'; import 'package:apidash/consts.dart'; class TabLabel extends StatelessWidget { - const TabLabel({super.key, required this.text, this.showIndicator = false}); + const TabLabel({ + super.key, + required this.text, + this.showIndicator = false, + }); final String text; final bool showIndicator; @@ -18,14 +22,14 @@ class TabLabel extends StatelessWidget { textAlign: TextAlign.center, overflow: TextOverflow.fade, softWrap: false, - style: kTextStyleButton, + style: kTextStyleTab, ), ), if (showIndicator) const Align( alignment: Alignment.topCenter, child: Padding( - padding: EdgeInsets.only(top: 6), + padding: EdgeInsets.only(top: 1), child: Icon( Icons.circle, size: 6, diff --git a/lib/widgets/textfields.dart b/lib/widgets/textfields.dart index da68f9e2..35d3437f 100644 --- a/lib/widgets/textfields.dart +++ b/lib/widgets/textfields.dart @@ -94,14 +94,39 @@ class JsonSearchField extends StatelessWidget { @override Widget build(BuildContext context) { - return TextField( + return RawTextField( controller: controller, onChanged: onChanged, style: kCodeStyle, - decoration: const InputDecoration( + hintText: 'Search..', + ); + } +} + +class RawTextField extends StatelessWidget { + const RawTextField({ + super.key, + this.onChanged, + this.controller, + this.hintText, + this.style, + }); + + final void Function(String)? onChanged; + final TextEditingController? controller; + final String? hintText; + final TextStyle? style; + + @override + Widget build(BuildContext context) { + return TextField( + controller: controller, + onChanged: onChanged, + style: style, + decoration: InputDecoration( isDense: true, border: InputBorder.none, - hintText: 'Search..', + hintText: hintText, ), ); } diff --git a/pubspec.yaml b/pubspec.yaml index 6f78ae1e..b7c79bde 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: multi_split_view: ^2.4.0 url_launcher: ^6.2.5 flutter_riverpod: ^2.5.1 - riverpod: ^2.5.1 + riverpod: ^2.5.1 uuid: ^4.3.3 davi: ^3.4.1 http: ^1.2.1 diff --git a/test/codegen/rust_curl_codegen_test.dart b/test/codegen/rust_curl_codegen_test.dart new file mode 100644 index 00000000..badc18ba --- /dev/null +++ b/test/codegen/rust_curl_codegen_test.dart @@ -0,0 +1,992 @@ +import 'package:apidash/codegen/codegen.dart'; +import 'package:apidash/consts.dart'; +import 'package:test/test.dart'; +import '../request_models.dart'; + +void main() { + final codeGen = Codegen(); + + group('GET Request', () { + test('GET1', () { + const expectedCode = r""" +use curl::easy::Easy; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://api.apidash.dev").unwrap(); + easy.get(true).unwrap(); + + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelGet1, "https"), + expectedCode); + }); + test('GET2', () { + const expectedCode = r""" +use curl::easy::Easy; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://api.apidash.dev/country/data?code=US").unwrap(); + easy.get(true).unwrap(); + + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelGet2, "https"), + expectedCode); + }); + test('GET3', () { + const expectedCode = r""" +use curl::easy::Easy; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://api.apidash.dev/country/data?code=IND").unwrap(); + easy.get(true).unwrap(); + + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelGet3, "https"), + expectedCode); + }); + test('GET4', () { + const expectedCode = r""" +use curl::easy::Easy; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://api.apidash.dev/humanize/social?num=8700000&digits=3&system=SS&add_space=true&trailing_zeros=true").unwrap(); + easy.get(true).unwrap(); + + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelGet4, "https"), + expectedCode); + }); + test('GET5', () { + const expectedCode = r""" +use curl::easy::Easy; +use curl::easy::List; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://api.github.com/repos/foss42/apidash").unwrap(); + easy.get(true).unwrap(); + + let mut list = List::new(); + list.append("User-Agent: Test Agent").unwrap(); + easy.http_headers(list).unwrap(); + + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelGet5, "https"), + expectedCode); + }); + test('GET6', () { + const expectedCode = r""" +use curl::easy::Easy; +use curl::easy::List; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://api.github.com/repos/foss42/apidash?raw=true").unwrap(); + easy.get(true).unwrap(); + + let mut list = List::new(); + list.append("User-Agent: Test Agent").unwrap(); + easy.http_headers(list).unwrap(); + + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelGet6, "https"), + expectedCode); + }); + test('GET7', () { + const expectedCode = r""" +use curl::easy::Easy; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://api.apidash.dev").unwrap(); + easy.get(true).unwrap(); + + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelGet7, "https"), + expectedCode); + }); + test('GET8', () { + const expectedCode = r""" +use curl::easy::Easy; +use curl::easy::List; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://api.github.com/repos/foss42/apidash?raw=true").unwrap(); + easy.get(true).unwrap(); + + let mut list = List::new(); + list.append("User-Agent: Test Agent").unwrap(); + easy.http_headers(list).unwrap(); + + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelGet8, "https"), + expectedCode); + }); + test('GET9', () { + const expectedCode = r""" +use curl::easy::Easy; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://api.apidash.dev/humanize/social?num=8700000&add_space=true").unwrap(); + easy.get(true).unwrap(); + + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelGet9, "https"), + expectedCode); + }); + test('GET10', () { + const expectedCode = r""" +use curl::easy::Easy; +use curl::easy::List; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://api.apidash.dev/humanize/social").unwrap(); + easy.get(true).unwrap(); + + let mut list = List::new(); + list.append("User-Agent: Test Agent").unwrap(); + easy.http_headers(list).unwrap(); + + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelGet10, "https"), + expectedCode); + }); + test('GET11', () { + const expectedCode = r""" +use curl::easy::Easy; +use curl::easy::List; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://api.apidash.dev/humanize/social?num=8700000&digits=3").unwrap(); + easy.get(true).unwrap(); + + let mut list = List::new(); + list.append("User-Agent: Test Agent").unwrap(); + easy.http_headers(list).unwrap(); + + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelGet11, "https"), + expectedCode); + }); + test('GET12', () { + const expectedCode = r""" +use curl::easy::Easy; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://api.apidash.dev/humanize/social").unwrap(); + easy.get(true).unwrap(); + + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelGet12, "https"), + expectedCode); + }); + }); + + group('HEAD Request', () { + test('HEAD1', () { + const expectedCode = r""" +use curl::easy::Easy; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://api.apidash.dev").unwrap(); + easy.nobody(true).unwrap(); + + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelHead1, "https"), + expectedCode); + }); + test('HEAD2', () { + const expectedCode = r""" +use curl::easy::Easy; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://api.apidash.dev").unwrap(); + easy.nobody(true).unwrap(); + + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelHead2, "https"), + expectedCode); + }); + }); + + group('POST Request', () { + test('POST1', () { + const expectedCode = r""" +use curl::easy::Easy; +use curl::easy::List; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://api.apidash.dev/case/lower").unwrap(); + easy.post(true).unwrap(); + + easy.post_fields_copy(r#"{ +"text": "I LOVE Flutter" +}"#.as_bytes()).unwrap(); + + let mut list = List::new(); + list.append("Content-Type: text/plain").unwrap(); + easy.http_headers(list).unwrap(); + + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelPost1, "https"), + expectedCode); + }); + test('POST2', () { + const expectedCode = r""" +use curl::easy::Easy; +use serde_json::json; +use curl::easy::List; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://api.apidash.dev/case/lower").unwrap(); + easy.post(true).unwrap(); + + easy.post_fields_copy(json!({ +"text": "I LOVE Flutter", +"flag": null, +"male": true, +"female": false, +"no": 1.2, +"arr": ["null", "true", "false", null] +}).to_string().as_bytes()).unwrap(); + + let mut list = List::new(); + list.append("Content-Type: application/json").unwrap(); + easy.http_headers(list).unwrap(); + + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelPost2, "https"), + expectedCode); + }); + test('POST3', () { + const expectedCode = r""" +use curl::easy::Easy; +use serde_json::json; +use curl::easy::List; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://api.apidash.dev/case/lower").unwrap(); + easy.post(true).unwrap(); + + easy.post_fields_copy(json!({ +"text": "I LOVE Flutter" +}).to_string().as_bytes()).unwrap(); + + let mut list = List::new(); + list.append("User-Agent: Test Agent").unwrap(); + list.append("Content-Type: application/json").unwrap(); + easy.http_headers(list).unwrap(); + + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelPost3, "https"), + expectedCode); + }); + test('POST4', () { + const expectedCode = r""" +use curl::easy::Easy; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://api.apidash.dev/io/form").unwrap(); + easy.post(true).unwrap(); + + let mut form = curl::easy::Form::new(); + + form.part("text") + .contents(b"API") + .add().unwrap(); + + form.part("sep") + .contents(b"|") + .add().unwrap(); + + form.part("times") + .contents(b"3") + .add().unwrap(); + + easy.httppost(form).unwrap(); + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelPost4, "https"), + expectedCode); + }); + test('POST5', () { + const expectedCode = r""" +use curl::easy::Easy; +use curl::easy::List; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://api.apidash.dev/io/form").unwrap(); + easy.post(true).unwrap(); + + let mut form = curl::easy::Form::new(); + + form.part("text") + .contents(b"API") + .add().unwrap(); + + form.part("sep") + .contents(b"|") + .add().unwrap(); + + form.part("times") + .contents(b"3") + .add().unwrap(); + + easy.httppost(form).unwrap(); + let mut list = List::new(); + list.append("User-Agent: Test Agent").unwrap(); + easy.http_headers(list).unwrap(); + + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelPost5, "https"), + expectedCode); + }); + test('POST6', () { + const expectedCode = r""" +use curl::easy::Easy; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://api.apidash.dev/io/img").unwrap(); + easy.post(true).unwrap(); + + let mut form = curl::easy::Form::new(); + + form.part("token") + .contents(b"xyz") + .add().unwrap(); + + form.part("imfile") + .file("/Documents/up/1.png") + .add().unwrap(); + + easy.httppost(form).unwrap(); + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelPost6, "https"), + expectedCode); + }); + test('POST7', () { + const expectedCode = r""" +use curl::easy::Easy; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://api.apidash.dev/io/img").unwrap(); + easy.post(true).unwrap(); + + let mut form = curl::easy::Form::new(); + + form.part("token") + .contents(b"xyz") + .add().unwrap(); + + form.part("imfile") + .file("/Documents/up/1.png") + .add().unwrap(); + + easy.httppost(form).unwrap(); + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelPost7, "https"), + expectedCode); + }); + test('POST8', () { + const expectedCode = r""" +use curl::easy::Easy; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://api.apidash.dev/io/form?size=2&len=3").unwrap(); + easy.post(true).unwrap(); + + let mut form = curl::easy::Form::new(); + + form.part("text") + .contents(b"API") + .add().unwrap(); + + form.part("sep") + .contents(b"|") + .add().unwrap(); + + form.part("times") + .contents(b"3") + .add().unwrap(); + + easy.httppost(form).unwrap(); + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelPost8, "https"), + expectedCode); + }); + test('POST9', () { + const expectedCode = r""" +use curl::easy::Easy; +use curl::easy::List; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://api.apidash.dev/io/img?size=2&len=3").unwrap(); + easy.post(true).unwrap(); + + let mut form = curl::easy::Form::new(); + + form.part("token") + .contents(b"xyz") + .add().unwrap(); + + form.part("imfile") + .file("/Documents/up/1.png") + .add().unwrap(); + + easy.httppost(form).unwrap(); + let mut list = List::new(); + list.append("User-Agent: Test Agent").unwrap(); + list.append("Keep-Alive: true").unwrap(); + easy.http_headers(list).unwrap(); + + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelPost9, "https"), + expectedCode); + }); + }); + + group('PUT Request', () { + test('PUT1', () { + const expectedCode = r""" +use curl::easy::Easy; +use serde_json::json; +use curl::easy::List; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://reqres.in/api/users/2").unwrap(); + easy.put(true).unwrap(); + + easy.post_fields_copy(json!({ +"name": "morpheus", +"job": "zion resident" +}).to_string().as_bytes()).unwrap(); + + let mut list = List::new(); + list.append("Content-Type: application/json").unwrap(); + easy.http_headers(list).unwrap(); + + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelPut1, "https"), + expectedCode); + }); + }); + + group('PATCH Request', () { + test('PATCH1', () { + const expectedCode = r""" +use curl::easy::Easy; +use serde_json::json; +use curl::easy::List; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://reqres.in/api/users/2").unwrap(); + easy.custom_request("PATCH").unwrap(); + + easy.post_fields_copy(json!({ +"name": "marfeus", +"job": "accountant" +}).to_string().as_bytes()).unwrap(); + + let mut list = List::new(); + list.append("Content-Type: application/json").unwrap(); + easy.http_headers(list).unwrap(); + + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelPatch1, "https"), + expectedCode); + }); + }); + + group('DELETE Request', () { + test('DELETE1', () { + const expectedCode = r""" +use curl::easy::Easy; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://reqres.in/api/users/2").unwrap(); + easy.custom_request("DELETE").unwrap(); + + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelDelete1, "https"), + expectedCode); + }); + test('DELETE2', () { + const expectedCode = r""" +use curl::easy::Easy; +use serde_json::json; +use curl::easy::List; + +fn main() { + let mut easy = Easy::new(); + let mut data = Vec::new(); + easy.url("https://reqres.in/api/users/2").unwrap(); + easy.custom_request("DELETE").unwrap(); + + easy.post_fields_copy(json!({ +"name": "marfeus", +"job": "accountant" +}).to_string().as_bytes()).unwrap(); + + let mut list = List::new(); + list.append("Content-Type: application/json").unwrap(); + easy.http_headers(list).unwrap(); + + { + let mut transfer = easy.transfer(); + transfer.write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }).unwrap(); + transfer.perform().unwrap(); + } + + let response_body = String::from_utf8_lossy(&data); + + println!("Response body: {}", response_body); + println!("Response code: {}", easy.response_code().unwrap()); +}"""; + expect( + codeGen.getCode( + CodegenLanguage.rustCurl, requestModelDelete2, "https"), + expectedCode); + }); + }); + +} diff --git a/test/providers/ui_providers_test.dart b/test/providers/ui_providers_test.dart new file mode 100644 index 00000000..9d34619d --- /dev/null +++ b/test/providers/ui_providers_test.dart @@ -0,0 +1,339 @@ +import 'dart:io'; + +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/screens/dashboard.dart'; +import 'package:apidash/screens/home_page/collection_pane.dart'; +import 'package:apidash/screens/home_page/home_page.dart'; +import 'package:apidash/screens/intro_page.dart'; +import 'package:apidash/screens/settings_page.dart'; +import 'package:apidash/services/hive_services.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() async { + const MethodChannel channel = + MethodChannel('plugins.flutter.io/path_provider'); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + if (methodCall.method == 'getApplicationDocumentsDirectory') { + // Create a mock app doc directory for testing + Directory tempDir = + await Directory.systemTemp.createTemp('mock_app_doc_dir'); + return tempDir.path; // Return the path to the mock directory + } + return null; + }); + await openBoxes(); + }); + + group('Testing navRailIndexStateProvider', () { + testWidgets('Dashboard should display correct initial page', + (tester) async { + await tester.pumpWidget( + const ProviderScope( + child: MaterialApp( + home: Dashboard(), + ), + ), + ); + + // Verify that the HomePage is displayed initially + expect(find.byType(HomePage), findsOneWidget); + expect(find.byType(IntroPage), findsNothing); + expect(find.byType(SettingsPage), findsNothing); + }); + + testWidgets( + "Dashboard should display IntroPage when navRailIndexStateProvider is 1", + (WidgetTester tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + navRailIndexStateProvider.overrideWith((ref) => 1), + ], + child: const MaterialApp( + home: Dashboard(), + ), + ), + ); + + // Verify that the IntroPage is displayed + expect(find.byType(IntroPage), findsOneWidget); + expect(find.byType(HomePage), findsNothing); + expect(find.byType(SettingsPage), findsNothing); + }); + + testWidgets( + "Dashboard should display SettingsPage when navRailIndexStateProvider is 2", + (WidgetTester tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + navRailIndexStateProvider.overrideWith((ref) => 2), + ], + child: const MaterialApp( + home: Dashboard(), + ), + ), + ); + + // Verify that the SettingsPage is displayed + expect(find.byType(SettingsPage), findsOneWidget); + expect(find.byType(IntroPage), findsNothing); + expect(find.byType(HomePage), findsNothing); + }); + + testWidgets( + 'navRailIndexStateProvider should update when icon button is pressed', + (tester) async { + await tester.pumpWidget( + const ProviderScope( + child: MaterialApp( + home: Dashboard(), + ), + ), + ); + + // Tap on the Intro icon + await tester.tap(find.byIcon(Icons.help_outline)); + await tester.pump(); + + // Verify that the navRailIndexStateProvider is updated + final dashboard = tester.element(find.byType(Dashboard)); + final container = ProviderScope.containerOf(dashboard); + expect(container.read(navRailIndexStateProvider), 1); + }); + + testWidgets( + 'navRailIndexStateProvider should persist across widget rebuilds', + (tester) async { + // Pump the initial widget tree + await tester.pumpWidget( + const ProviderScope( + child: MaterialApp( + home: Dashboard(), + ), + ), + ); + + // Tap on the Settings icon to change the index to 2 + await tester.tap(find.byIcon(Icons.settings_outlined)); + await tester.pump(); + + // Rebuild the widget tree with the same ProviderScope + await tester.pumpWidget( + const ProviderScope( + child: MaterialApp( + home: Dashboard(), + ), + ), + ); + + // Verify that the navRailIndexStateProvider still has the updated value + final dashboard = tester.element(find.byType(Dashboard)); + final container = ProviderScope.containerOf(dashboard); + expect(container.read(navRailIndexStateProvider), 2); + + // Verify that the SettingsPage is still displayed after the rebuild + expect(find.byType(SettingsPage), findsOneWidget); + expect(find.byType(IntroPage), findsNothing); + expect(find.byType(HomePage), findsNothing); + }); + + testWidgets( + 'UI should update correctly when navRailIndexStateProvider changes', + (tester) async { + await tester.pumpWidget( + const ProviderScope( + child: MaterialApp( + home: Dashboard(), + ), + ), + ); + + // Grab the Dashboard widget and its ProviderContainer + final dashboard = tester.element(find.byType(Dashboard)); + final container = ProviderScope.containerOf(dashboard); + + // Go to IntroPage + container.read(navRailIndexStateProvider.notifier).state = 1; + await tester.pump(); + + // Verify that the IntroPage is displayed + expect(find.byType(IntroPage), findsOneWidget); + // Verify that the selected icon is the filled version (selectedIcon) + expect(find.byIcon(Icons.help), findsOneWidget); + + // Go to SettingsPage + container.read(navRailIndexStateProvider.notifier).state = 2; + await tester.pump(); + + // Verify that the SettingsPage is displayed + expect(find.byType(SettingsPage), findsOneWidget); + // Verify that the selected icon is the filled version (selectedIcon) + expect(find.byIcon(Icons.settings), findsOneWidget); + }); + + testWidgets( + 'navRailIndexStateProvider should be disposed when Dashboard is removed', + (tester) async { + await tester.pumpWidget( + const ProviderScope( + child: MaterialApp( + home: Dashboard(), + ), + ), + ); + + // Grab the Dashboard widget and its ProviderContainer + final dashboard = tester.element(find.byType(Dashboard)); + final container = ProviderScope.containerOf(dashboard); + + // Pumping a different widget to remove the Dashboard from the widget tree + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: Text('Different Widget')), + ), + ); + + // Verify that the ProviderContainer has been disposed + // by trying to read from disposed container + bool isDisposed = false; + try { + container.read(navRailIndexStateProvider); + } catch (e) { + isDisposed = true; + } + expect(isDisposed, true); + }); + }); + + group("Testing selectedIdEditStateProvider", () { + testWidgets('It should have an initial value of null', (tester) async { + await tester.pumpWidget( + const ProviderScope( + child: MaterialApp( + home: Scaffold( + body: CollectionPane(), + ), + ), + ), + ); + + // Verify that the initial value is null + final collectionPane = tester.element(find.byType(CollectionPane)); + final container = ProviderScope.containerOf(collectionPane); + expect(container.read(selectedIdEditStateProvider), null); + }); + + testWidgets( + 'selectedIdEditStateProvider should not be null after rename button has been tapped', + (tester) async { + await tester.pumpWidget( + const ProviderScope( + child: MaterialApp( + home: Scaffold( + body: CollectionPane(), + ), + ), + ), + ); + + // Tap on the three dots to open the request card menu + await tester.tap(find.byType(RequestList)); + await tester.pump(); + await tester.tap(find.byType(RequestItem)); + await tester.pump(); + await tester.tap(find.byIcon(Icons.more_vert).first); + await tester.pumpAndSettle(); + + // Tap on the "Rename" option in the menu + await tester.tap(find.text('Rename')); + await tester.pumpAndSettle(); + + // Verify that the selectedIdEditStateProvider is not null + final collectionPane = tester.element(find.byType(CollectionPane)); + final container = ProviderScope.containerOf(collectionPane); + expect(container.read(selectedIdEditStateProvider), isNotNull); + expect((container.read(selectedIdEditStateProvider)).runtimeType, String); + }); + + testWidgets( + 'It should be set back to null when user taps outside name editor', + (tester) async { + await tester.pumpWidget( + const ProviderScope( + child: MaterialApp( + home: Scaffold( + body: CollectionPane(), + ), + ), + ), + ); + + // Grab the CollectionPane widget and its ProviderContainer + final collectionPane = tester.element(find.byType(CollectionPane)); + final container = ProviderScope.containerOf(collectionPane); + + // Tap on the three dots to open the request card menu + await tester.tap(find.byType(RequestList)); + await tester.pump(); + await tester.tap(find.byType(RequestItem)); + await tester.pump(); + await tester.tap(find.byIcon(Icons.more_vert).first); + await tester.pumpAndSettle(); + + // Tap on the "Rename" option in the menu + await tester.tap(find.text('Rename')); + await tester.pumpAndSettle(); + + // Verify that the selectedIdEditStateProvider is not null + expect(container.read(selectedIdEditStateProvider), isNotNull); + expect((container.read(selectedIdEditStateProvider)).runtimeType, String); + + // Tap on the screen to simulate tapping outside the name editor + await tester.tap(find.byType(CollectionPane)); + await tester.pumpAndSettle(); + + // Verify that the selectedIdEditStateProvider is null + expect(container.read(selectedIdEditStateProvider), null); + }); + testWidgets("It should be properly disposed", (tester) async { + await tester.pumpWidget( + const ProviderScope( + child: MaterialApp( + home: Scaffold( + body: CollectionPane(), + ), + ), + ), + ); + + // Grab the Dashboard widget and its ProviderContainer + final collectionPane = tester.element(find.byType(CollectionPane)); + final container = ProviderScope.containerOf(collectionPane); + + // Pumping a different widget to remove the CollectionPane from the widget tree + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: Text('Foo')), + ), + ); + + // Verify that the ProviderContainer has been disposed + // by trying to read from disposed container + bool isDisposed = false; + try { + container.read(selectedIdEditStateProvider); + } catch (e) { + isDisposed = true; + } + expect(isDisposed, true); + }); + }); +} diff --git a/test/widgets/request_widgets_test.dart b/test/widgets/request_widgets_test.dart index b50a033f..b47114b8 100644 --- a/test/widgets/request_widgets_test.dart +++ b/test/widgets/request_widgets_test.dart @@ -21,7 +21,6 @@ void main() { ); expect(find.byType(Center), findsAtLeastNWidgets(1)); - expect(find.text('Request'), findsOneWidget); expect(find.text('Hide Code'), findsOneWidget); expect(find.text('View Code'), findsNothing); expect(find.text('URL Params'), findsOneWidget); @@ -52,7 +51,6 @@ void main() { ); expect(find.byType(Center), findsAtLeastNWidgets(1)); - expect(find.text('Request'), findsOneWidget); expect(find.text('Hide Code'), findsOneWidget); expect(find.text('View Code'), findsNothing); expect(find.text('URL Params'), findsOneWidget); @@ -83,7 +81,6 @@ void main() { ); expect(find.byType(Center), findsAtLeastNWidgets(1)); - expect(find.text('Request'), findsOneWidget); expect(find.text('Hide Code'), findsNothing); expect(find.text('View Code'), findsOneWidget); expect(find.text('URL Params'), findsOneWidget); @@ -117,7 +114,6 @@ void main() { ); expect(find.byType(Center), findsAtLeastNWidgets(1)); - expect(find.text('Request'), findsOneWidget); expect(find.text('URL Params'), findsOneWidget); expect(find.text('Headers'), findsOneWidget); expect(find.text('Body'), findsOneWidget); diff --git a/test/widgets/response_widgets_test.dart b/test/widgets/response_widgets_test.dart index a8def9cc..3bd5d2d6 100644 --- a/test/widgets/response_widgets_test.dart +++ b/test/widgets/response_widgets_test.dart @@ -75,11 +75,8 @@ void main() { ), ); - expect(find.byType(RichText), findsAtLeastNWidgets(1)); - expect( - find.textContaining("Response (", findRichText: true), findsOneWidget); - expect(find.text('Hi'), findsOneWidget); expect(find.textContaining("200", findRichText: true), findsOneWidget); + expect(find.textContaining("Hi", findRichText: true), findsOneWidget); expect(find.text(humanizeDuration(const Duration(microseconds: 23))), findsOneWidget); }); @@ -96,7 +93,7 @@ void main() { ), ); - expect(find.text('Body'), findsOneWidget); + expect(find.text('Response Body'), findsOneWidget); expect(find.text('Headers'), findsOneWidget); await tester.tap(find.text('Headers')); @@ -104,7 +101,7 @@ void main() { expect(find.text('first'), findsNothing); expect(find.text('second'), findsOneWidget); - await tester.tap(find.text('Body')); + await tester.tap(find.text('Response Body')); await tester.pumpAndSettle(); expect(find.text('first'), findsOneWidget);