mirror of
https://github.com/foss42/apidash.git
synced 2025-12-12 16:17:34 +08:00
feat: add dashbot widget tests(cv: 99)
This commit is contained in:
120
test/dashbot/widgets/chat/chat_bubble_test.dart
Normal file
120
test/dashbot/widgets/chat/chat_bubble_test.dart
Normal 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');
|
||||||
|
});
|
||||||
|
}
|
||||||
132
test/dashbot/widgets/chat/dashbot_task_buttons_test.dart
Normal file
132
test/dashbot/widgets/chat/dashbot_task_buttons_test.dart
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
30
test/dashbot/widgets/home/home_screen_task_button_test.dart
Normal file
30
test/dashbot/widgets/home/home_screen_task_button_test.dart
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user