test: add test for all dashbot pages

This commit is contained in:
Udhay-Adithya
2025-09-27 22:25:38 +05:30
parent 15821a06ea
commit dc647847af
4 changed files with 808 additions and 0 deletions

View File

@@ -0,0 +1,318 @@
import 'package:apidash/dashbot/core/constants/constants.dart';
import 'package:apidash/dashbot/features/chat/models/chat_message.dart';
import 'package:apidash/dashbot/features/chat/models/chat_state.dart';
import 'package:apidash/dashbot/features/chat/view/pages/dashbot_chat_page.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/providers/collection_providers.dart';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'test_utils.dart';
void main() {
Widget createChatScreen({
List<Override> overrides = const [],
ChatMessageType? initialTask,
}) {
return ProviderScope(
overrides: [
// Override the selectedRequestModelProvider to prevent Hive dependency issues
selectedRequestModelProvider.overrideWith((ref) => null),
...overrides,
],
child: MaterialApp(
home: Scaffold(
body: ChatScreen(initialTask: initialTask),
),
),
);
}
testWidgets('ChatScreen shows empty-state prompt when idle', (tester) async {
late SpyChatViewmodel spy;
await tester.pumpWidget(
createChatScreen(
overrides: [
chatViewmodelProvider.overrideWith((ref) {
spy = SpyChatViewmodel(ref);
spy.setState(const ChatState());
return spy;
}),
],
),
);
await tester.pump();
expect(find.text('Ask me anything!'), findsOneWidget);
expect(spy.sendMessageCalls, isEmpty);
});
testWidgets('ChatScreen triggers initial task without user input',
(tester) async {
late SpyChatViewmodel spy;
await tester.pumpWidget(
createChatScreen(
initialTask: ChatMessageType.generateDoc,
overrides: [
chatViewmodelProvider.overrideWith((ref) {
spy = SpyChatViewmodel(ref);
spy.setState(const ChatState());
return spy;
}),
],
),
);
await tester.pump();
expect(spy.sendMessageCalls.length, 1);
expect(spy.sendMessageCalls.first.text, isEmpty);
expect(spy.sendMessageCalls.first.type, ChatMessageType.generateDoc);
expect(spy.sendMessageCalls.first.countAsUser, isFalse);
});
testWidgets('ChatScreen toggles task suggestions panel', (tester) async {
late SpyChatViewmodel spy;
await tester.pumpWidget(
createChatScreen(
overrides: [
chatViewmodelProvider.overrideWith((ref) {
spy = SpyChatViewmodel(ref);
spy.setState(const ChatState());
return spy;
}),
],
),
);
expect(find.byType(DashbotTaskButtons), findsNothing);
await tester.tap(find.byIcon(Icons.help_outline_rounded));
await tester.pump();
expect(find.byType(DashbotTaskButtons), findsOneWidget);
});
testWidgets('Clear chat icon delegates to viewmodel', (tester) async {
late SpyChatViewmodel spy;
await tester.pumpWidget(
createChatScreen(
overrides: [
chatViewmodelProvider.overrideWith((ref) {
spy = SpyChatViewmodel(ref);
spy.setState(const ChatState());
return spy;
}),
],
),
);
await tester.tap(find.byIcon(Icons.clear_all_rounded));
await tester.pump();
expect(spy.clearCalled, isTrue);
});
testWidgets('Submitting text sends general chat message', (tester) async {
late SpyChatViewmodel spy;
await tester.pumpWidget(
createChatScreen(
overrides: [
chatViewmodelProvider.overrideWith((ref) {
spy = SpyChatViewmodel(ref);
spy.setState(const ChatState());
return spy;
}),
],
),
);
await tester.enterText(find.byType(TextField), 'Hello Dashbot');
await tester.tap(find.byIcon(Icons.send_rounded));
await tester.pump();
expect(spy.sendMessageCalls.length, 1);
expect(spy.sendMessageCalls.first.text, 'Hello Dashbot');
expect(spy.sendMessageCalls.first.type, ChatMessageType.general);
});
testWidgets('Streaming state renders temporary ChatBubble', (tester) async {
late SpyChatViewmodel spy;
await tester.pumpWidget(
createChatScreen(
overrides: [
chatViewmodelProvider.overrideWith((ref) {
spy = SpyChatViewmodel(ref);
spy.setState(const ChatState(
isGenerating: true, currentStreamingResponse: 'Streaming...'));
return spy;
}),
],
),
);
await tester.pump();
final markdown =
tester.widget<MarkdownBody>(find.byType(MarkdownBody).first);
expect(markdown.data, 'Streaming...');
});
testWidgets('Existing chat messages render in list', (tester) async {
late SpyChatViewmodel spy;
final messages = [
ChatMessage(
id: '1',
content: 'First',
role: MessageRole.user,
timestamp: DateTime(2024),
),
ChatMessage(
id: '2',
content: 'Second',
role: MessageRole.system,
timestamp: DateTime(2024, 2),
),
];
await tester.pumpWidget(
createChatScreen(
overrides: [
chatViewmodelProvider.overrideWith((ref) {
spy = SpyChatViewmodel(ref);
spy.setMessages(messages);
spy.setState(ChatState(chatSessions: {'global': messages}));
return spy;
}),
],
),
);
await tester.pump();
expect(find.byType(ListView), findsOneWidget);
expect(find.text('First'), findsOneWidget);
expect(find.text('Second'), findsOneWidget);
});
testWidgets('TextField onSubmitted sends message', (tester) async {
late SpyChatViewmodel spy;
await tester.pumpWidget(
createChatScreen(
overrides: [
chatViewmodelProvider.overrideWith((ref) {
spy = SpyChatViewmodel(ref);
spy.setState(const ChatState());
return spy;
}),
],
),
);
await tester.enterText(find.byType(TextField), 'Test message');
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pump();
expect(spy.sendMessageCalls.length, 1);
expect(spy.sendMessageCalls.first.text, 'Test message');
expect(spy.sendMessageCalls.first.type, ChatMessageType.general);
});
testWidgets('Task suggestions panel hides when generating starts',
(tester) async {
late SpyChatViewmodel spy;
await tester.pumpWidget(
createChatScreen(
overrides: [
chatViewmodelProvider.overrideWith((ref) {
spy = SpyChatViewmodel(ref);
spy.setState(const ChatState());
return spy;
}),
],
),
);
// First show task suggestions
await tester.tap(find.byIcon(Icons.help_outline_rounded));
await tester.pump();
expect(find.byType(DashbotTaskButtons), findsOneWidget);
// Then start generating - this should hide the task suggestions
spy.setState(const ChatState(isGenerating: true));
await tester.pump();
expect(find.byType(DashbotTaskButtons), findsNothing);
});
testWidgets('Scroll animation triggers on streaming response changes',
(tester) async {
late SpyChatViewmodel spy;
await tester.pumpWidget(
createChatScreen(
overrides: [
chatViewmodelProvider.overrideWith((ref) {
spy = SpyChatViewmodel(ref);
spy.setState(const ChatState(
isGenerating: true,
currentStreamingResponse: 'Initial...',
));
return spy;
}),
],
),
);
await tester.pump();
// Change the streaming response - this should trigger scroll
spy.setState(const ChatState(
isGenerating: true,
currentStreamingResponse: 'Updated streaming response...',
));
await tester.pump();
// Verify scrolling behavior by checking that the new content is rendered
expect(find.text('Updated streaming response...'), findsOneWidget);
});
testWidgets('Scroll animation triggers when generation completes',
(tester) async {
late SpyChatViewmodel spy;
final messages = [
ChatMessage(
id: '1',
content: 'Generated response',
role: MessageRole.system,
timestamp: DateTime(2024),
),
];
await tester.pumpWidget(
createChatScreen(
overrides: [
chatViewmodelProvider.overrideWith((ref) {
spy = SpyChatViewmodel(ref);
spy.setState(const ChatState(isGenerating: true));
return spy;
}),
],
),
);
await tester.pump();
// Complete generation - this should trigger scroll
spy.setMessages(messages);
spy.setState(ChatState(
isGenerating: false,
chatSessions: {'global': messages},
));
await tester.pump();
expect(find.text('Generated response'), findsOneWidget);
});
}

