diff --git a/lib/features/connections/new_connection_dialog.dart b/lib/features/connections/new_connection_dialog.dart index 62b9bb61..6bd23e8c 100644 --- a/lib/features/connections/new_connection_dialog.dart +++ b/lib/features/connections/new_connection_dialog.dart @@ -92,8 +92,8 @@ class _NewConnectionDialogContentState extends material.State<_NewConnectionDial final mq = MediaQuery.sizeOf(context); final dialogMaxW = WindowLayout.newConnectionDialogMaxWidth(mq.width); final dialogH = WindowLayout.newConnectionDialogHeight(mq.height); - final sidebarW = WindowLayout.newConnectionSidebarWidth(dialogMaxW); final headerPadH = dialogMaxW < 420 ? 16.0 : 24.0; + final stackFilters = dialogMaxW < 520; return material.Container( width: dialogMaxW, @@ -145,74 +145,61 @@ class _NewConnectionDialogContentState extends material.State<_NewConnectionDial child: TextField( controller: _searchController, placeholder: const Text('Search...'), - onChanged: (v) => setState(() => _searchQuery = v), + onChanged: (v) => setState(() { + _searchQuery = v; + if (_selectedType != null && + !_filteredTypes.contains(_selectedType)) { + _selectedType = null; + } + }), ), ), ], ), ), + const material.SizedBox(height: 12), + _FilterDropdowns( + stackVertically: stackFilters, + category: _category, + selectedType: _selectedType, + filteredTypes: _filteredTypes, + onCategoryChanged: (category) { + setState(() { + _category = category; + if (_selectedType != null && + !_categoryTypes.contains(_selectedType)) { + _selectedType = null; + } + }); + }, + onTypeChanged: (type) => setState(() => _selectedType = type), + ), ], ), ), material.Expanded( - child: material.Row( - crossAxisAlignment: material.CrossAxisAlignment.stretch, - children: [ - material.Container( - width: sidebarW, - decoration: material.BoxDecoration( - border: material.Border( - right: material.BorderSide( - color: theme.border.withValues(alpha: 0.4), - ), - ), - ), - child: material.ListView( - padding: const material.EdgeInsets.symmetric(vertical: 8), - children: [ - _CategoryTile( - label: 'All', - icon: material.Icons.dns_rounded, - selected: _category == _Category.all, - onTap: () => setState(() => _category = _Category.all), - theme: theme, - ), - _CategoryTile( - label: 'SQL', - icon: material.Icons.table_chart_rounded, - selected: _category == _Category.sql, - onTap: () => setState(() => _category = _Category.sql), - theme: theme, - ), - _CategoryTile( - label: 'NoSQL', - icon: material.Icons.memory_rounded, - selected: _category == _Category.nosql, - onTap: () => setState(() => _category = _Category.nosql), - theme: theme, - ), - ], - ), - ), - material.Expanded( - child: material.LayoutBuilder( - builder: (context, constraints) { - const spacing = 12.0; - final gridPad = dialogMaxW < 420 ? 12.0 : 16.0; - final innerW = math.max(0.0, constraints.maxWidth - gridPad * 2); - final crossAxisCount = - WindowLayout.dbTypeGridCrossAxisCount(innerW); - final cardHeight = - WindowLayout.dbTypeCardHeight(crossAxisCount); - final cardWidth = crossAxisCount > 0 - ? (innerW - spacing * (crossAxisCount - 1)) / - crossAxisCount - : innerW; - final aspect = - cardHeight > 0 ? cardWidth / cardHeight : 1.0; - return material.Padding( - padding: material.EdgeInsets.all(gridPad), - child: material.GridView.count( + child: material.LayoutBuilder( + builder: (context, constraints) { + const spacing = 12.0; + final gridPad = dialogMaxW < 420 ? 12.0 : 16.0; + final innerW = math.max(0.0, constraints.maxWidth - gridPad * 2); + final crossAxisCount = + WindowLayout.dbTypeGridCrossAxisCount(innerW); + final cardHeight = + WindowLayout.dbTypeCardHeight(crossAxisCount); + final cardWidth = crossAxisCount > 0 + ? (innerW - spacing * (crossAxisCount - 1)) / crossAxisCount + : innerW; + final aspect = cardHeight > 0 ? cardWidth / cardHeight : 1.0; + return material.Padding( + padding: material.EdgeInsets.all(gridPad), + child: _filteredTypes.isEmpty + ? material.Center( + child: const Text('No databases match your search.') + .muted() + .small(), + ) + : material.GridView.count( crossAxisCount: crossAxisCount, mainAxisSpacing: spacing, crossAxisSpacing: spacing, @@ -229,11 +216,8 @@ class _NewConnectionDialogContentState extends material.State<_NewConnectionDial ), ], ), - ); - }, - ), - ), - ], + ); + }, ), ), material.Container( @@ -271,38 +255,100 @@ class _NewConnectionDialogContentState extends material.State<_NewConnectionDial } } -class _CategoryTile extends StatelessWidget { - const _CategoryTile({ - required this.label, - required this.icon, - required this.selected, - required this.onTap, - required this.theme, +class _FilterDropdowns extends StatelessWidget { + const _FilterDropdowns({ + required this.stackVertically, + required this.category, + required this.selectedType, + required this.filteredTypes, + required this.onCategoryChanged, + required this.onTypeChanged, }); - final String label; - final material.IconData icon; - final bool selected; - final VoidCallback onTap; - final ColorScheme theme; + final bool stackVertically; + final _Category category; + final ConnectionType? selectedType; + final List filteredTypes; + final void Function(_Category category) onCategoryChanged; + final void Function(ConnectionType? type) onTypeChanged; + + static const _categoryItems = [ + QueryaDropdownItem( + value: _Category.all, + label: 'All databases', + leading: material.Icon(material.Icons.dns_rounded, size: 18), + ), + QueryaDropdownItem( + value: _Category.sql, + label: 'SQL', + leading: material.Icon(material.Icons.table_chart_rounded, size: 18), + ), + QueryaDropdownItem( + value: _Category.nosql, + label: 'NoSQL', + leading: material.Icon(material.Icons.memory_rounded, size: 18), + ), + ]; @override material.Widget build(material.BuildContext context) { - return material.Material( - color: selected ? theme.muted.withValues(alpha: 0.35) : material.Colors.transparent, - child: material.InkWell( - onTap: onTap, - child: material.Padding( - padding: const material.EdgeInsets.symmetric(horizontal: 12, vertical: 10), - child: material.Row( - children: [ - material.Icon(icon, size: 20, color: theme.mutedForeground), - const material.SizedBox(width: 14), - selected ? Text(label).semiBold().small() : Text(label).small(), - ], - ), + final categoryField = material.Column( + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + const Text('Category').small().muted(), + const material.SizedBox(height: 4), + QueryaDropdown<_Category>( + value: category, + expandToParent: true, + items: _categoryItems, + onSelected: (value) { + if (value != null) onCategoryChanged(value); + }, ), - ), + ], + ); + + final typeField = material.Column( + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + const Text('Database type').small().muted(), + const material.SizedBox(height: 4), + QueryaDropdown( + value: selectedType, + hint: filteredTypes.isEmpty ? 'No matches' : 'Select database…', + enabled: filteredTypes.isNotEmpty, + expandToParent: true, + items: [ + for (final type in filteredTypes) + QueryaDropdownItem( + value: type, + label: type.label, + leading: material.Icon(type.icon, size: 18), + ), + ], + onSelected: onTypeChanged, + ), + ], + ); + + if (stackVertically) { + return material.Column( + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + categoryField, + const material.SizedBox(height: 10), + typeField, + ], + ); + } + + return material.Row( + crossAxisAlignment: material.CrossAxisAlignment.start, + children: [ + material.Expanded(child: categoryField), + const material.SizedBox(width: 12), + material.Expanded(child: typeField), + ], ); } } diff --git a/lib/features/mongodb/mongo_stats_view.dart b/lib/features/mongodb/mongo_stats_view.dart index 0a029b5e..4b7be2e6 100644 --- a/lib/features/mongodb/mongo_stats_view.dart +++ b/lib/features/mongodb/mongo_stats_view.dart @@ -333,34 +333,17 @@ class _MongoStatsViewState extends material.State { ), material.Padding( padding: const material.EdgeInsets.symmetric(horizontal: 4), - child: material.DropdownButton( + child: QueryaDropdown( + width: 132, value: _autoRefreshInterval, - underline: const material.SizedBox.shrink(), - isDense: true, - borderRadius: material.BorderRadius.circular(8), items: const [ - material.DropdownMenuItem( - value: null, - child: material.Text('Auto: off'), - ), - material.DropdownMenuItem( - value: Duration(seconds: 3), - child: material.Text('Auto: 3 s'), - ), - material.DropdownMenuItem( - value: Duration(seconds: 10), - child: material.Text('Auto: 10 s'), - ), - material.DropdownMenuItem( - value: Duration(seconds: 30), - child: material.Text('Auto: 30 s'), - ), - material.DropdownMenuItem( - value: Duration(seconds: 60), - child: material.Text('Auto: 60 s'), - ), + QueryaDropdownItem(value: null, label: 'Auto: off'), + QueryaDropdownItem(value: Duration(seconds: 3), label: 'Auto: 3 s'), + QueryaDropdownItem(value: Duration(seconds: 10), label: 'Auto: 10 s'), + QueryaDropdownItem(value: Duration(seconds: 30), label: 'Auto: 30 s'), + QueryaDropdownItem(value: Duration(seconds: 60), label: 'Auto: 60 s'), ], - onChanged: (value) { + onSelected: (value) { setState(() => _autoRefreshInterval = value); _startTimer(); }, diff --git a/lib/features/settings/preferences_controls.dart b/lib/features/settings/preferences_controls.dart index 0029c287..f5b40850 100644 --- a/lib/features/settings/preferences_controls.dart +++ b/lib/features/settings/preferences_controls.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart' as material; +import 'package:querya_desktop/shared/widgets/querya_dropdown.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; /// Helper / hint copy in Preferences — readable on imported themes. @@ -21,7 +22,7 @@ class PreferencesHint extends StatelessWidget { } } -/// Material 3 dropdown anchored to the field (stable inside scroll views). +/// Preferences dropdown backed by [QueryaDropdown] ([MenuAnchor]). class PreferencesDropdownMenu extends StatelessWidget { const PreferencesDropdownMenu({ super.key, @@ -35,7 +36,7 @@ class PreferencesDropdownMenu extends StatelessWidget { final T value; final List> entries; - final ValueChanged onSelected; + final material.ValueChanged onSelected; /// Fixed width for field + menu. Do not pass [double.infinity] — menu glitches. final double? width; @@ -46,46 +47,22 @@ class PreferencesDropdownMenu extends StatelessWidget { @override material.Widget build(material.BuildContext context) { - final cs = Theme.of(context).colorScheme; - final mTheme = material.Theme.of(context); - final textStyle = material.TextStyle( - color: cs.popoverForeground, - fontSize: 14, - ); - - return material.Theme( - data: mTheme.copyWith( - canvasColor: cs.popover, - colorScheme: mTheme.colorScheme.copyWith( - surface: cs.popover, - onSurface: cs.popoverForeground, - ), - ), - child: material.DropdownMenu( - enabled: enabled, - width: width, - expandedInsets: - expandToParent ? material.EdgeInsets.zero : null, - initialSelection: value, - onSelected: enabled ? onSelected : null, - dropdownMenuEntries: entries, - textStyle: textStyle, - inputDecorationTheme: material.InputDecorationTheme( - isDense: true, - contentPadding: const material.EdgeInsets.symmetric(vertical: 6), - enabledBorder: material.UnderlineInputBorder( - borderSide: material.BorderSide(color: cs.border), - ), - focusedBorder: material.UnderlineInputBorder( - borderSide: material.BorderSide(color: cs.ring, width: 2), - ), - ), - menuStyle: material.MenuStyle( - backgroundColor: material.WidgetStatePropertyAll(cs.popover), - surfaceTintColor: material.WidgetStatePropertyAll(cs.popover), - elevation: const material.WidgetStatePropertyAll(8), + final items = [ + for (final entry in entries) + QueryaDropdownItem( + value: entry.value, + label: entry.label, + enabled: entry.enabled, ), - ), + ]; + + return QueryaDropdown( + value: value, + items: items, + enabled: enabled, + width: width, + expandToParent: expandToParent, + onSelected: onSelected, ); } } diff --git a/lib/features/settings/sql_statement_timeout_dropdown.dart b/lib/features/settings/sql_statement_timeout_dropdown.dart index fa0848f0..4a664a20 100644 --- a/lib/features/settings/sql_statement_timeout_dropdown.dart +++ b/lib/features/settings/sql_statement_timeout_dropdown.dart @@ -1,12 +1,20 @@ import 'package:flutter/material.dart' as material; -import 'package:querya_desktop/features/settings/preferences_controls.dart'; +import 'package:querya_desktop/shared/widgets/querya_dropdown.dart'; /// Shared dropdown values for SQL statement timeouts (PostgreSQL / MySQL). +const List> kSqlStatementTimeoutMenuItems = [ + QueryaDropdownItem(value: null, label: 'No limit'), + QueryaDropdownItem(value: 10, label: '10 s'), + QueryaDropdownItem(value: 30, label: '30 s'), + QueryaDropdownItem(value: 60, label: '60 s'), + QueryaDropdownItem(value: 120, label: '2 min'), + QueryaDropdownItem(value: 300, label: '5 min'), + QueryaDropdownItem(value: 600, label: '10 min'), +]; + +/// Legacy alias for tests and call sites that still reference menu entries. const List> kSqlStatementTimeoutMenuEntries = [ - material.DropdownMenuEntry( - value: null, - label: 'No limit', - ), + material.DropdownMenuEntry(value: null, label: 'No limit'), material.DropdownMenuEntry(value: 10, label: '10 s'), material.DropdownMenuEntry(value: 30, label: '30 s'), material.DropdownMenuEntry(value: 60, label: '60 s'), @@ -30,11 +38,11 @@ class SqlStatementTimeoutDropdown extends material.StatelessWidget { @override material.Widget build(material.BuildContext context) { - return PreferencesDropdownMenu( + return QueryaDropdown( value: value, enabled: enabled, onSelected: onChanged, - entries: kSqlStatementTimeoutMenuEntries, + items: kSqlStatementTimeoutMenuItems, ); } } diff --git a/lib/shared/widgets/querya_dropdown.dart b/lib/shared/widgets/querya_dropdown.dart new file mode 100644 index 00000000..d6723e79 --- /dev/null +++ b/lib/shared/widgets/querya_dropdown.dart @@ -0,0 +1,210 @@ +import 'package:flutter/material.dart' as material; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// One selectable row in [QueryaDropdown]. +class QueryaDropdownItem { + const QueryaDropdownItem({ + required this.value, + required this.label, + this.enabled = true, + this.leading, + }); + + final T value; + final String label; + final bool enabled; + final material.Widget? leading; +} + +/// Stable dropdown built on [material.MenuAnchor] (no Overlay portal). +/// +/// Styling uses shadcn [ColorScheme] tokens from [Theme.of]. +class QueryaDropdown extends material.StatefulWidget { + const QueryaDropdown({ + super.key, + required this.value, + required this.items, + required this.onSelected, + this.controller, + this.enabled = true, + this.width, + this.expandToParent = false, + this.alignmentOffset = const material.Offset(0, 6), + this.menuMaxHeight = 320, + this.hint, + }); + + final T value; + final List> items; + final material.ValueChanged onSelected; + final material.MenuController? controller; + final bool enabled; + final double? width; + final bool expandToParent; + final material.Offset alignmentOffset; + final double menuMaxHeight; + final String? hint; + + @override + material.State> createState() => _QueryaDropdownState(); +} + +class _QueryaDropdownState extends material.State> { + late material.MenuController _controller; + + @override + void initState() { + super.initState(); + _controller = widget.controller ?? material.MenuController(); + } + + @override + void didUpdateWidget(covariant QueryaDropdown oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.controller != oldWidget.controller) { + _controller = widget.controller ?? material.MenuController(); + } + } + + material.Widget _triggerLabel({ + required String label, + required ColorScheme cs, + required bool expand, + }) { + final text = material.Text( + label, + maxLines: 1, + overflow: material.TextOverflow.ellipsis, + style: material.TextStyle( + fontSize: 14, + color: widget.enabled ? cs.popoverForeground : cs.mutedForeground, + ), + ); + if (expand) { + return material.Expanded(child: text); + } + return text; + } + + String _labelFor(T value) { + for (final item in widget.items) { + if (item.value == value) return item.label; + } + return widget.hint ?? ''; + } + + material.Widget _menuItem(QueryaDropdownItem item, ColorScheme cs) { + final selected = item.value == widget.value; + return material.MenuItemButton( + style: material.MenuItemButton.styleFrom( + minimumSize: const material.Size(180, 36), + padding: const material.EdgeInsets.symmetric(horizontal: 12), + backgroundColor: selected ? cs.muted.withValues(alpha: 0.45) : null, + foregroundColor: cs.popoverForeground, + disabledForegroundColor: cs.mutedForeground.withValues(alpha: 0.5), + shape: material.RoundedRectangleBorder( + borderRadius: material.BorderRadius.circular(6), + ), + ), + onPressed: !widget.enabled || !item.enabled + ? null + : () { + widget.onSelected(item.value); + _controller.close(); + }, + leadingIcon: item.leading, + child: material.Text( + item.label, + style: material.TextStyle( + fontSize: 14, + fontWeight: selected ? material.FontWeight.w600 : material.FontWeight.w400, + ), + ), + ); + } + + @override + material.Widget build(material.BuildContext context) { + final cs = Theme.of(context).colorScheme; + final radius = Theme.of(context).radiusMd; + final label = _labelFor(widget.value); + final fieldWidth = widget.expandToParent ? null : widget.width; + + final menuChildren = widget.items.map((item) => _menuItem(item, cs)).toList(); + + final anchor = material.MenuAnchor( + controller: _controller, + alignmentOffset: widget.alignmentOffset, + consumeOutsideTap: true, + style: material.MenuStyle( + backgroundColor: material.WidgetStatePropertyAll(cs.popover), + surfaceTintColor: material.WidgetStatePropertyAll(cs.popover), + elevation: const material.WidgetStatePropertyAll(8), + maximumSize: material.WidgetStatePropertyAll( + material.Size(double.infinity, widget.menuMaxHeight), + ), + padding: const material.WidgetStatePropertyAll( + material.EdgeInsets.symmetric(vertical: 4, horizontal: 4), + ), + shape: material.WidgetStatePropertyAll( + material.RoundedRectangleBorder( + borderRadius: material.BorderRadius.circular(radius), + side: material.BorderSide(color: cs.border), + ), + ), + ), + menuChildren: menuChildren, + builder: (context, controller, child) { + final field = material.InkWell( + onTap: widget.enabled + ? () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + } + : null, + borderRadius: material.BorderRadius.circular(6), + child: material.Container( + width: fieldWidth, + padding: const material.EdgeInsets.symmetric(horizontal: 4, vertical: 6), + decoration: material.BoxDecoration( + border: material.Border( + bottom: material.BorderSide( + color: widget.enabled ? cs.border : cs.border.withValues(alpha: 0.4), + ), + ), + ), + child: material.Row( + mainAxisSize: + widget.expandToParent ? material.MainAxisSize.max : material.MainAxisSize.min, + children: [ + _triggerLabel( + label: label, + cs: cs, + expand: widget.expandToParent, + ), + material.Icon( + material.Icons.arrow_drop_down_rounded, + size: 22, + color: widget.enabled ? cs.mutedForeground : cs.mutedForeground.withValues(alpha: 0.5), + ), + ], + ), + ), + ); + + if (widget.expandToParent) { + return material.SizedBox(width: double.infinity, child: field); + } + return field; + }, + ); + + if (fieldWidth != null && !widget.expandToParent) { + return material.SizedBox(width: fieldWidth, child: anchor); + } + return anchor; + } +} diff --git a/lib/shared/widgets/widgets.dart b/lib/shared/widgets/widgets.dart index 1cd9ec8c..12750aeb 100644 --- a/lib/shared/widgets/widgets.dart +++ b/lib/shared/widgets/widgets.dart @@ -9,4 +9,5 @@ library; export 'app_dialog.dart'; +export 'querya_dropdown.dart'; export 'package:shadcn_flutter/shadcn_flutter.dart'; diff --git a/test/features/settings/sql_statement_timeout_dropdown_test.dart b/test/features/settings/sql_statement_timeout_dropdown_test.dart index cde436bb..cdd6a208 100644 --- a/test/features/settings/sql_statement_timeout_dropdown_test.dart +++ b/test/features/settings/sql_statement_timeout_dropdown_test.dart @@ -4,17 +4,16 @@ import 'package:querya_desktop/features/settings/sql_statement_timeout_dropdown. import '../../support/querya_theme_test_shell.dart'; void main() { - group('kSqlStatementTimeoutMenuEntries', () { + group('kSqlStatementTimeoutMenuItems', () { test('has seven entries with expected values', () { - expect(kSqlStatementTimeoutMenuEntries.length, 7); - final values = - kSqlStatementTimeoutMenuEntries.map((e) => e.value).toList(); + expect(kSqlStatementTimeoutMenuItems.length, 7); + final values = kSqlStatementTimeoutMenuItems.map((e) => e.value).toList(); expect(values, [null, 10, 30, 60, 120, 300, 600]); }); }); group('SqlStatementTimeoutDropdown', () { - testWidgets('builds DropdownMenu with current value', (tester) async { + testWidgets('builds MenuAnchor with current value', (tester) async { await tester.pumpWidget( queryaThemeTestShell( child: material.Scaffold( @@ -27,15 +26,11 @@ void main() { ); await tester.pump(); - expect(find.byType(material.DropdownMenu), findsOneWidget); - final menu = tester.widget>( - find.byType(material.DropdownMenu), - ); - expect(menu.initialSelection, 60); - expect(menu.onSelected, isNotNull); + expect(find.byType(material.MenuAnchor), findsOneWidget); + expect(find.text('60 s'), findsOneWidget); }); - testWidgets('disables changes when enabled is false', (tester) async { + testWidgets('disables menu when enabled is false', (tester) async { await tester.pumpWidget( queryaThemeTestShell( child: material.Scaffold( @@ -49,10 +44,10 @@ void main() { ); await tester.pump(); - final menu = tester.widget>( - find.byType(material.DropdownMenu), - ); - expect(menu.enabled, isFalse); + await tester.tap(find.text('30 s')); + await tester.pumpAndSettle(); + + expect(find.text('10 s'), findsNothing); }); }); } diff --git a/test/shared/querya_dropdown_test.dart b/test/shared/querya_dropdown_test.dart new file mode 100644 index 00000000..0f1a9c58 --- /dev/null +++ b/test/shared/querya_dropdown_test.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart' as material; +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/shared/widgets/querya_dropdown.dart'; + +import '../support/querya_theme_test_shell.dart'; + +void main() { + group('QueryaDropdown', () { + testWidgets('builds MenuAnchor with current label', (tester) async { + await tester.pumpWidget( + queryaThemeTestShell( + child: material.Scaffold( + body: QueryaDropdown( + value: 'b', + items: const [ + QueryaDropdownItem(value: 'a', label: 'Alpha'), + QueryaDropdownItem(value: 'b', label: 'Beta'), + ], + onSelected: (_) {}, + ), + ), + ), + ); + await tester.pump(); + + expect(find.byType(material.MenuAnchor), findsOneWidget); + expect(find.text('Beta'), findsOneWidget); + }); + + testWidgets('opens menu and reports selection', (tester) async { + int? picked; + await tester.pumpWidget( + queryaThemeTestShell( + child: material.Scaffold( + body: QueryaDropdown( + value: 1, + items: const [ + QueryaDropdownItem(value: 1, label: 'One'), + QueryaDropdownItem(value: 2, label: 'Two'), + ], + onSelected: (v) => picked = v, + ), + ), + ), + ); + await tester.pump(); + + await tester.tap(find.text('One')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Two')); + await tester.pumpAndSettle(); + + expect(picked, 2); + }); + + testWidgets('shows hint when value is null', (tester) async { + await tester.pumpWidget( + queryaThemeTestShell( + child: material.Scaffold( + body: QueryaDropdown( + value: null, + hint: 'Pick one', + items: const [ + QueryaDropdownItem(value: 'x', label: 'X'), + ], + onSelected: (_) {}, + ), + ), + ), + ); + await tester.pump(); + + expect(find.text('Pick one'), findsOneWidget); + }); + }); +}