From 585dc1482942bd2c4a498b4724b24ff56f422746 Mon Sep 17 00:00:00 2001 From: Mike Kell Date: Fri, 10 Jul 2026 20:13:27 -0400 Subject: [PATCH] =?UTF-8?q?feat(mobile):=20Stage=206C=20=E2=80=94=20mobile?= =?UTF-8?q?=20inventory=20surface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add MobileInventoryPage with search bar, horizontal status filter chips (All / In Stock / Low Stock / Out of Stock / Draft), item count display, compact _MobileInventoryCard list at 120px height (name, SKU, status chip, quantity-on-hand), pull-to-refresh, and _detailPageActive guard to suppress behind-route SnackBars - Add MobileInventoryDetailPage wrapping shared InventoryDetailPanel; local controller listener for SnackBar feedback; showAdjustQuantityDialog on Adjust Quantity tap; haptic feedback (mediumImpact on result, lightImpact on successful adjust); SafeArea wrapper - Update MobileShell Inventory tab (case 1) from shared InventoryPage to MobileInventoryPage; remove unused feature_inventory direct import - Add 6 Stage 6C widget tests: Inventory tab search bar, status filter chips, item count, card navigation to detail, adjust quantity dialog, back navigation returning to list (26/26 kell_mobile total) All tests passing: 26/26 kell_mobile, 75/75 feature_inventory dart analyze --fatal-infos: no issues found Stage 6 complete — Products, Orders, and Inventory all have mobile surfaces --- docs/development/build_execution_tracker.md | 21 +- docs/development/master_development_brief.md | 10 +- .../pages/mobile_inventory_detail_page.dart | 115 +++++++ .../lib/pages/mobile_inventory_page.dart | 310 ++++++++++++++++++ .../kell_mobile/lib/shell/mobile_shell.dart | 4 +- .../apps/kell_mobile/test/widget_test.dart | 137 ++++++++ 6 files changed, 587 insertions(+), 10 deletions(-) create mode 100644 kell_creations_apps/apps/kell_mobile/lib/pages/mobile_inventory_detail_page.dart create mode 100644 kell_creations_apps/apps/kell_mobile/lib/pages/mobile_inventory_page.dart diff --git a/docs/development/build_execution_tracker.md b/docs/development/build_execution_tracker.md index 8cf7edf..7199f19 100644 --- a/docs/development/build_execution_tracker.md +++ b/docs/development/build_execution_tracker.md @@ -2,9 +2,9 @@ ## Current status -- main baseline updated through: feat/orders-mobile-surface (Stage 6B complete) -- next branch: feat/bulk-operator-workflows (Stage 7B) or feat/inventory-mobile-surface (Stage 6C) -- current stage: Stage 6B complete — mobile orders surface with MobileOrdersPage (search, filter chips, order count, pull-to-refresh), MobileOrderDetailPage (haptic feedback, confirmation dialogs, local SnackBar listener), and 6 new kell_mobile tests (20/20 total) +- main baseline updated through: feat/inventory-mobile-surface (Stage 6C complete) +- next branch: feat/bulk-operator-workflows (Stage 7B) +- current stage: Stage 6C complete — mobile inventory surface with MobileInventoryPage (search, filter chips, item count, pull-to-refresh, compact 120px cards), MobileInventoryDetailPage (haptic feedback, adjust quantity dialog, local SnackBar listener, SafeArea), shell updated from shared InventoryPage to MobileInventoryPage, and 6 new kell_mobile tests (26/26 total). Stage 6 complete. ## Slice tracker @@ -344,6 +344,21 @@ - analyze: passed (dart analyze --fatal-infos — no issues found) - brief updated: yes +### feat/inventory-mobile-surface + +- status: merged to main +- date: 2026-07-10 +- inspection: complete +- implementation: complete +- files changed: + - `kell_mobile/lib/pages/mobile_inventory_page.dart` — new `MobileInventoryPage` with search bar, horizontal status filter chips (All / In Stock / Low Stock / Out of Stock / Draft), item count display, compact `_MobileInventoryCard` list at 120px height showing name, SKU, status chip, and quantity-on-hand, pull-to-refresh, `_detailPageActive` guard to suppress behind-route SnackBars + - `kell_mobile/lib/pages/mobile_inventory_detail_page.dart` — new `MobileInventoryDetailPage` wrapping shared `InventoryDetailPanel`; StatefulWidget with local controller listener for SnackBar feedback; shows `showAdjustQuantityDialog` on Adjust Quantity tap; haptic feedback (mediumImpact on result, lightImpact on successful adjust); `SafeArea` wrapper + - `kell_mobile/lib/shell/mobile_shell.dart` — Inventory tab (case 1) switched from shared `InventoryPage` to `MobileInventoryPage`; removed unused `feature_inventory` direct import + - `kell_mobile/test/widget_test.dart` — added 6 new Stage 6C tests: Inventory tab search bar, status filter chips, item count, inventory card navigation to detail, adjust quantity dialog, back navigation returning to inventory list — 26 total kell_mobile tests +- tests: passed (26/26 kell_mobile, 75/75 feature_inventory — all passing) +- analyze: passed (dart analyze --fatal-infos — no issues found) +- brief updated: yes + ### chore/analyze-cleanup-and-tracker-sync - status: merged to main diff --git a/docs/development/master_development_brief.md b/docs/development/master_development_brief.md index a957421..5b29d14 100644 --- a/docs/development/master_development_brief.md +++ b/docs/development/master_development_brief.md @@ -137,9 +137,9 @@ Rules: - latest reported count for `feature_orders`: `115/115 passed` - latest reported count for `feature_wordpress`: `311/311 passed` - latest reported count for `kell_web`: `24/24 passed` -- latest reported count for `kell_mobile`: `20/20 passed` -- total: `606/606 passed` -- baseline commit: merge of `feat/orders-mobile-surface` (Stage 6B complete, 2026-07-10) +- latest reported count for `kell_mobile`: `26/26 passed` +- total: `612/612 passed` +- baseline commit: merge of `feat/inventory-mobile-surface` (Stage 6C complete, 2026-07-10) #### Baseline test coverage (established 2026-05-22) @@ -155,8 +155,8 @@ No minimum thresholds are enforced — this is visibility-only tracking. Coverag ### Next recommended branch -**`feat/inventory-mobile-surface`** — Stage 6C (inventory): Mobile inventory surface for Android (matching the pattern established by `feat/android-publishing-surface` and `feat/orders-mobile-surface`), or **`feat/bulk-operator-workflows`** — Stage 7B: Additional bulk actions (bulk publish, bulk move to pending review). -Branch from latest `main`. Stage 6B (orders mobile surface) is complete. The orders feature now has full write-capable UI on both web and mobile. Next logical step is either surfacing inventory on mobile or adding more bulk publishing actions. +**`feat/bulk-operator-workflows`** — Stage 7B: Additional bulk actions (bulk publish, bulk move to pending review). +Branch from latest `main`. Stage 6C (inventory mobile surface) is complete. All three primary features (Products, Orders, Inventory) now have mobile-optimized surfaces on Android. Stage 6 is complete. Next logical step is adding more bulk publishing actions. --- diff --git a/kell_creations_apps/apps/kell_mobile/lib/pages/mobile_inventory_detail_page.dart b/kell_creations_apps/apps/kell_mobile/lib/pages/mobile_inventory_detail_page.dart new file mode 100644 index 0000000..663e0da --- /dev/null +++ b/kell_creations_apps/apps/kell_mobile/lib/pages/mobile_inventory_detail_page.dart @@ -0,0 +1,115 @@ +import 'package:feature_inventory/feature_inventory.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// Full-screen inventory detail page for mobile. +/// +/// Receives the shared [InventoryController] so that quantity adjustments are +/// immediately reflected in the list when the user pops back. +/// +/// Wraps the shared [InventoryDetailPanel] from [feature_inventory] inside a +/// [Scaffold] with an [AppBar] showing the item name. +/// +/// Unlike the web layout where feedback SnackBars are handled by the inventory +/// page wrapper, this detail page is pushed via [Navigator] and owns its own +/// [Scaffold]. It therefore attaches its own listener to the controller and +/// shows action-result SnackBars using its local [BuildContext], ensuring +/// they are visible on the active screen. +/// +/// Quantity-adjust actions present a confirmation dialog before executing, +/// reducing accidental taps on touch screens. +class MobileInventoryDetailPage extends StatefulWidget { + final InventoryController controller; + + /// Optional callback to navigate to the Products page for a given SKU. + final void Function(String sku)? onViewProduct; + + const MobileInventoryDetailPage({super.key, required this.controller, this.onViewProduct}); + + @override + State createState() => _MobileInventoryDetailPageState(); +} + +class _MobileInventoryDetailPageState extends State { + InventoryController get _controller => widget.controller; + + @override + void initState() { + super.initState(); + _controller.addListener(_onControllerChanged); + } + + @override + void dispose() { + _controller.removeListener(_onControllerChanged); + super.dispose(); + } + + /// Handles action result feedback via SnackBars on this detail page. + /// + /// Because this page is pushed on top of the list page via [Navigator], + /// the list page's listener cannot reliably display SnackBars (they + /// would appear behind this route). This listener ensures feedback is + /// always visible to the operator. + void _onControllerChanged() { + if (!mounted) return; + + final result = _controller.lastActionResult; + if (result != null && !_controller.isUpdating) { + _controller.lastActionResult = null; + showInventoryActionSnackBar(context, result); + if (result.success) HapticFeedback.mediumImpact(); + } + } + + /// Handles the quantity adjustment flow. + /// + /// Shows the shared [showAdjustQuantityDialog] and, if confirmed, calls + /// [InventoryController.adjustQuantity] with haptic feedback on success. + Future _handleAdjustQuantity() async { + final item = _controller.selectedItem; + if (item == null) return; + + final dialogResult = await showAdjustQuantityDialog(context, item); + if (dialogResult == null) return; + + if (mounted) { + await _controller.adjustQuantity(item.id, dialogResult.delta, dialogResult.reason); + if (_controller.lastActionResult?.success == true) { + HapticFeedback.lightImpact(); + } + } + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller, + builder: (context, _) { + final item = _controller.selectedItem; + + // If the item was removed or deselected, pop back. + if (item == null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) Navigator.of(context).pop(); + }); + return const Scaffold(body: Center(child: CircularProgressIndicator())); + } + + return Scaffold( + appBar: AppBar(title: Text(item.name)), + body: SafeArea( + child: InventoryDetailPanel( + item: item, + isUpdating: _controller.isUpdating, + onAdjustQuantity: _handleAdjustQuantity, + onViewProduct: widget.onViewProduct != null + ? () => widget.onViewProduct!(item.sku) + : null, + ), + ), + ); + }, + ); + } +} diff --git a/kell_creations_apps/apps/kell_mobile/lib/pages/mobile_inventory_page.dart b/kell_creations_apps/apps/kell_mobile/lib/pages/mobile_inventory_page.dart new file mode 100644 index 0000000..397f570 --- /dev/null +++ b/kell_creations_apps/apps/kell_mobile/lib/pages/mobile_inventory_page.dart @@ -0,0 +1,310 @@ +import 'package:design_system/design_system.dart'; +import 'package:feature_inventory/feature_inventory.dart'; +import 'package:flutter/material.dart'; + +import 'mobile_inventory_detail_page.dart'; + +/// Mobile-optimized inventory workspace page. +/// +/// Provides browsing, filtering, and searching for inventory items on smaller +/// screens. Tapping an item pushes a full-screen detail page for viewing and +/// quantity adjustment. +/// +/// Reuses the shared [InventoryController] and all use cases from +/// [feature_inventory] — no business logic is forked for mobile. +class MobileInventoryPage extends StatefulWidget { + final InventoryRepository repository; + + /// Optional callback to navigate to the Products page for a given SKU. + final void Function(String sku)? onViewProduct; + + const MobileInventoryPage({super.key, required this.repository, this.onViewProduct}); + + @override + State createState() => _MobileInventoryPageState(); +} + +class _MobileInventoryPageState extends State { + late final InventoryController _controller; + late final TextEditingController _searchController; + + /// Whether the detail page is currently pushed on top. + /// + /// When `true`, action-result SnackBars are suppressed here because the + /// detail page owns its own listener and shows feedback in its own + /// [Scaffold]. Without this guard, SnackBars would be rendered behind + /// the detail route and be invisible to the operator. + bool _detailPageActive = false; + + @override + void initState() { + super.initState(); + _searchController = TextEditingController(); + + final repo = widget.repository; + _controller = InventoryController( + GetInventoryItems(repo), + AdjustInventoryQuantity(repo), + CreateInventoryItem(repo), + UpdateInventoryItem(repo), + ); + + _controller.addListener(_onControllerChanged); + _controller.load(); + } + + @override + void dispose() { + _controller.removeListener(_onControllerChanged); + _controller.dispose(); + _searchController.dispose(); + super.dispose(); + } + + /// Handles action result feedback via SnackBars. + /// + /// Suppressed while the detail page is active — the detail page has its + /// own listener that shows SnackBars using its local context. + void _onControllerChanged() { + if (_detailPageActive) return; + + final result = _controller.lastActionResult; + if (result != null && !_controller.isUpdating) { + _controller.lastActionResult = null; + showInventoryActionSnackBar(context, result); + } + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller, + builder: (context, _) { + if (_controller.isLoading) { + return const KcLoadingState(message: 'Loading inventory…'); + } + + if (_controller.error != null) { + return KcErrorState(message: 'Failed to load inventory.', onRetry: _controller.load); + } + + return Column( + children: [ + _buildSearchBar(), + _buildFilterChips(), + _buildItemCount(), + Expanded(child: _buildItemList()), + ], + ); + }, + ); + } + + /// Search bar with real-time filtering. + Widget _buildSearchBar() { + return Padding( + padding: const EdgeInsets.fromLTRB(KcSpacing.md, KcSpacing.sm, KcSpacing.md, KcSpacing.xs), + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: 'Search by name or SKU…', + prefixIcon: const Icon(Icons.search, size: 20), + suffixIcon: _searchController.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear, size: 20), + onPressed: () { + _searchController.clear(); + _controller.setSearchQuery(''); + }, + ) + : null, + isDense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: KcSpacing.sm, + vertical: KcSpacing.sm, + ), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + ), + onChanged: (query) { + _controller.setSearchQuery(query); + setState(() {}); // refresh clear button visibility + }, + ), + ); + } + + /// Horizontal scrollable filter chips for status filtering. + Widget _buildFilterChips() { + const filters = [ + (null, 'All'), + ('inStock', 'In Stock'), + ('lowStock', 'Low Stock'), + ('outOfStock', 'Out of Stock'), + ('draft', 'Draft'), + ]; + + return SizedBox( + height: 48, + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: KcSpacing.md), + itemCount: filters.length, + separatorBuilder: (_, _) => const SizedBox(width: KcSpacing.xs), + itemBuilder: (context, index) { + final (value, label) = filters[index]; + final isActive = _controller.activeFilter == value; + return FilterChip( + label: Text(label), + selected: isActive, + onSelected: (_) => _controller.setFilter(isActive ? null : value), + ); + }, + ), + ); + } + + /// Item count row. + Widget _buildItemCount() { + final count = _controller.items.length; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: KcSpacing.md, vertical: KcSpacing.xs), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + '$count ${count == 1 ? 'item' : 'items'}', + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: KcColors.neutral), + ), + ), + ); + } + + /// Scrollable inventory list using compact cards. + Widget _buildItemList() { + final items = _controller.items; + + if (items.isEmpty) { + return KcEmptyState( + icon: Icons.inventory_2_outlined, + message: _controller.searchQuery.isNotEmpty || _controller.activeFilter != null + ? 'No items match your criteria.' + : 'No inventory items available.', + ); + } + + return RefreshIndicator( + onRefresh: _controller.load, + child: ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: KcSpacing.md), + itemCount: items.length, + separatorBuilder: (_, _) => const SizedBox(height: KcSpacing.xs), + itemBuilder: (context, index) { + final item = items[index]; + return SizedBox( + height: 120, + child: _MobileInventoryCard( + item: item, + isSelected: item.id == _controller.selectedItem?.id, + onTap: () => _navigateToDetail(item), + ), + ); + }, + ), + ); + } + + /// Navigates to the full-screen inventory detail page. + /// + /// Sets [_detailPageActive] to suppress SnackBars on this page while the + /// detail page is visible. Cleared when the detail page pops back. + void _navigateToDetail(InventoryItem item) { + _controller.selectItem(item); + setState(() => _detailPageActive = true); + Navigator.of(context) + .push( + MaterialPageRoute( + builder: (_) => MobileInventoryDetailPage( + controller: _controller, + onViewProduct: widget.onViewProduct, + ), + ), + ) + .then((_) { + if (mounted) setState(() => _detailPageActive = false); + }); + } +} + +// ── Compact mobile inventory card ────────────────────────────────────────────── + +/// A compact card for displaying an [InventoryItem] in the mobile list. +/// +/// Shows name, SKU, status chip, and quantity at a glance. +class _MobileInventoryCard extends StatelessWidget { + final InventoryItem item; + final bool isSelected; + final VoidCallback onTap; + + const _MobileInventoryCard({required this.item, required this.isSelected, required this.onTap}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Card( + elevation: isSelected ? 3 : 1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: isSelected ? BorderSide(color: theme.colorScheme.primary, width: 2) : BorderSide.none, + ), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.all(KcSpacing.sm), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + item.name, + style: theme.textTheme.titleSmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + 'SKU: ${item.sku}', + style: theme.textTheme.bodySmall?.copyWith(color: KcColors.neutral), + ), + const SizedBox(height: KcSpacing.xs), + InventoryStatusChip(status: item.status), + ], + ), + ), + const SizedBox(width: KcSpacing.sm), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '${item.quantityOnHand}', + style: theme.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + ), + Text( + 'on hand', + style: theme.textTheme.bodySmall?.copyWith(color: KcColors.neutral), + ), + ], + ), + const SizedBox(width: KcSpacing.xs), + const Icon(Icons.chevron_right, color: KcColors.neutral), + ], + ), + ), + ), + ); + } +} diff --git a/kell_creations_apps/apps/kell_mobile/lib/shell/mobile_shell.dart b/kell_creations_apps/apps/kell_mobile/lib/shell/mobile_shell.dart index 3ea24f6..3cb8ae6 100644 --- a/kell_creations_apps/apps/kell_mobile/lib/shell/mobile_shell.dart +++ b/kell_creations_apps/apps/kell_mobile/lib/shell/mobile_shell.dart @@ -1,5 +1,4 @@ import 'package:core/core.dart'; -import 'package:feature_inventory/feature_inventory.dart'; import 'package:feature_policy/feature_policy.dart'; import 'package:flutter/material.dart'; @@ -9,6 +8,7 @@ import '../dashboard/application/get_dashboard_summary.dart'; import '../pages/dashboard_page.dart'; import '../pages/finance_placeholder_page.dart'; import '../pages/integrations_placeholder_page.dart'; +import '../pages/mobile_inventory_page.dart'; import '../pages/mobile_orders_page.dart'; import '../pages/mobile_publishing_page.dart'; @@ -96,7 +96,7 @@ class _MobileShellState extends State { ), ); case 1: - return InventoryPage( + return MobileInventoryPage( repository: services.inventoryRepository, onViewProduct: (_) { // Cross-feature nav: switch to Products tab. diff --git a/kell_creations_apps/apps/kell_mobile/test/widget_test.dart b/kell_creations_apps/apps/kell_mobile/test/widget_test.dart index ce89564..28882eb 100644 --- a/kell_creations_apps/apps/kell_mobile/test/widget_test.dart +++ b/kell_creations_apps/apps/kell_mobile/test/widget_test.dart @@ -402,4 +402,141 @@ void main() { } } }); + + // ── Stage 6C: Mobile inventory surface tests ───────────────────── + + testWidgets('Inventory tab shows mobile inventory page with search bar', ( + WidgetTester tester, + ) async { + await tester.pumpWidget(_buildTestApp()); + await tester.pumpAndSettle(); + + // Tap the Inventory destination + await tester.tap(find.text('Inventory').last); + await tester.pumpAndSettle(); + + // Should show the search bar + expect(find.byType(TextField), findsOneWidget); + expect(find.text('Search by name or SKU…'), findsOneWidget); + }); + + testWidgets('Inventory tab shows status filter chips', (WidgetTester tester) async { + await tester.pumpWidget(_buildTestApp()); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Inventory').last); + await tester.pumpAndSettle(); + + // Should show filter chips for inventory status categories + expect(find.widgetWithText(FilterChip, 'All'), findsOneWidget); + expect(find.widgetWithText(FilterChip, 'In Stock'), findsOneWidget); + expect(find.widgetWithText(FilterChip, 'Low Stock'), findsOneWidget); + }); + + testWidgets('Inventory tab shows item count', (WidgetTester tester) async { + await tester.pumpWidget(_buildTestApp()); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Inventory').last); + await tester.pumpAndSettle(); + + // Fake data should produce an item count label + expect(find.textContaining('item'), findsWidgets); + }); + + testWidgets('tapping inventory card navigates to inventory detail page', ( + WidgetTester tester, + ) async { + await tester.pumpWidget(_buildTestApp()); + await tester.pumpAndSettle(); + + // Navigate to Inventory tab + await tester.tap(find.text('Inventory').last); + await tester.pumpAndSettle(); + + // Tap the first 120-height inventory card + final firstItem = find.byWidgetPredicate( + (widget) => widget is SizedBox && widget.height == 120 && widget.child != null, + ); + if (firstItem.evaluate().isNotEmpty) { + await tester.tap(firstItem.first); + await tester.pumpAndSettle(); + + // Should now be on the detail page — AppBar shows the item name + expect(find.byType(AppBar), findsOneWidget); + // Detail page shows Adjust Quantity button + expect(find.text('Adjust Quantity'), findsOneWidget); + } + }); + + testWidgets('inventory detail page shows adjust quantity dialog', (WidgetTester tester) async { + await tester.pumpWidget(_buildTestApp()); + await tester.pumpAndSettle(); + + // Navigate to Inventory tab + await tester.tap(find.text('Inventory').last); + await tester.pumpAndSettle(); + + // Tap the first inventory card to navigate to detail + final firstItem = find.byWidgetPredicate( + (widget) => widget is SizedBox && widget.height == 120 && widget.child != null, + ); + if (firstItem.evaluate().isNotEmpty) { + await tester.tap(firstItem.first); + await tester.pumpAndSettle(); + + // Scroll down to reveal the Adjust Quantity button + await tester.drag(find.byType(SingleChildScrollView), const Offset(0, -300)); + await tester.pumpAndSettle(); + + // Tap Adjust Quantity + final adjustButton = find.text('Adjust Quantity'); + if (adjustButton.evaluate().isNotEmpty) { + await tester.tap(adjustButton); + await tester.pumpAndSettle(); + + // Adjust Quantity dialog should appear + expect(find.byType(AlertDialog), findsOneWidget); + expect(find.text('Adjust Quantity'), findsWidgets); + expect(find.text('Cancel'), findsOneWidget); + expect(find.text('Confirm'), findsOneWidget); + + // Cancel should dismiss the dialog + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + + expect(find.byType(AlertDialog), findsNothing); + } + } + }); + + testWidgets('inventory detail page back navigation returns to inventory list', ( + WidgetTester tester, + ) async { + await tester.pumpWidget(_buildTestApp()); + await tester.pumpAndSettle(); + + // Navigate to Inventory tab + await tester.tap(find.text('Inventory').last); + await tester.pumpAndSettle(); + + // Tap the first inventory card + final firstItem = find.byWidgetPredicate( + (widget) => widget is SizedBox && widget.height == 120 && widget.child != null, + ); + if (firstItem.evaluate().isNotEmpty) { + await tester.tap(firstItem.first); + await tester.pumpAndSettle(); + + // Press back + final backButton = find.byType(BackButton); + if (backButton.evaluate().isNotEmpty) { + await tester.tap(backButton); + await tester.pumpAndSettle(); + + // Should be back on the inventory list with search bar + expect(find.text('Search by name or SKU…'), findsOneWidget); + } + } + }); }