View File

@@ -0,0 +1,128 @@
import 'package:apidash/dashbot/core/common/pages/dashbot_default_page.dart';
import 'package:apidash/dashbot/core/constants/constants.dart';
import 'package:apidash/dashbot/core/routes/dashbot_routes.dart';
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';
import 'test_utils.dart';
Finder _taskButton(String snippet) => find.byWidgetPredicate(
(widget) =>
widget is HomeScreenTaskButton && widget.label.contains(snippet),
);
void main() {
testWidgets('DashbotDefaultPage renders greeting and actions',
(tester) async {
await tester.pumpWidget(const MaterialApp(home: DashbotDefaultPage()));
expect(find.textContaining('Hello there'), findsOneWidget);
expect(find.textContaining('make one'), findsOneWidget);
expect(find.textContaining('Open Chat'), findsOneWidget);
expect(find.textContaining('Import cURL'), findsOneWidget);
expect(find.textContaining('Import OpenAPI'), findsOneWidget);
});
testWidgets('Open Chat button pushes chat route without arguments',
(tester) async {
final observer = RecordingNavigatorObserver();
Object? capturedArgs;
await tester.pumpWidget(
MaterialApp(
navigatorObservers: [observer],
onGenerateRoute: (settings) {
if (settings.name == DashbotRoutes.dashbotChat) {
capturedArgs = settings.arguments;
}
return MaterialPageRoute(
settings: settings,
builder: (_) => const SizedBox.shrink(),
);
},
home: const DashbotDefaultPage(),
),
);
await tester.pump();
final openChatButton = _taskButton('Open Chat');
expect(openChatButton, findsOneWidget);
await tester.tap(find.descendant(
of: openChatButton,
matching: find.byType(TextButton),
));
await tester.pumpAndSettle();
expect(observer.lastRoute?.settings.name, DashbotRoutes.dashbotChat);
expect(capturedArgs, isNull);
});
group('Import buttons push chat route with correct arguments', () {
testWidgets('Import cURL button', (tester) async {
final observer = RecordingNavigatorObserver();
Object? capturedArgs;
await tester.pumpWidget(
MaterialApp(
navigatorObservers: [observer],
onGenerateRoute: (settings) {
if (settings.name == DashbotRoutes.dashbotChat) {
capturedArgs = settings.arguments;
}
return MaterialPageRoute(
settings: settings,
builder: (_) => const SizedBox.shrink(),
);
},
home: const DashbotDefaultPage(),
),
);
await tester.pump();
final importCurlButton = _taskButton('Import cURL');
expect(importCurlButton, findsOneWidget);
await tester.tap(find.descendant(
of: importCurlButton,
matching: find.byType(TextButton),
));
await tester.pumpAndSettle();
expect(observer.lastRoute?.settings.name, DashbotRoutes.dashbotChat);
expect(capturedArgs, ChatMessageType.importCurl);
});
testWidgets('Import OpenAPI button', (tester) async {
final observer = RecordingNavigatorObserver();
Object? capturedArgs;
await tester.pumpWidget(
MaterialApp(
navigatorObservers: [observer],
onGenerateRoute: (settings) {
if (settings.name == DashbotRoutes.dashbotChat) {
capturedArgs = settings.arguments;
}
return MaterialPageRoute(
settings: settings,
builder: (_) => const SizedBox.shrink(),
);
},
home: const DashbotDefaultPage(),
),
);
await tester.pump();
final importOpenApiButton = _taskButton('Import OpenAPI');
expect(importOpenApiButton, findsOneWidget);
await tester.tap(find.descendant(
of: importOpenApiButton,
matching: find.byType(TextButton),
));
await tester.pumpAndSettle();
expect(observer.lastRoute?.settings.name, DashbotRoutes.dashbotChat);
expect(capturedArgs, ChatMessageType.importOpenApi);
});
});
}

