From 229d0fcac47363b1ee8d0a227b79d77d52e259e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Jul 2026 13:28:18 +0000 Subject: [PATCH 1/3] refactor(widgets): share festival status badge and sheet handle Fold FestivalCard._buildStatusBadge into FestivalStatusBadge behind a compact flag so both surfaces keep their current labels and geometry from one source of truth. Promote the filter sheets' private _SheetHandle to a shared SheetHandle and use it for the three hand-rolled festival/settings/theme sheet handles. Fixes #499 --- lib/widgets/drink_filter_sheets.dart | 28 ++----- lib/widgets/festival_header.dart | 66 +++++++++++---- lib/widgets/festival_menu_sheets.dart | 93 ++------------------- lib/widgets/sheet_handle.dart | 30 +++++++ lib/widgets/widgets.dart | 1 + test/widgets/festival_header_test.dart | 51 +++++++++-- test/widgets/festival_menu_sheets_test.dart | 3 + test/widgets/sheet_handle_test.dart | 68 +++++++++++++++ 8 files changed, 206 insertions(+), 134 deletions(-) create mode 100644 lib/widgets/sheet_handle.dart create mode 100644 test/widgets/sheet_handle_test.dart diff --git a/lib/widgets/drink_filter_sheets.dart b/lib/widgets/drink_filter_sheets.dart index 2204214a..84b3dae0 100644 --- a/lib/widgets/drink_filter_sheets.dart +++ b/lib/widgets/drink_filter_sheets.dart @@ -3,6 +3,7 @@ import 'package:provider/provider.dart'; import '../domain/models/models.dart'; import '../providers/providers.dart'; import '../utils/utils.dart'; +import 'sheet_handle.dart'; /// Shows the category filter as a modal bottom sheet. void showCategoryFilter(BuildContext context) { @@ -32,25 +33,6 @@ void _showSheet(BuildContext context, WidgetBuilder builder) { ); } -/// Drag handle shown at the top of every filter sheet. -class _SheetHandle extends StatelessWidget { - const _SheetHandle(); - - @override - Widget build(BuildContext context) { - return Center( - child: Container( - width: 32, - height: 4, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onSurfaceVariant, - borderRadius: BorderRadius.circular(2), - ), - ), - ); - } -} - /// Single-select category filter sheet. class CategoryFilterSheet extends StatelessWidget { final BeerProvider provider; @@ -72,7 +54,7 @@ class CategoryFilterSheet extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const _SheetHandle(), + const SheetHandle(), const SizedBox(height: 16), Text('Filter by Category', style: theme.textTheme.titleLarge), const SizedBox(height: 16), @@ -152,7 +134,7 @@ class SortOptionsSheet extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const _SheetHandle(), + const SheetHandle(), const SizedBox(height: 16), Text('Sort By', style: theme.textTheme.titleLarge), const SizedBox(height: 16), @@ -220,7 +202,7 @@ class StyleFilterSheet extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const _SheetHandle(), + const SheetHandle(), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -341,7 +323,7 @@ class VisibilityFilterSheet extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const _SheetHandle(), + const SheetHandle(), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/widgets/festival_header.dart b/lib/widgets/festival_header.dart index fa50493f..70b481ac 100644 --- a/lib/widgets/festival_header.dart +++ b/lib/widgets/festival_header.dart @@ -58,7 +58,7 @@ class FestivalHeader extends StatelessWidget { ), ), const SizedBox(width: 8), - FestivalStatusBadge(status: status), + FestivalStatusBadge(status: status, compact: true), ], ), ], @@ -70,29 +70,46 @@ class FestivalHeader extends StatelessWidget { } } -/// Small coloured pill summarising a festival's [FestivalStatus] -/// (LIVE / SOON / RECENT / PAST). Colours adapt to light and dark themes. +/// Small coloured pill summarising a festival's [FestivalStatus]. +/// +/// [compact] selects both the label wording and the geometry: +/// - `compact: true` (app-bar header) — short labels (LIVE / SOON / RECENT / +/// PAST), tighter padding, smaller radius and font. +/// - `compact: false` (default, festival browser cards) — long labels +/// (LIVE / COMING SOON / MOST RECENT / PAST), roomier padding, larger +/// radius and font. +/// +/// Colours adapt to light and dark themes and are identical for both modes. class FestivalStatusBadge extends StatelessWidget { - const FestivalStatusBadge({required this.status, super.key}); + const FestivalStatusBadge({ + required this.status, + this.compact = false, + super.key, + }); final FestivalStatus status; + final bool compact; @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; - final (badgeLabel, _, lightColor, darkColor) = _styleFor(status); + final (compactLabel, longLabel, _, lightColor, darkColor) = _styleFor( + status, + ); return Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), + padding: compact + ? const EdgeInsets.symmetric(horizontal: 6, vertical: 1) + : const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: isDark ? darkColor : lightColor, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(compact ? 8 : 12), ), child: Text( - badgeLabel, - style: const TextStyle( + compact ? compactLabel : longLabel, + style: TextStyle( color: Colors.white, - fontSize: 9, + fontSize: compact ? 9 : 10, fontWeight: FontWeight.bold, ), ), @@ -101,17 +118,27 @@ class FestivalStatusBadge extends StatelessWidget { /// Spoken form of the status for screen-reader labels (the badge text is /// terse and is excluded from semantics at the parent level). - static String spokenLabel(FestivalStatus status) => _styleFor(status).$2; + static String spokenLabel(FestivalStatus status) => _styleFor(status).$3; - /// Returns the badge label, spoken label, and (light, dark) background - /// colours for [status]. Single source of truth for all status styling. - static (String, String, Color, Color) _styleFor(FestivalStatus status) { + /// Returns the compact label, long label, spoken label, and (light, dark) + /// background colours for [status]. Single source of truth for all status + /// styling, shared by the app-bar header and the festival browser cards. + static (String, String, String, Color, Color) _styleFor( + FestivalStatus status, + ) { switch (status) { case FestivalStatus.live: - return const ('LIVE', 'live now', Color(0xFF2E7D32), Color(0xFF4CAF50)); + return const ( + 'LIVE', + 'LIVE', + 'live now', + Color(0xFF2E7D32), + Color(0xFF4CAF50), + ); case FestivalStatus.upcoming: return const ( 'SOON', + 'COMING SOON', 'starting soon', Color(0xFF1976D2), Color(0xFF42A5F5), @@ -119,12 +146,19 @@ class FestivalStatusBadge extends StatelessWidget { case FestivalStatus.mostRecent: return const ( 'RECENT', + 'MOST RECENT', 'most recent', Color(0xFFEF6C00), Color(0xFFFF9800), ); case FestivalStatus.past: - return const ('PAST', 'past', Color(0xFF616161), Color(0xFF9E9E9E)); + return const ( + 'PAST', + 'PAST', + 'past', + Color(0xFF616161), + Color(0xFF9E9E9E), + ); } } } diff --git a/lib/widgets/festival_menu_sheets.dart b/lib/widgets/festival_menu_sheets.dart index 6c52f885..afb77c60 100644 --- a/lib/widgets/festival_menu_sheets.dart +++ b/lib/widgets/festival_menu_sheets.dart @@ -4,6 +4,8 @@ import 'package:provider/provider.dart'; import '../models/models.dart'; import '../providers/providers.dart'; import '../utils/utils.dart'; +import 'festival_header.dart'; +import 'sheet_handle.dart'; /// Shows the festival browser/selector as a modal bottom sheet void showFestivalBrowser(BuildContext context) { @@ -89,17 +91,7 @@ class FestivalSelectorSheet extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Center( - child: Container( - key: const Key('festival_selector_drag_handle'), - width: 32, - height: 4, - decoration: BoxDecoration( - color: theme.colorScheme.onSurfaceVariant, - borderRadius: BorderRadius.circular(2), - ), - ), - ), + const SheetHandle(key: Key('festival_selector_drag_handle')), const SizedBox(height: 16), Row( children: [ @@ -300,7 +292,8 @@ class FestivalCard extends StatelessWidget { children: [ Row( children: [ - _buildStatusBadge(status), + FestivalStatusBadge(status: status), + const SizedBox(width: 8), Expanded( child: Text( festival.name, @@ -422,58 +415,6 @@ class FestivalCard extends StatelessWidget { ), ); } - - Widget _buildStatusBadge(FestivalStatus status) { - return Builder( - builder: (context) { - final theme = Theme.of(context); - final isDark = theme.brightness == Brightness.dark; - - Color backgroundColor; - String label; - - switch (status) { - case FestivalStatus.live: - backgroundColor = isDark - ? const Color(0xFF4CAF50) - : const Color(0xFF2E7D32); - label = 'LIVE'; - case FestivalStatus.upcoming: - backgroundColor = isDark - ? const Color(0xFF42A5F5) - : const Color(0xFF1976D2); - label = 'COMING SOON'; - case FestivalStatus.mostRecent: - backgroundColor = isDark - ? const Color(0xFFFF9800) - : const Color(0xFFEF6C00); - label = 'MOST RECENT'; - case FestivalStatus.past: - backgroundColor = isDark - ? const Color(0xFF9E9E9E) - : const Color(0xFF616161); - label = 'PAST'; - } - - return Container( - margin: const EdgeInsets.only(right: 8), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: BorderRadius.circular(12), - ), - child: Text( - label, - style: const TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ); - }, - ); - } } /// Settings bottom sheet with theme selector @@ -508,17 +449,7 @@ class SettingsSheet extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Center( - child: Container( - key: const Key('settings_sheet_drag_handle'), - width: 32, - height: 4, - decoration: BoxDecoration( - color: theme.colorScheme.onSurfaceVariant, - borderRadius: BorderRadius.circular(2), - ), - ), - ), + const SheetHandle(key: Key('settings_sheet_drag_handle')), const SizedBox(height: 16), Text('Settings', style: theme.textTheme.titleLarge), const SizedBox(height: 16), @@ -569,17 +500,7 @@ class ThemeSelectorSheet extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Center( - child: Container( - key: const Key('theme_selector_sheet_drag_handle'), - width: 32, - height: 4, - decoration: BoxDecoration( - color: theme.colorScheme.onSurfaceVariant, - borderRadius: BorderRadius.circular(2), - ), - ), - ), + const SheetHandle(key: Key('theme_selector_sheet_drag_handle')), const SizedBox(height: 16), Text('Theme', style: theme.textTheme.titleLarge), const SizedBox(height: 16), diff --git a/lib/widgets/sheet_handle.dart b/lib/widgets/sheet_handle.dart new file mode 100644 index 00000000..6818f8bf --- /dev/null +++ b/lib/widgets/sheet_handle.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +/// Drag handle shown at the top of every modal bottom sheet in this app +/// (filter sheets, festival selector, settings, theme selector). +/// +/// The key passed to this widget is applied to the inner [Container] rather +/// than to [SheetHandle] itself, so callers that key the drag handle for +/// tests (`SheetHandle(key: Key('some_drag_handle'))`) can keep finding a +/// [Container] with that key — matching the pre-refactor widget tree where +/// the key lived directly on the hand-rolled `Container`. +class SheetHandle extends StatelessWidget { + const SheetHandle({Key? key}) : _handleKey = key, super(key: null); + + final Key? _handleKey; + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + key: _handleKey, + width: 32, + height: 4, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onSurfaceVariant, + borderRadius: BorderRadius.circular(2), + ), + ), + ); + } +} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 9269ebad..7e3ababe 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -18,6 +18,7 @@ export 'info_chip.dart'; export 'overflow_menu.dart'; export 'page_title.dart'; export 'section_header.dart'; +export 'sheet_handle.dart'; export 'star_rating.dart'; export 'style_hero_panel.dart'; export 'your_take_card.dart'; diff --git a/test/widgets/festival_header_test.dart b/test/widgets/festival_header_test.dart index 43ee2dec..c8647d2c 100644 --- a/test/widgets/festival_header_test.dart +++ b/test/widgets/festival_header_test.dart @@ -14,6 +14,7 @@ void main() { group('FestivalStatusBadge', () { Widget wrap( FestivalStatus status, { + bool? compact, Brightness brightness = Brightness.light, }) { // Wrap the badge in an explicit Theme so brightness is deterministic @@ -22,26 +23,58 @@ void main() { home: Theme( data: ThemeData(brightness: brightness), child: Scaffold( - body: Center(child: FestivalStatusBadge(status: status)), + body: Center( + child: compact == null + ? FestivalStatusBadge(status: status) + : FestivalStatusBadge(status: status, compact: compact), + ), ), ), ); } - const expectedLabels = { + const compactLabels = { FestivalStatus.live: 'LIVE', FestivalStatus.upcoming: 'SOON', FestivalStatus.mostRecent: 'RECENT', FestivalStatus.past: 'PAST', }; - for (final entry in expectedLabels.entries) { - testWidgets('renders ${entry.value} label for ${entry.key}', ( - tester, - ) async { - await tester.pumpWidget(wrap(entry.key)); - expect(find.text(entry.value), findsOneWidget); - }); + const longLabels = { + FestivalStatus.live: 'LIVE', + FestivalStatus.upcoming: 'COMING SOON', + FestivalStatus.mostRecent: 'MOST RECENT', + FestivalStatus.past: 'PAST', + }; + + for (final entry in compactLabels.entries) { + testWidgets( + 'compact: true renders ${entry.value} label for ${entry.key}', + (tester) async { + await tester.pumpWidget(wrap(entry.key, compact: true)); + expect(find.text(entry.value), findsOneWidget); + }, + ); + } + + for (final entry in longLabels.entries) { + testWidgets( + 'compact: false renders ${entry.value} label for ${entry.key}', + (tester) async { + await tester.pumpWidget(wrap(entry.key, compact: false)); + expect(find.text(entry.value), findsOneWidget); + }, + ); + } + + for (final entry in longLabels.entries) { + testWidgets( + 'defaults to the long label (compact not passed) for ${entry.key}', + (tester) async { + await tester.pumpWidget(wrap(entry.key)); + expect(find.text(entry.value), findsOneWidget); + }, + ); } testWidgets('badge colour adapts to light and dark themes', (tester) async { diff --git a/test/widgets/festival_menu_sheets_test.dart b/test/widgets/festival_menu_sheets_test.dart index ffd6350a..03cf8c89 100644 --- a/test/widgets/festival_menu_sheets_test.dart +++ b/test/widgets/festival_menu_sheets_test.dart @@ -604,6 +604,9 @@ void main() { ); expect(find.text('LIVE'), findsOneWidget); + // FestivalCard's badge is the shared FestivalStatusBadge (compact: + // false, the default), not a bespoke copy — regression guard for #499. + expect(find.byType(FestivalStatusBadge), findsOneWidget); }); testWidgets('shows COMING SOON badge for an upcoming festival', ( diff --git a/test/widgets/sheet_handle_test.dart b/test/widgets/sheet_handle_test.dart new file mode 100644 index 00000000..c46245a9 --- /dev/null +++ b/test/widgets/sheet_handle_test.dart @@ -0,0 +1,68 @@ +import 'package:cambridge_beer_festival/app_theme.dart'; +import 'package:cambridge_beer_festival/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('SheetHandle', () { + testWidgets('renders a 32x4 rounded container', (tester) async { + await tester.pumpWidget( + const MaterialApp(home: Scaffold(body: SheetHandle())), + ); + + final container = tester.widget(find.byType(Container)); + expect( + container.constraints, + const BoxConstraints.tightFor(width: 32, height: 4), + ); + + final decoration = container.decoration! as BoxDecoration; + expect(decoration.borderRadius, BorderRadius.circular(2)); + }); + + testWidgets('centres the handle in its available space', (tester) async { + await tester.pumpWidget( + const MaterialApp(home: Scaffold(body: SheetHandle())), + ); + + expect( + find.ancestor( + of: find.byType(Container), + matching: find.byType(Center), + ), + findsOneWidget, + ); + }); + + testWidgets('honours a passed key on the inner container', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: SheetHandle(key: Key('my_drag_handle'))), + ), + ); + + final container = tester.widget( + find.byKey(const Key('my_drag_handle')), + ); + expect( + container.constraints, + const BoxConstraints.tightFor(width: 32, height: 4), + ); + }); + + testWidgets('uses onSurfaceVariant from the current theme', (tester) async { + final lightTheme = buildAppTheme(Brightness.light); + + await tester.pumpWidget( + MaterialApp( + theme: lightTheme, + home: const Scaffold(body: SheetHandle()), + ), + ); + + final container = tester.widget(find.byType(Container)); + final decoration = container.decoration! as BoxDecoration; + expect(decoration.color, lightTheme.colorScheme.onSurfaceVariant); + }); + }); +} From 7b1cbd3b64d87835a034d3c5ff22fd5f43531b42 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Jul 2026 13:33:28 +0000 Subject: [PATCH 2/3] refactor(widgets): name SheetHandle's container key explicitly Replace the constructor that accepted `key` and forwarded it to the inner Container while passing null to super. Swallowing `key` made SheetHandle(key: k) silently unkeyed, which is surprising for a shared widget. An explicit `handleKey` keeps the three drag-handle test assertions matching a single Container and leaves `key` behaving normally. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017B31wKKL578bV18hcpXyE2 --- lib/widgets/festival_menu_sheets.dart | 6 +++--- lib/widgets/sheet_handle.dart | 17 +++++++++-------- test/widgets/sheet_handle_test.dart | 22 ++++++++++++++++++++-- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/lib/widgets/festival_menu_sheets.dart b/lib/widgets/festival_menu_sheets.dart index afb77c60..4418913b 100644 --- a/lib/widgets/festival_menu_sheets.dart +++ b/lib/widgets/festival_menu_sheets.dart @@ -91,7 +91,7 @@ class FestivalSelectorSheet extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SheetHandle(key: Key('festival_selector_drag_handle')), + const SheetHandle(handleKey: Key('festival_selector_drag_handle')), const SizedBox(height: 16), Row( children: [ @@ -449,7 +449,7 @@ class SettingsSheet extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SheetHandle(key: Key('settings_sheet_drag_handle')), + const SheetHandle(handleKey: Key('settings_sheet_drag_handle')), const SizedBox(height: 16), Text('Settings', style: theme.textTheme.titleLarge), const SizedBox(height: 16), @@ -500,7 +500,7 @@ class ThemeSelectorSheet extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SheetHandle(key: Key('theme_selector_sheet_drag_handle')), + const SheetHandle(handleKey: Key('theme_selector_sheet_drag_handle')), const SizedBox(height: 16), Text('Theme', style: theme.textTheme.titleLarge), const SizedBox(height: 16), diff --git a/lib/widgets/sheet_handle.dart b/lib/widgets/sheet_handle.dart index 6818f8bf..273d9c40 100644 --- a/lib/widgets/sheet_handle.dart +++ b/lib/widgets/sheet_handle.dart @@ -3,21 +3,22 @@ import 'package:flutter/material.dart'; /// Drag handle shown at the top of every modal bottom sheet in this app /// (filter sheets, festival selector, settings, theme selector). /// -/// The key passed to this widget is applied to the inner [Container] rather -/// than to [SheetHandle] itself, so callers that key the drag handle for -/// tests (`SheetHandle(key: Key('some_drag_handle'))`) can keep finding a -/// [Container] with that key — matching the pre-refactor widget tree where -/// the key lived directly on the hand-rolled `Container`. +/// [handleKey] is applied to the inner [Container] rather than to this widget, +/// so callers that key the drag handle for tests keep finding a [Container] +/// with that key — matching the pre-refactor widget tree, where the key lived +/// directly on the hand-rolled `Container`. Keying the widget itself via [key] +/// remains available and behaves normally. class SheetHandle extends StatelessWidget { - const SheetHandle({Key? key}) : _handleKey = key, super(key: null); + const SheetHandle({this.handleKey, super.key}); - final Key? _handleKey; + /// Key applied to the inner container, for tests locating the handle. + final Key? handleKey; @override Widget build(BuildContext context) { return Center( child: Container( - key: _handleKey, + key: handleKey, width: 32, height: 4, decoration: BoxDecoration( diff --git a/test/widgets/sheet_handle_test.dart b/test/widgets/sheet_handle_test.dart index c46245a9..7b2ca002 100644 --- a/test/widgets/sheet_handle_test.dart +++ b/test/widgets/sheet_handle_test.dart @@ -34,13 +34,17 @@ void main() { ); }); - testWidgets('honours a passed key on the inner container', (tester) async { + testWidgets('applies handleKey to the inner container', (tester) async { await tester.pumpWidget( const MaterialApp( - home: Scaffold(body: SheetHandle(key: Key('my_drag_handle'))), + home: Scaffold(body: SheetHandle(handleKey: Key('my_drag_handle'))), ), ); + // Exactly one match: the key lands on the Container only, never on the + // SheetHandle itself, so tester.widget() stays unambiguous. + expect(find.byKey(const Key('my_drag_handle')), findsOneWidget); + final container = tester.widget( find.byKey(const Key('my_drag_handle')), ); @@ -50,6 +54,20 @@ void main() { ); }); + testWidgets('keys the widget itself normally via key', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: SheetHandle(key: Key('handle_widget'))), + ), + ); + + expect(find.byKey(const Key('handle_widget')), findsOneWidget); + expect( + tester.widget(find.byKey(const Key('handle_widget'))), + isA(), + ); + }); + testWidgets('uses onSurfaceVariant from the current theme', (tester) async { final lightTheme = buildAppTheme(Brightness.light); From ae86ae6c2efd121f70985a3331f9a13fe1519862 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Jul 2026 13:50:33 +0000 Subject: [PATCH 3/3] test(widgets): scope SheetHandle container finders to its subtree find.byType(Container) matched the whole pumped tree, so the assertions would become ambiguous if the test scaffolding ever gained a Container of its own. Scope to SheetHandle's descendants and assert a single match before reading the widget. Addresses review feedback on #501. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017B31wKKL578bV18hcpXyE2 --- test/widgets/sheet_handle_test.dart | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/test/widgets/sheet_handle_test.dart b/test/widgets/sheet_handle_test.dart index 7b2ca002..010b2c67 100644 --- a/test/widgets/sheet_handle_test.dart +++ b/test/widgets/sheet_handle_test.dart @@ -5,12 +5,20 @@ import 'package:flutter_test/flutter_test.dart'; void main() { group('SheetHandle', () { + // Scoped to SheetHandle's own subtree so the finders stay unambiguous + // if the surrounding test scaffolding ever gains its own Containers. + final handleContainer = find.descendant( + of: find.byType(SheetHandle), + matching: find.byType(Container), + ); + testWidgets('renders a 32x4 rounded container', (tester) async { await tester.pumpWidget( const MaterialApp(home: Scaffold(body: SheetHandle())), ); - final container = tester.widget(find.byType(Container)); + expect(handleContainer, findsOneWidget); + final container = tester.widget(handleContainer); expect( container.constraints, const BoxConstraints.tightFor(width: 32, height: 4), @@ -26,10 +34,7 @@ void main() { ); expect( - find.ancestor( - of: find.byType(Container), - matching: find.byType(Center), - ), + find.ancestor(of: handleContainer, matching: find.byType(Center)), findsOneWidget, ); }); @@ -78,7 +83,8 @@ void main() { ), ); - final container = tester.widget(find.byType(Container)); + expect(handleContainer, findsOneWidget); + final container = tester.widget(handleContainer); final decoration = container.decoration! as BoxDecoration; expect(decoration.color, lightTheme.colorScheme.onSurfaceVariant); });