From be64addcb96a941582077201bcb18462af86a134 Mon Sep 17 00:00:00 2001 From: Rahul Gulati Date: Tue, 1 Sep 2026 13:57:56 +0000 Subject: [PATCH 1/5] feat(search-list): optional trailing widget beside the search field [REQ-177] IntegratedSearchTextField gains an optional, null-defaulted 'trailing' widget rendered after the search Card (e.g. a filter button). SearchList exposes it as 'searchBarTrailing' and threads it into both the SearchBarInBody and SearchWithAppBar call-sites. Null default means every existing consumer's widget tree is byte-identical. Co-Authored-By: Claude Opus 4.8 --- .../integrated_search_textfield.dart | 176 ++++++++++-------- .../common/search_list/search_list.dart | 7 + test/integrated_search_textfield_test.dart | 42 +++++ 3 files changed, 144 insertions(+), 81 deletions(-) create mode 100644 test/integrated_search_textfield_test.dart diff --git a/lib/src/modules/common/search_list/integrated_search_textfield.dart b/lib/src/modules/common/search_list/integrated_search_textfield.dart index 867f0c1f..e042b101 100644 --- a/lib/src/modules/common/search_list/integrated_search_textfield.dart +++ b/lib/src/modules/common/search_list/integrated_search_textfield.dart @@ -18,6 +18,7 @@ class IntegratedSearchTextField extends StatefulWidget { required this.queryTextController, this.showCrossbutton = false, this.borderRadius = 5, + this.trailing, Key? key, }) : super(key: key); @@ -36,6 +37,10 @@ class IntegratedSearchTextField extends StatefulWidget { final VoidCallback? onMicTap; final double borderRadius; + // [REQ-177] Optional widget rendered beside the search field (e.g. a filter button). + // Null by default, so every existing consumer's widget tree is unchanged. + final Widget? trailing; + // final SearchListBloc _searchListBloc=SearchListBloc(); @override @@ -49,95 +54,104 @@ class _IntegratedSearchTextFieldState extends State { FlutterTts _flutterTts = FlutterTts(); @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Card( - elevation: widget.elevation ?? 0, - child: TextField( - onTap: widget.onTap, - controller: widget.queryTextController, - style: TextStyle(color: Colors.black), - autofocus: widget.autoFocus ?? true, - textInputAction: widget.textInputAction, - keyboardType: widget.keyboardType, - onSubmitted: widget.onSubmitted, - decoration: InputDecoration( - filled: true, - fillColor: widget.bgColor, - prefixIcon: widget.prefixIcon, - isDense: true, - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(widget.borderRadius), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - hintText: widget.searchFieldLabel, - suffixIcon: Row( - children: [ - if (widget.showCrossbutton) - Container( - width: 18, - height: 18, - margin: EdgeInsets.fromLTRB(10, 10, 10, 10), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(9), - color: Colors.black.withOpacity(0.4), - ), - child: InkWell( - onTap: widget.queryTextController.clear, - child: Icon( - Icons.close, - size: 12, - color: Colors.white, - ), + final searchCard = Card( + elevation: widget.elevation ?? 0, + child: TextField( + onTap: widget.onTap, + controller: widget.queryTextController, + style: TextStyle(color: Colors.black), + autofocus: widget.autoFocus ?? true, + textInputAction: widget.textInputAction, + keyboardType: widget.keyboardType, + onSubmitted: widget.onSubmitted, + decoration: InputDecoration( + filled: true, + fillColor: widget.bgColor, + prefixIcon: widget.prefixIcon, + isDense: true, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(widget.borderRadius), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + hintText: widget.searchFieldLabel, + suffixIcon: Row( + children: [ + if (widget.showCrossbutton) + Container( + width: 18, + height: 18, + margin: EdgeInsets.fromLTRB(10, 10, 10, 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(9), + color: Colors.black.withOpacity(0.4), + ), + child: InkWell( + onTap: widget.queryTextController.clear, + child: Icon( + Icons.close, + size: 12, + color: Colors.white, ), ), - if (widget.searchThroughMic) - SizedBox( - width: 20, - child: IconButton( - onPressed: widget.onMicTap ?? - () async { - await _speakPrompt(); - await Future.delayed( - const Duration(seconds: 1), + ), + if (widget.searchThroughMic) + SizedBox( + width: 20, + child: IconButton( + onPressed: widget.onMicTap ?? + () async { + await _speakPrompt(); + await Future.delayed( + const Duration(seconds: 1), + ); + var available = await speech.initialize(); + if (available) { + await speech.listen( + onResult: (result) { + setState(() { + print(widget.queryTextController.text); + recognizedText = result.recognizedWords; + widget.queryTextController.text = + recognizedText; + }); + if (result.finalResult) { + speech.stop(); + } + }, ); - var available = await speech.initialize(); - if (available) { - await speech.listen( - onResult: (result) { - setState(() { - print(widget.queryTextController.text); - recognizedText = result.recognizedWords; - widget.queryTextController.text = - recognizedText; - }); - if (result.finalResult) { - speech.stop(); - } - }, - ); - } - }, - icon: Icon(Icons.mic), - ), + } + }, + icon: Icon(Icons.mic), ), - ], - ), - suffixIconConstraints: BoxConstraints( - maxHeight: 38, - maxWidth: 38, - ), - hintStyle: TextStyle( - color: Colors.black26, - ), + ), + ], + ), + suffixIconConstraints: BoxConstraints( + maxHeight: 38, + maxWidth: 38, + ), + hintStyle: TextStyle( + color: Colors.black26, ), ), ), ); + + return Padding( + padding: const EdgeInsets.all(8.0), + child: widget.trailing == null + ? searchCard + : Row( + children: [ + Expanded(child: searchCard), + widget.trailing!, + ], + ), + ); } Future _speakPrompt() async { diff --git a/lib/src/modules/common/search_list/search_list.dart b/lib/src/modules/common/search_list/search_list.dart index 8e90892f..36aa93a4 100644 --- a/lib/src/modules/common/search_list/search_list.dart +++ b/lib/src/modules/common/search_list/search_list.dart @@ -61,6 +61,7 @@ class SearchList extends StatefulWidget { this.showCrossbutton = false, this.bottomGradient, this.actionWidget, + this.searchBarTrailing, Key? key, }) : assert(!showDefaultAppBar ? textEditingController != null : true), super(key: key); @@ -112,6 +113,10 @@ class SearchList extends StatefulWidget { final LinearGradient? bottomGradient; final Widget? actionWidget; + /// Optional widget rendered beside the search field (e.g. a filter button). + /// Null by default, so every existing consumer renders byte-identically. + final Widget? searchBarTrailing; + @override _SearchListState createState() => _SearchListState(); } @@ -236,6 +241,7 @@ class _SearchListState extends State> { queryTextController: searchQueryController!, searchFieldLabel: widget.searchBarTitle ?? 'Search', showCrossbutton: widget.showCrossbutton, + trailing: widget.searchBarTrailing, ), Expanded(child: _child), ], @@ -290,6 +296,7 @@ class _SearchListState extends State> { widget.searchBarTitle ?? 'Search', showCrossbutton: widget.showCrossbutton, borderRadius: 14, + trailing: widget.searchBarTrailing, ), ], ), diff --git a/test/integrated_search_textfield_test.dart b/test/integrated_search_textfield_test.dart new file mode 100644 index 00000000..9a00f3ff --- /dev/null +++ b/test/integrated_search_textfield_test.dart @@ -0,0 +1,42 @@ +// [REQ-177] IntegratedSearchTextField.trailing: an optional widget rendered beside the +// search field. Null by default, so an existing consumer's widget tree is unchanged. +import 'package:fa_flutter_ui_kit/src/modules/common/search_list/integrated_search_textfield.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + Widget wrap(Widget child) => MaterialApp(home: Scaffold(body: child)); + + testWidgets('renders no trailing widget when trailing is null', + (tester) async { + await tester.pumpWidget( + wrap( + IntegratedSearchTextField( + searchFieldLabel: 'Search', + queryTextController: TextEditingController(), + ), + ), + ); + + expect(find.byKey(const Key('search-trailing')), findsNothing); + expect(find.byType(TextField), findsOneWidget); + }); + + testWidgets( + 'renders the trailing widget beside the search field when supplied', + (tester) async { + await tester.pumpWidget( + wrap( + IntegratedSearchTextField( + searchFieldLabel: 'Search', + queryTextController: TextEditingController(), + trailing: const Icon(Icons.filter_list, key: Key('search-trailing')), + ), + ), + ); + + expect(find.byKey(const Key('search-trailing')), findsOneWidget); + expect(find.byType(TextField), findsOneWidget); + expect(find.byType(Row), findsWidgets); + }); +} From 947538f98d5c4ad25bed38c3654d2c6686ae3d7c Mon Sep 17 00:00:00 2001 From: Rahul Gulati Date: Tue, 1 Sep 2026 13:58:12 +0000 Subject: [PATCH 2/5] chore: ignore agent runtime notes [REQ-177] Co-Authored-By: Claude Opus 4.8 --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 3f8fcf83..2f069431 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,8 @@ build/ # FVM Version Cache .fvm/ + +# Agent runtime notes (never committed) +AI_AGENT_CHANGES/ +AGENT.md +AGENTS.md From 2b40c46b56020c2bfa320bc892939512a507673a Mon Sep 17 00:00:00 2001 From: Rahul Gulati Date: Fri, 4 Sep 2026 15:14:39 +0530 Subject: [PATCH 3/5] fix(search-list): fix vacuous trailing tests + add SearchList coverage + gap [REQ-177] The null-path test only asserted a Key that is never mounted regardless of implementation (always findsNothing), and the trailing-path test's find.byType(Row) also matches the suffixIcon Row inside the TextField's own decoration, so neither could actually fail. Rewrote both to assert the real structural invariant (Padding.child is the Card directly, or a Row of [Expanded(Card), trailing]). Added search_list_test.dart covering the two untested SearchList call sites (SearchBarInBody, SearchWithAppBar) that wire searchBarTrailing through. Also added an 8px gap between the search field and trailing widget so it doesn't sit flush against the Card edge. Addresses Vipin's CHANGES_REQUESTED review on PR #209. Co-Authored-By: Claude Sonnet 5 --- .../integrated_search_textfield.dart | 6 +++ test/integrated_search_textfield_test.dart | 25 ++++++++++-- test/search_list_test.dart | 39 +++++++++++++++++++ 3 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 test/search_list_test.dart diff --git a/lib/src/modules/common/search_list/integrated_search_textfield.dart b/lib/src/modules/common/search_list/integrated_search_textfield.dart index e042b101..bf12a743 100644 --- a/lib/src/modules/common/search_list/integrated_search_textfield.dart +++ b/lib/src/modules/common/search_list/integrated_search_textfield.dart @@ -142,12 +142,18 @@ class _IntegratedSearchTextFieldState extends State { ); return Padding( + // [REQ-177 rework] stable key so tests can target this Padding directly instead of + // relying on tree-order (find.byType(Padding).first also matches ancestor framework + // widgets in some hosts). + key: const Key('integrated-search-textfield-padding'), padding: const EdgeInsets.all(8.0), child: widget.trailing == null ? searchCard : Row( children: [ Expanded(child: searchCard), + // [REQ-177 rework] gap so a trailing icon/button doesn't sit flush against the Card edge. + const SizedBox(width: 8), widget.trailing!, ], ), diff --git a/test/integrated_search_textfield_test.dart b/test/integrated_search_textfield_test.dart index 9a00f3ff..54709529 100644 --- a/test/integrated_search_textfield_test.dart +++ b/test/integrated_search_textfield_test.dart @@ -7,7 +7,8 @@ import 'package:flutter_test/flutter_test.dart'; void main() { Widget wrap(Widget child) => MaterialApp(home: Scaffold(body: child)); - testWidgets('renders no trailing widget when trailing is null', + testWidgets( + 'renders the Card directly (no wrapping Row) when trailing is null', (tester) async { await tester.pumpWidget( wrap( @@ -18,12 +19,17 @@ void main() { ), ); - expect(find.byKey(const Key('search-trailing')), findsNothing); + // [REQ-177 rework, Vipin] find.byKey('search-trailing') can never fail here since that + // key is only ever attached by a caller-supplied trailing widget — assert the actual + // structural invariant instead: the outer Padding's child is the Card itself, not a Row. + final padding = tester.widget( + find.byKey(const Key('integrated-search-textfield-padding'))); + expect(padding.child, isA()); expect(find.byType(TextField), findsOneWidget); }); testWidgets( - 'renders the trailing widget beside the search field when supplied', + 'renders the trailing widget beside the search field, inside a Row, when supplied', (tester) async { await tester.pumpWidget( wrap( @@ -37,6 +43,17 @@ void main() { expect(find.byKey(const Key('search-trailing')), findsOneWidget); expect(find.byType(TextField), findsOneWidget); - expect(find.byType(Row), findsWidgets); + + // [REQ-177 rework, Vipin] find.byType(Row) alone also matches the suffixIcon Row inside + // the TextField's decoration, so it passes even if `trailing` were placed elsewhere. + // Assert the actual wiring: the outer Padding's child is a Row whose children are + // Expanded(searchCard) followed by the trailing widget itself. + final padding = tester.widget( + find.byKey(const Key('integrated-search-textfield-padding'))); + expect(padding.child, isA()); + final row = padding.child! as Row; + expect(row.children.first, isA()); + expect((row.children.first as Expanded).child, isA()); + expect(row.children.last.key, const Key('search-trailing')); }); } diff --git a/test/search_list_test.dart b/test/search_list_test.dart new file mode 100644 index 00000000..2a0deee5 --- /dev/null +++ b/test/search_list_test.dart @@ -0,0 +1,39 @@ +// [REQ-177 rework, Vipin] SearchList.searchBarTrailing is plumbed to IntegratedSearchTextField +// on both the SearchBarInBody and SearchWithAppBar code paths, but neither was pumped by a +// widget test — untested since #209 was opened. Covers both here. +import 'package:fa_flutter_ui_kit/src/modules/common/search_list/search_list.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + Widget buildSearchList(SearchListType type) { + return MaterialApp( + home: SearchList( + data: const ['Alpha', 'Beta'], + selectedItem: (_) {}, + itemBuilder: (item, isSelected) => Text(item), + type: type, + searchBarTrailing: + const Icon(Icons.filter_list, key: Key('search-trailing')), + ), + ); + } + + testWidgets( + 'SearchBarInBody renders searchBarTrailing beside the search field', + (tester) async { + await tester.pumpWidget(buildSearchList(SearchListType.SearchBarInBody)); + await tester.pump(); + + expect(find.byKey(const Key('search-trailing')), findsOneWidget); + }); + + testWidgets( + 'SearchWithAppBar renders searchBarTrailing beside the search field', + (tester) async { + await tester.pumpWidget(buildSearchList(SearchListType.SearchWithAppBar)); + await tester.pump(); + + expect(find.byKey(const Key('search-trailing')), findsOneWidget); + }); +} From ff4a6d79a60d4b9fd4cb96cb0f82603ddab851ae Mon Sep 17 00:00:00 2001 From: Rahul Gulati Date: Tue, 15 Sep 2026 05:27:28 +0000 Subject: [PATCH 4/5] style(search-list): address review nits on test comments [REQ-177] singhtaranjeet (2026-09-09 re-review, round 2): commit-message material ([REQ-177 rework, Vipin] tag, "untested since #209 was opened" reference) doesn't belong in source comments a future reader has to parse. Drop both, keep the technical reasoning for each assertion. Co-Authored-By: Claude Opus 4.8 --- test/integrated_search_textfield_test.dart | 4 ++-- test/search_list_test.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/integrated_search_textfield_test.dart b/test/integrated_search_textfield_test.dart index 54709529..aab15709 100644 --- a/test/integrated_search_textfield_test.dart +++ b/test/integrated_search_textfield_test.dart @@ -19,7 +19,7 @@ void main() { ), ); - // [REQ-177 rework, Vipin] find.byKey('search-trailing') can never fail here since that + // find.byKey('search-trailing') can never fail here since that // key is only ever attached by a caller-supplied trailing widget — assert the actual // structural invariant instead: the outer Padding's child is the Card itself, not a Row. final padding = tester.widget( @@ -44,7 +44,7 @@ void main() { expect(find.byKey(const Key('search-trailing')), findsOneWidget); expect(find.byType(TextField), findsOneWidget); - // [REQ-177 rework, Vipin] find.byType(Row) alone also matches the suffixIcon Row inside + // find.byType(Row) alone also matches the suffixIcon Row inside // the TextField's decoration, so it passes even if `trailing` were placed elsewhere. // Assert the actual wiring: the outer Padding's child is a Row whose children are // Expanded(searchCard) followed by the trailing widget itself. diff --git a/test/search_list_test.dart b/test/search_list_test.dart index 2a0deee5..ede9c47c 100644 --- a/test/search_list_test.dart +++ b/test/search_list_test.dart @@ -1,6 +1,6 @@ -// [REQ-177 rework, Vipin] SearchList.searchBarTrailing is plumbed to IntegratedSearchTextField +// SearchList.searchBarTrailing is plumbed to IntegratedSearchTextField // on both the SearchBarInBody and SearchWithAppBar code paths, but neither was pumped by a -// widget test — untested since #209 was opened. Covers both here. +// widget test. Covers both here. import 'package:fa_flutter_ui_kit/src/modules/common/search_list/search_list.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; From 3dc14880403db0a0e9c03d5ee991284623343425 Mon Sep 17 00:00:00 2001 From: Rahul Gulati Date: Thu, 17 Sep 2026 13:34:29 +0530 Subject: [PATCH 5/5] fix(search-list): drop unnecessary source comments [REQ-177] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the [REQ-177]-tagged explanatory comments on IntegratedSearchTextField.trailing, the Padding key, and the trailing-gap SizedBox per review — the code is self-explanatory without them. Co-Authored-By: Claude Opus 4.8 --- .../common/search_list/integrated_search_textfield.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/src/modules/common/search_list/integrated_search_textfield.dart b/lib/src/modules/common/search_list/integrated_search_textfield.dart index bf12a743..bdec5c54 100644 --- a/lib/src/modules/common/search_list/integrated_search_textfield.dart +++ b/lib/src/modules/common/search_list/integrated_search_textfield.dart @@ -37,8 +37,6 @@ class IntegratedSearchTextField extends StatefulWidget { final VoidCallback? onMicTap; final double borderRadius; - // [REQ-177] Optional widget rendered beside the search field (e.g. a filter button). - // Null by default, so every existing consumer's widget tree is unchanged. final Widget? trailing; // final SearchListBloc _searchListBloc=SearchListBloc(); @@ -142,9 +140,6 @@ class _IntegratedSearchTextFieldState extends State { ); return Padding( - // [REQ-177 rework] stable key so tests can target this Padding directly instead of - // relying on tree-order (find.byType(Padding).first also matches ancestor framework - // widgets in some hosts). key: const Key('integrated-search-textfield-padding'), padding: const EdgeInsets.all(8.0), child: widget.trailing == null @@ -152,7 +147,6 @@ class _IntegratedSearchTextFieldState extends State { : Row( children: [ Expanded(child: searchCard), - // [REQ-177 rework] gap so a trailing icon/button doesn't sit flush against the Card edge. const SizedBox(width: 8), widget.trailing!, ],