View File

@@ -0,0 +1,311 @@
import 'package:apidash/dashbot/features/home/view/pages/dashbot_home_page.dart';
import 'package:apidash/dashbot/features/home/view/widgets/home_screen_task_button.dart';
import 'package:apidash/dashbot/core/constants/constants.dart';
import 'package:apidash/dashbot/core/providers/dashbot_window_notifier.dart';
import 'package:apidash/dashbot/core/routes/dashbot_routes.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 '../widgets/action_buttons/test_utils.dart';
Finder _taskButton(String snippet) => find.byWidgetPredicate(
(widget) =>
widget is HomeScreenTaskButton && widget.label.contains(snippet),
);
Future<RecordingDashbotWindowNotifier> _pumpHomePage(
WidgetTester tester, {
RequestModel? selectedModel,
void Function(String? name, Object? arguments)? onRoute,
}) async {
final windowNotifier = RecordingDashbotWindowNotifier();
await tester.pumpWidget(
ProviderScope(
overrides: [
dashbotWindowNotifierProvider.overrideWith((ref) => windowNotifier),
selectedRequestModelProvider.overrideWith((ref) => selectedModel),
],
child: MaterialApp(
onGenerateRoute: (settings) {
onRoute?.call(settings.name, settings.arguments);
return MaterialPageRoute(
settings: settings,
builder: (_) => const SizedBox.shrink(),
);
},
home: const Scaffold(body: DashbotHomePage()),
),
),
);
await tester.pumpAndSettle();
return windowNotifier;
}
void main() {
testWidgets('DashbotHomePage renders greeting and quick actions',
(tester) async {
await _pumpHomePage(tester, onRoute: (_, __) {});
expect(find.textContaining('Hello there'), findsOneWidget);
expect(find.textContaining('How can I help you today'), findsOneWidget);
// Note: 'Chat with Dashbot' is only available in debug mode, so we don't test for it here
expect(find.textContaining('Explain me this response'), findsOneWidget);
expect(find.textContaining('Generate documentation'), findsOneWidget);
});
testWidgets('Chat with Dashbot button appears and works in debug mode',
(tester) async {
// In debug mode (which tests run in), the button should be present
String? capturedRoute;
Object? capturedArgs;
await _pumpHomePage(
tester,
onRoute: (name, arguments) {
capturedRoute = name;
capturedArgs = arguments;
},
);
// The button should be visible in debug mode
final buttonFinder = _taskButton('Chat with Dashbot');
expect(buttonFinder, findsOneWidget);
// Tap the button
await tester.tap(find.descendant(
of: buttonFinder,
matching: find.byType(TextButton),
));
await tester.pumpAndSettle();
// Should navigate to chat without any initial task arguments
expect(capturedRoute, DashbotRoutes.dashbotChat);
expect(capturedArgs, isNull);
});
group('Quick action buttons navigate with correct arguments', () {
testWidgets('Explain me this response button', (tester) async {
String? capturedRoute;
Object? capturedArgs;
await _pumpHomePage(
tester,
onRoute: (name, arguments) {
capturedRoute = name;
capturedArgs = arguments;
},
);
final buttonFinder = _taskButton('Explain me this response');
expect(buttonFinder, findsOneWidget);
await tester.tap(find.descendant(
of: buttonFinder,
matching: find.byType(TextButton),
));
await tester.pumpAndSettle();
expect(capturedRoute, DashbotRoutes.dashbotChat);
expect(capturedArgs, ChatMessageType.explainResponse);
});
testWidgets('Help me debug this error button', (tester) async {
String? capturedRoute;
Object? capturedArgs;
await _pumpHomePage(
tester,
onRoute: (name, arguments) {
capturedRoute = name;
capturedArgs = arguments;
},
);
final buttonFinder = _taskButton('Help me debug this error');
expect(buttonFinder, findsOneWidget);
await tester.tap(find.descendant(
of: buttonFinder,
matching: find.byType(TextButton),
));
await tester.pumpAndSettle();
expect(capturedRoute, DashbotRoutes.dashbotChat);
expect(capturedArgs, ChatMessageType.debugError);
});
testWidgets('Generate documentation button', (tester) async {
String? capturedRoute;
Object? capturedArgs;
await _pumpHomePage(
tester,
onRoute: (name, arguments) {
capturedRoute = name;
capturedArgs = arguments;
},
);
final buttonFinder = _taskButton('Generate documentation');
expect(buttonFinder, findsOneWidget);
await tester.tap(find.descendant(
of: buttonFinder,
matching: find.byType(TextButton),
));
await tester.pumpAndSettle();
expect(capturedRoute, DashbotRoutes.dashbotChat);
expect(capturedArgs, ChatMessageType.generateDoc);
});
testWidgets('Generate Tests button', (tester) async {
String? capturedRoute;
Object? capturedArgs;
await _pumpHomePage(
tester,
onRoute: (name, arguments) {
capturedRoute = name;
capturedArgs = arguments;
},
);
final buttonFinder = _taskButton('Generate Tests');
expect(buttonFinder, findsOneWidget);
await tester.tap(find.descendant(
of: buttonFinder,
matching: find.byType(TextButton),
));
await tester.pumpAndSettle();
expect(capturedRoute, DashbotRoutes.dashbotChat);
expect(capturedArgs, ChatMessageType.generateTest);
});
testWidgets('Generate Code button', (tester) async {
String? capturedRoute;
Object? capturedArgs;
await _pumpHomePage(
tester,
onRoute: (name, arguments) {
capturedRoute = name;
capturedArgs = arguments;
},
);
final buttonFinder = _taskButton('Generate Code');
expect(buttonFinder, findsOneWidget);
await tester.tap(find.descendant(
of: buttonFinder,
matching: find.byType(TextButton),
));
await tester.pumpAndSettle();
expect(capturedRoute, DashbotRoutes.dashbotChat);
expect(capturedArgs, ChatMessageType.generateCode);
});
testWidgets('Import cURL button', (tester) async {
String? capturedRoute;
Object? capturedArgs;
await _pumpHomePage(
tester,
onRoute: (name, arguments) {
capturedRoute = name;
capturedArgs = arguments;
},
);
final buttonFinder = _taskButton('Import cURL');
expect(buttonFinder, findsOneWidget);
await tester.tap(find.descendant(
of: buttonFinder,
matching: find.byType(TextButton),
));
await tester.pumpAndSettle();
expect(capturedRoute, DashbotRoutes.dashbotChat);
expect(capturedArgs, ChatMessageType.importCurl);
});
testWidgets('Import OpenAPI button', (tester) async {
String? capturedRoute;
Object? capturedArgs;
await _pumpHomePage(
tester,
onRoute: (name, arguments) {
capturedRoute = name;
capturedArgs = arguments;
},
);
final buttonFinder = _taskButton('Import OpenAPI');
expect(buttonFinder, findsOneWidget);
await tester.tap(find.descendant(
of: buttonFinder,
matching: find.byType(TextButton),
));
await tester.pumpAndSettle();
expect(capturedRoute, DashbotRoutes.dashbotChat);
expect(capturedArgs, ChatMessageType.importOpenApi);
});
});
testWidgets(
'Generate Tool hides and shows dashbot window even without response',
(tester) async {
final notifier = await _pumpHomePage(tester, onRoute: (_, __) {});
await tester.tap(find.text('🛠️ Generate Tool'));
await tester.pumpAndSettle();
expect(notifier.hideCalls, 1);
expect(notifier.showCalls, 1);
});
testWidgets('Generate UI opens dialog and restores dashbot window',
(tester) async {
final responseModel = const HttpResponseModel(
body: 'example response',
formattedBody: 'formatted',
);
final requestModel = RequestModel(
id: 'req-1',
httpRequestModel: const HttpRequestModel(),
httpResponseModel: responseModel,
);
final notifier = await _pumpHomePage(
tester,
selectedModel: requestModel,
onRoute: (_, __) {},
);
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(notifier.hideCalls, 1);
expect(notifier.showCalls, 1);
});
}

