From d7b6d1e49a97d182e8149157131efb87ef389dd3 Mon Sep 17 00:00:00 2001 From: hunteraraujo Date: Thu, 24 Aug 2023 11:55:05 -0700 Subject: [PATCH] Implement and Test Chat Input Field Widget This commit introduces the ChatInputField widget, a custom text input field designed for use within the ChatView. The ChatInputField widget handles varying screen sizes and gracefully resizes itself according to the available width. It starts with a height of 50 and can expand up to 400 as the user types more lines of text. In addition to the implementation, this commit also includes widget tests to ensure the ChatInputField behaves as expected. - Add ChatInputField widget with dynamic resizing - Include IconButton for sending messages - Add widget tests for ChatInputField - Handle edge cases and overflows --- lib/views/chat/chat_input_field.dart | 79 +++++++++++++++++++++++ lib/views/{ => task}/new_task_button.dart | 0 lib/views/{ => task}/task_list_tile.dart | 0 lib/views/{ => task}/task_view.dart | 8 +-- test/chat_input_field_test.dart | 71 ++++++++++++++++++++ test/new_task_button_test.dart | 2 +- test/task_list_tile_test.dart | 2 +- 7 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 lib/views/chat/chat_input_field.dart rename lib/views/{ => task}/new_task_button.dart (100%) rename lib/views/{ => task}/task_list_tile.dart (100%) rename lib/views/{ => task}/task_view.dart (90%) create mode 100644 test/chat_input_field_test.dart diff --git a/lib/views/chat/chat_input_field.dart b/lib/views/chat/chat_input_field.dart new file mode 100644 index 00000000..9f6ee817 --- /dev/null +++ b/lib/views/chat/chat_input_field.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; + +class ChatInputField extends StatefulWidget { + // Callback to be triggered when the send button is pressed + final VoidCallback onSendPressed; + + const ChatInputField({ + Key? key, + required this.onSendPressed, + }) : super(key: key); + + @override + _ChatInputFieldState createState() => _ChatInputFieldState(); +} + +class _ChatInputFieldState extends State { + // Controller for the TextField to manage its content + final TextEditingController _controller = TextEditingController(); + + @override + Widget build(BuildContext context) { + // Using LayoutBuilder to provide the current constraints of the widget, + // ensuring it rebuilds when the window size changes + return LayoutBuilder( + builder: (context, constraints) { + // Calculate the width of the chat view based on the constraints provided + double chatViewWidth = constraints.maxWidth; + + // Determine the width of the input field based on the chat view width. + // If the chat view width is 1000 or more, the input width will be 900. + // Otherwise, the input width will be the chat view width minus 40. + double inputWidth = (chatViewWidth >= 1000) ? 900 : chatViewWidth - 40; + + return Row( + // Centering the children of the Row + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Container( + width: inputWidth, + // Defining the minimum and maximum height for the TextField container + constraints: BoxConstraints( + minHeight: 50, + maxHeight: 400, + ), + // Styling the container with a border and rounded corners + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.black, width: 0.5), + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric(horizontal: 8), + // Using SingleChildScrollView to ensure the TextField can scroll + // when the content exceeds its maximum height + child: SingleChildScrollView( + reverse: true, + child: TextField( + controller: _controller, + // Allowing the TextField to expand vertically and accommodate multiple lines + maxLines: null, + decoration: InputDecoration( + hintText: 'Type a message...', + border: InputBorder.none, + ), + ), + ), + ), + ), + // Send button to trigger the provided onSendPressed callback + IconButton( + icon: Icon(Icons.send), + onPressed: widget.onSendPressed, + ), + ], + ); + }, + ); + } +} diff --git a/lib/views/new_task_button.dart b/lib/views/task/new_task_button.dart similarity index 100% rename from lib/views/new_task_button.dart rename to lib/views/task/new_task_button.dart diff --git a/lib/views/task_list_tile.dart b/lib/views/task/task_list_tile.dart similarity index 100% rename from lib/views/task_list_tile.dart rename to lib/views/task/task_list_tile.dart diff --git a/lib/views/task_view.dart b/lib/views/task/task_view.dart similarity index 90% rename from lib/views/task_view.dart rename to lib/views/task/task_view.dart index 9295e9a2..3d865d51 100644 --- a/lib/views/task_view.dart +++ b/lib/views/task/task_view.dart @@ -1,5 +1,5 @@ -import 'package:auto_gpt_flutter_client/views/new_task_button.dart'; -import 'package:auto_gpt_flutter_client/views/task_list_tile.dart'; +import 'package:auto_gpt_flutter_client/views/task/new_task_button.dart'; +import 'package:auto_gpt_flutter_client/views/task/task_list_tile.dart'; import 'package:flutter/material.dart'; import 'package:auto_gpt_flutter_client/viewmodels/task_viewmodel.dart'; @@ -34,11 +34,11 @@ class _TaskViewState extends State { padding: const EdgeInsets.all(8.0), child: Column( children: [ - Text( + const Text( 'Tasks', style: TextStyle(fontSize: 20, fontWeight: FontWeight.normal), ), - SizedBox(height: 8), + const SizedBox(height: 8), NewTaskButton( onPressed: () { // TODO: Implement add new task action diff --git a/test/chat_input_field_test.dart b/test/chat_input_field_test.dart new file mode 100644 index 00000000..2aa8eade --- /dev/null +++ b/test/chat_input_field_test.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:auto_gpt_flutter_client/views/chat/chat_input_field.dart'; + +void main() { + // Test if the ChatInputField widget renders correctly + testWidgets('ChatInputField renders correctly', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: ChatInputField( + onSendPressed: () {}, + ), + ), + ), + ); + + // Find the TextField widget + expect(find.byType(TextField), findsOneWidget); + // Find the send IconButton + expect(find.byIcon(Icons.send), findsOneWidget); + }); + + // Test if the TextField inside ChatInputField can accept and display input + testWidgets('ChatInputField text field accepts input', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: ChatInputField( + onSendPressed: () {}, + ), + ), + ), + ); + + // Type 'Hello' into the TextField + await tester.enterText(find.byType(TextField), 'Hello'); + // Rebuild the widget with the new text + await tester.pump(); + + // Expect to find 'Hello' in the TextField + expect(find.text('Hello'), findsOneWidget); + }); + + // Test if the send button triggers the provided onSendPressed callback + testWidgets('ChatInputField send button triggers callback', + (WidgetTester tester) async { + bool onPressedCalled = false; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: ChatInputField( + onSendPressed: () { + onPressedCalled = true; + }, + ), + ), + ), + ); + + // Tap the send IconButton + await tester.tap(find.byIcon(Icons.send)); + // Rebuild the widget after the tap + await tester.pump(); + + // Check if the callback was called + expect(onPressedCalled, isTrue); + }); +} diff --git a/test/new_task_button_test.dart b/test/new_task_button_test.dart index f0d5078a..107a28eb 100644 --- a/test/new_task_button_test.dart +++ b/test/new_task_button_test.dart @@ -1,4 +1,4 @@ -import 'package:auto_gpt_flutter_client/views/new_task_button.dart'; +import 'package:auto_gpt_flutter_client/views/task/new_task_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/test/task_list_tile_test.dart b/test/task_list_tile_test.dart index 39706cda..e9b0c21d 100644 --- a/test/task_list_tile_test.dart +++ b/test/task_list_tile_test.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; -import 'package:auto_gpt_flutter_client/views/task_list_tile.dart'; +import 'package:auto_gpt_flutter_client/views/task/task_list_tile.dart'; import 'package:auto_gpt_flutter_client/models/task.dart'; void main() {