feat: add dashbot widget tests(cv: 99)

This commit is contained in:
Udhay-Adithya
2025-09-25 20:08:33 +05:30
parent 74366454f8
commit 50fb3cfee2
4 changed files with 465 additions and 0 deletions

View File

@@ -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<MarkdownBody>(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');
});
}

View File

@@ -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);
});
}

View File

@@ -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<OpenApiOperationItem>? 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<List<OpenApiOperationItem>?> 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<FilledButton>(importFinder).onPressed,
isNotNull,
);
// Toggle "Select all" off → deselect everything & disable import
await tester.tap(selectAllFinder);
await tester.pumpAndSettle();
expect(
tester.widget<FilledButton>(importFinder).onPressed,
isNull,
);
// Toggle "Select all" back on → reselect all and enable import
await tester.tap(selectAllFinder);
await tester.pumpAndSettle();
expect(
tester.widget<FilledButton>(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<FilledButton>(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<List<OpenApiOperationItem>?> 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);
});
}

View File

@@ -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<Text>(find.text('Perform action'));
expect(textWidget.textAlign, TextAlign.left);
await tester.tap(find.byType(TextButton));
await tester.pump();
expect(tapped, isTrue);
});
}