View File

@@ -0,0 +1,51 @@
import 'package:apidash/dashbot/features/chat/models/chat_message.dart';
import 'package:apidash/dashbot/features/chat/viewmodel/chat_viewmodel.dart';
import 'package:apidash/dashbot/features/chat/models/chat_state.dart';
import 'package:apidash/dashbot/core/constants/constants.dart';
import 'package:flutter/material.dart';
class SpyChatViewmodel extends ChatViewmodel {
SpyChatViewmodel(super.ref);
final List<({String text, ChatMessageType type, bool countAsUser})>
sendMessageCalls = [];
bool clearCalled = false;
List<ChatMessage> _messages = const [];
void setMessages(List<ChatMessage> messages) {
_messages = messages;
state = state.copyWith(chatSessions: {'global': messages});
}
void setState(ChatState newState) {
state = newState;
}
@override
List<ChatMessage> get currentMessages => _messages;
@override
Future<void> sendMessage({
required String text,
ChatMessageType type = ChatMessageType.general,
bool countAsUser = true,
}) async {
sendMessageCalls.add((text: text, type: type, countAsUser: countAsUser));
}
@override
void clearCurrentChat() {
clearCalled = true;
}
}
class RecordingNavigatorObserver extends NavigatorObserver {
Route<dynamic>? lastRoute;
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
super.didPush(route, previousRoute);
lastRoute = route;
}
}