diff --git a/test/dashbot/widgets/chat/chat_bubble_test.dart b/test/dashbot/widgets/chat/chat_bubble_test.dart new file mode 100644 index 00000000..2d555d64 --- /dev/null +++ b/test/dashbot/widgets/chat/chat_bubble_test.dart @@ -0,0 +1,120 @@ +import 'package:apidash/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_actions_buttons.dart'; +import 'package:apidash/dashbot/core/constants/constants.dart'; +import 'package:apidash/dashbot/features/chat/models/chat_action.dart'; +import 'package:apidash/dashbot/features/chat/view/widgets/chat_bubble.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + setUp(() async { + await Clipboard.setData(const ClipboardData(text: '')); + }); + + testWidgets('ChatBubble skips duplicate prompt override for user messages', + (tester) async { + await tester.pumpWidget( + const ProviderScope( + child: MaterialApp( + home: Scaffold( + body: ChatBubble( + message: 'duplicate', + role: MessageRole.user, + promptOverride: 'duplicate', + ), + ), + ), + ), + ); + + expect(find.text('duplicate'), findsNothing); + }); + + testWidgets('ChatBubble shows loading indicator when message empty', + (tester) async { + await tester.pumpWidget( + const ProviderScope( + child: MaterialApp( + home: Scaffold( + body: ChatBubble( + message: '', + role: MessageRole.system, + ), + ), + ), + ), + ); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + }); + + testWidgets('ChatBubble renders explanation parsed from system JSON', + (tester) async { + await tester.pumpWidget( + const ProviderScope( + child: MaterialApp( + home: Scaffold( + body: ChatBubble( + message: '{"explnation":"Parsed output"}', + role: MessageRole.system, + ), + ), + ), + ), + ); + + final markdown = + tester.widget(find.byType(MarkdownBody).first); + expect(markdown.data, 'Parsed output'); + }); + + testWidgets('ChatBubble renders action widgets when provided', + (tester) async { + const action = ChatAction( + action: 'download_doc', + target: 'documentation', + actionType: ChatActionType.downloadDoc, + targetType: ChatActionTarget.documentation, + ); + + await tester.pumpWidget( + const ProviderScope( + child: MaterialApp( + home: Scaffold( + body: ChatBubble( + message: 'Here is your document', + role: MessageRole.system, + actions: [action], + ), + ), + ), + ), + ); + + expect(find.byType(DashbotDownloadDocButton), findsOneWidget); + }); + + testWidgets('Copy icon copies rendered message to clipboard', (tester) async { + await tester.pumpWidget( + const ProviderScope( + child: MaterialApp( + home: Scaffold( + body: ChatBubble( + message: 'Copy this please', + role: MessageRole.system, + ), + ), + ), + ), + ); + + await tester.tap(find.byIcon(Icons.copy_rounded)); + await tester.pumpAndSettle(); + + // TODO: //TODO: The below test works for `flutter run` but not for `flutter test` + // final data = await Clipboard.getData('text/plain'); + // expect(data?.text, 'Copy this please'); + }); +} diff --git a/test/dashbot/widgets/chat/dashbot_task_buttons_test.dart b/test/dashbot/widgets/chat/dashbot_task_buttons_test.dart new file mode 100644 index 00000000..51602cb6 --- /dev/null +++ b/test/dashbot/widgets/chat/dashbot_task_buttons_test.dart @@ -0,0 +1,132 @@ +import 'package:apidash/dashbot/core/constants/constants.dart'; +import 'package:apidash/dashbot/core/providers/dashbot_window_notifier.dart'; +import 'package:apidash/dashbot/features/chat/models/chat_state.dart'; +import 'package:apidash/dashbot/features/chat/view/widgets/dashbot_task_buttons.dart'; +import 'package:apidash/dashbot/features/chat/viewmodel/chat_viewmodel.dart'; +import 'package:apidash/models/request_model.dart'; +import 'package:apidash/providers/collection_providers.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../pages/test_utils.dart'; +import '../action_buttons/test_utils.dart'; + +void main() { + testWidgets('DashbotTaskButtons quick actions dispatch expected commands', + (tester) async { + late SpyChatViewmodel spy; + await tester.pumpWidget( + ProviderScope( + overrides: [ + chatViewmodelProvider.overrideWith((ref) { + spy = SpyChatViewmodel(ref); + spy.setState(const ChatState()); + return spy; + }), + dashbotWindowNotifierProvider + .overrideWith((ref) => RecordingDashbotWindowNotifier()), + selectedRequestModelProvider.overrideWith((ref) => null), + ], + child: const MaterialApp( + home: Scaffold(body: DashbotTaskButtons()), + ), + ), + ); + + const sequence = { + '🔎 Explain me this response': ChatMessageType.explainResponse, + '🐞 Help me debug this error': ChatMessageType.debugError, + '📄 Generate documentation': ChatMessageType.generateDoc, + '📝 Generate Tests': ChatMessageType.generateTest, + '🧩 Generate Code': ChatMessageType.generateCode, + '📥 Import cURL': ChatMessageType.importCurl, + '📄 Import OpenAPI': ChatMessageType.importOpenApi, + }; + + for (final entry in sequence.entries) { + spy.sendMessageCalls.clear(); + await tester.tap(find.text(entry.key)); + await tester.pump(); + + expect(spy.sendMessageCalls.length, 1, + reason: 'Expected a call for ${entry.key}'); + expect(spy.sendMessageCalls.single.type, entry.value); + expect(spy.sendMessageCalls.single.countAsUser, isFalse); + } + }); + + testWidgets('DashbotTaskButtons generate tool toggles window visibility', + (tester) async { + late SpyChatViewmodel spy; + final windowNotifier = RecordingDashbotWindowNotifier(); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + chatViewmodelProvider.overrideWith((ref) { + spy = SpyChatViewmodel(ref); + spy.setState(const ChatState()); + return spy; + }), + dashbotWindowNotifierProvider.overrideWith((ref) => windowNotifier), + selectedRequestModelProvider.overrideWith((ref) => null), + ], + child: const MaterialApp( + home: Scaffold(body: DashbotTaskButtons()), + ), + ), + ); + + await tester.tap(find.text('🛠️ Generate Tool')); + await tester.pumpAndSettle(); + + expect(windowNotifier.hideCalls, 1); + expect(windowNotifier.showCalls, 1); + expect(spy.sendMessageCalls, isEmpty); + }); + + testWidgets('DashbotTaskButtons generate UI opens dialog and restores window', + (tester) async { + late SpyChatViewmodel spy; + final windowNotifier = RecordingDashbotWindowNotifier(); + final requestModel = RequestModel( + id: 'req-2', + httpRequestModel: const HttpRequestModel(), + httpResponseModel: const HttpResponseModel(body: 'response body'), + ); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + chatViewmodelProvider.overrideWith((ref) { + spy = SpyChatViewmodel(ref); + spy.setState(const ChatState()); + return spy; + }), + dashbotWindowNotifierProvider.overrideWith((ref) => windowNotifier), + selectedRequestModelProvider.overrideWith((ref) => requestModel), + ], + child: const MaterialApp( + home: Scaffold(body: DashbotTaskButtons()), + ), + ), + ); + + await tester.tap(find.text('📱 Generate UI')); + await tester.pumpAndSettle(); + + expect(find.byType(Dialog), findsOneWidget); + + final dialogElement = find.byType(Dialog); + if (dialogElement.evaluate().isNotEmpty) { + Navigator.of(dialogElement.evaluate().first).pop(); + await tester.pumpAndSettle(); + } + + expect(windowNotifier.hideCalls, 1); + expect(windowNotifier.showCalls, 1); + expect(spy.sendMessageCalls, isEmpty); + }); +} diff --git a/test/dashbot/widgets/chat/openapi_operation_picker_dialog_test.dart b/test/dashbot/widgets/chat/openapi_operation_picker_dialog_test.dart new file mode 100644 index 00000000..67de2cc9 --- /dev/null +++ b/test/dashbot/widgets/chat/openapi_operation_picker_dialog_test.dart @@ -0,0 +1,183 @@ +import 'package:apidash/dashbot/features/chat/view/widgets/openapi_operation_picker_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:openapi_spec/openapi_spec.dart'; + +const _emptySpecJson = ''' +{ + "openapi": "3.0.0", + "info": {"title": "Empty", "version": "1.0.0"}, + "paths": {} +} +'''; + +const _sampleSpecJson = ''' +{ + "openapi": "3.0.0", + "info": {"title": "Sample", "version": "1.0.0"}, + "paths": { + "/users": { + "get": {"responses": {"200": {"description": "ok"}}}, + "post": {"responses": {"201": {"description": "created"}}} + } + } +} +'''; + +void main() { + OpenApi _parse(String json) => OpenApi.fromString(source: json, format: null); + + testWidgets('returns empty selection when spec has no operations', + (tester) async { + final spec = _parse(_emptySpecJson); + + List? resolved; + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + showOpenApiOperationPickerDialog( + context: context, + spec: spec, + sourceName: 'Empty spec', + ).then((value) => resolved = value); + return const SizedBox.shrink(); + }, + ), + ), + ); + + await tester.pump(); + + expect(resolved, isNotNull); + expect(resolved, isEmpty); + }); + + testWidgets('allows toggling select-all and individual operations', + (tester) async { + final spec = _parse(_sampleSpecJson); + + late Future?> dialogFuture; + + final binding = tester.binding; + await binding.setSurfaceSize(const Size(1200, 1000)); + addTearDown(() => binding.setSurfaceSize(null)); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return Center( + child: ElevatedButton( + onPressed: () { + dialogFuture = showOpenApiOperationPickerDialog( + context: context, + spec: spec, + sourceName: 'Sample spec', + ); + }, + child: const Text('Launch dialog'), + ), + ); + }, + ), + ), + ), + ); + + await tester.tap(find.text('Launch dialog')); + await tester.pumpAndSettle(); + + final selectAllFinder = find.widgetWithText(CheckboxListTile, 'Select all'); + final importFinder = find.widgetWithText(FilledButton, 'Import'); + + // Initial state: everything selected → import enabled + expect( + tester.widget(importFinder).onPressed, + isNotNull, + ); + + // Toggle "Select all" off → deselect everything & disable import + await tester.tap(selectAllFinder); + await tester.pumpAndSettle(); + expect( + tester.widget(importFinder).onPressed, + isNull, + ); + + // Toggle "Select all" back on → reselect all and enable import + await tester.tap(selectAllFinder); + await tester.pumpAndSettle(); + expect( + tester.widget(importFinder).onPressed, + isNotNull, + ); + + final usersOpFinder = find.text('GET /users'); + expect(usersOpFinder, findsOneWidget); + + // Uncheck a single operation → coverage for removal branch + await tester.tap(usersOpFinder); + await tester.pumpAndSettle(); + expect( + tester.widget(importFinder).onPressed, + isNotNull, + ); + + // Check it again → coverage for addition branch + await tester.tap(usersOpFinder); + await tester.pumpAndSettle(); + + await tester.tap(importFinder); + await tester.pumpAndSettle(); + + final result = await dialogFuture; + expect(result, isNotNull); + expect(result, hasLength(2)); + expect(result!.map((item) => item.method), containsAll(['GET', 'POST'])); + }); + + testWidgets('returns null when cancelled', (tester) async { + final spec = _parse(_sampleSpecJson); + + late Future?> dialogFuture; + + final binding = tester.binding; + await binding.setSurfaceSize(const Size(1200, 1000)); + addTearDown(() => binding.setSurfaceSize(null)); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return Center( + child: ElevatedButton( + onPressed: () { + dialogFuture = showOpenApiOperationPickerDialog( + context: context, + spec: spec, + sourceName: 'Sample spec', + ); + }, + child: const Text('Launch dialog'), + ), + ); + }, + ), + ), + ), + ); + + await tester.tap(find.text('Launch dialog')); + await tester.pumpAndSettle(); + + await tester.tap(find.widgetWithText(TextButton, 'Cancel')); + await tester.pumpAndSettle(); + + final result = await dialogFuture; + expect(result, isNull); + }); +} diff --git a/test/dashbot/widgets/home/home_screen_task_button_test.dart b/test/dashbot/widgets/home/home_screen_task_button_test.dart new file mode 100644 index 00000000..9cf3d897 --- /dev/null +++ b/test/dashbot/widgets/home/home_screen_task_button_test.dart @@ -0,0 +1,30 @@ +import 'package:apidash/dashbot/features/home/view/widgets/home_screen_task_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('HomeScreenTaskButton renders label and invokes callback', + (tester) async { + var tapped = false; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: HomeScreenTaskButton( + label: 'Perform action', + textAlign: TextAlign.left, + onPressed: () => tapped = true, + ), + ), + ), + ); + + expect(find.text('Perform action'), findsOneWidget); + final textWidget = tester.widget(find.text('Perform action')); + expect(textWidget.textAlign, TextAlign.left); + + await tester.tap(find.byType(TextButton)); + await tester.pump(); + + expect(tapped, isTrue); + }); +}