Compare commits
No commits in common. "427bb38381f004a96166d563e39aff5f10a1cb18" and "6f839d24cd0e9034c3baf455da8e11d0f37079eb" have entirely different histories.
427bb38381
...
6f839d24cd
|
|
@ -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/orders-page-ui (Stage 5D complete)
|
||||
- next branch: feat/bulk-operator-workflows (Stage 7B) or feat/orders-mobile-surface (Stage 6B)
|
||||
- current stage: Stage 5D complete — orders page UI with search/filter toolbar, OrderDetailPanel, AddNoteDialog, OrderCard, OrderStatusChip, OrdersActionSnackBar, and 115 tests
|
||||
|
||||
## Slice tracker
|
||||
|
||||
|
|
@ -329,21 +329,6 @@
|
|||
- analyze: passed (dart analyze — no issues found)
|
||||
- brief updated: yes
|
||||
|
||||
### feat/orders-mobile-surface
|
||||
|
||||
- status: merged to main
|
||||
- date: 2026-07-10
|
||||
- inspection: complete
|
||||
- implementation: complete
|
||||
- files changed:
|
||||
- `kell_mobile/lib/pages/mobile_orders_page.dart` — new `MobileOrdersPage` with search bar, horizontal status filter chips (All / Pending / Processing / Shipped / Delivered / Cancelled), order count display, `OrderCard` list at 160px height, pull-to-refresh, `_detailPageActive` guard to suppress behind-route SnackBars
|
||||
- `kell_mobile/lib/pages/mobile_order_detail_page.dart` — new `MobileOrderDetailPage` wrapping shared `OrderDetailPanel`; StatefulWidget with local controller listener for SnackBar feedback; confirmation dialog before status changes; haptic feedback (mediumImpact for status, lightImpact for note additions); `SafeArea` wrapper
|
||||
- `kell_mobile/lib/shell/mobile_shell.dart` — Orders tab (case 3) switched from shared `OrdersPage` to `MobileOrdersPage`; removed unused `feature_orders` direct import
|
||||
- `kell_mobile/test/widget_test.dart` — added 6 new Stage 6B tests: Orders tab search bar, status filter chips, order count, order card navigation to detail, confirmation dialog for status change, back navigation returning to orders list — 20 total kell_mobile tests
|
||||
- tests: passed (20/20 kell_mobile, 115/115 feature_orders — all passing)
|
||||
- analyze: passed (dart analyze --fatal-infos — no issues found)
|
||||
- brief updated: yes
|
||||
|
||||
### chore/analyze-cleanup-and-tracker-sync
|
||||
|
||||
- status: merged to main
|
||||
|
|
|
|||
|
|
@ -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`: `14/14 passed`
|
||||
- total: `600/600 passed`
|
||||
- baseline commit: merge of `feat/orders-page-ui` (Stage 5D 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/orders-mobile-surface`** — Stage 6B (orders): Mobile orders surface for Android (matching the pattern established by `feat/android-publishing-surface`), or **`feat/bulk-operator-workflows`** — Stage 7B: Additional bulk actions (bulk publish, bulk move to pending review).
|
||||
Branch from latest `main`. Stage 5D (orders page UI) is complete. The orders feature now has full write-capable UI on web. Next logical step is either surfacing orders on mobile or adding more bulk publishing actions.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,171 +0,0 @@
|
|||
import 'package:feature_orders/feature_orders.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Full-screen order detail page for mobile.
|
||||
///
|
||||
/// Receives the shared [OrdersController] so that status updates and note
|
||||
/// additions are immediately reflected in the list when the user pops back.
|
||||
///
|
||||
/// Wraps the shared [OrderDetailPanel] from [feature_orders] inside a
|
||||
/// [Scaffold] with an [AppBar] showing the order ID.
|
||||
///
|
||||
/// Unlike the web layout where feedback SnackBars are handled by the orders
|
||||
/// 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.
|
||||
///
|
||||
/// Status-change actions present a confirmation dialog before executing,
|
||||
/// reducing accidental taps on touch screens.
|
||||
class MobileOrderDetailPage extends StatefulWidget {
|
||||
final OrdersController controller;
|
||||
|
||||
/// Optional callback to navigate to the Products page for a given SKU.
|
||||
final void Function(String sku)? onViewProduct;
|
||||
|
||||
/// Optional callback to navigate to the Inventory page for a given SKU.
|
||||
final void Function(String sku)? onViewInventory;
|
||||
|
||||
const MobileOrderDetailPage({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.onViewProduct,
|
||||
this.onViewInventory,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MobileOrderDetailPage> createState() => _MobileOrderDetailPageState();
|
||||
}
|
||||
|
||||
class _MobileOrderDetailPageState extends State<MobileOrderDetailPage> {
|
||||
OrdersController 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;
|
||||
showOrderActionSnackBar(context, result);
|
||||
if (result.success) HapticFeedback.mediumImpact();
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows a confirmation dialog before executing a status change.
|
||||
///
|
||||
/// Returns `true` if the user confirmed, `false` otherwise.
|
||||
Future<bool> _confirmStatusChange({
|
||||
required String orderId,
|
||||
required OrderStatus newStatus,
|
||||
}) async {
|
||||
final label = _statusLabel(newStatus);
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Update to $label?'),
|
||||
content: Text('Change order $orderId status to "$label"?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(onPressed: () => Navigator.of(context).pop(true), child: Text('Update')),
|
||||
],
|
||||
),
|
||||
);
|
||||
return confirmed ?? false;
|
||||
}
|
||||
|
||||
/// Handles a status update with confirmation dialog.
|
||||
Future<void> _handleUpdateStatus(OrderStatus newStatus) async {
|
||||
final order = _controller.selectedOrder;
|
||||
if (order == null) return;
|
||||
|
||||
final confirmed = await _confirmStatusChange(orderId: order.id, newStatus: newStatus);
|
||||
if (confirmed && mounted) {
|
||||
await _controller.updateOrderStatus(order.id, newStatus);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles adding a note via the shared dialog.
|
||||
Future<void> _handleAddNote() async {
|
||||
final order = _controller.selectedOrder;
|
||||
if (order == null) return;
|
||||
|
||||
final dialogResult = await showAddNoteDialog(context);
|
||||
if (dialogResult == null) return;
|
||||
|
||||
if (mounted) {
|
||||
await _controller.addOrderNote(order.id, dialogResult.note);
|
||||
if (_controller.lastActionResult?.success == true) {
|
||||
HapticFeedback.lightImpact();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static String _statusLabel(OrderStatus status) {
|
||||
switch (status) {
|
||||
case OrderStatus.pending:
|
||||
return 'Pending';
|
||||
case OrderStatus.processing:
|
||||
return 'Processing';
|
||||
case OrderStatus.shipped:
|
||||
return 'Shipped';
|
||||
case OrderStatus.delivered:
|
||||
return 'Delivered';
|
||||
case OrderStatus.cancelled:
|
||||
return 'Cancelled';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, _) {
|
||||
final order = _controller.selectedOrder;
|
||||
|
||||
// If the order was removed or deselected, pop back.
|
||||
if (order == null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (context.mounted) Navigator.of(context).pop();
|
||||
});
|
||||
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Order ${order.id}')),
|
||||
body: SafeArea(
|
||||
child: OrderDetailPanel(
|
||||
order: order,
|
||||
isUpdating: _controller.isUpdating,
|
||||
onUpdateStatus: _handleUpdateStatus,
|
||||
onAddNote: _handleAddNote,
|
||||
onViewProduct: widget.onViewProduct,
|
||||
onViewInventory: widget.onViewInventory,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,244 +0,0 @@
|
|||
import 'package:design_system/design_system.dart';
|
||||
import 'package:feature_orders/feature_orders.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'mobile_order_detail_page.dart';
|
||||
|
||||
/// Mobile-optimized orders workspace page.
|
||||
///
|
||||
/// Provides browsing, filtering, and searching for orders on smaller screens.
|
||||
/// Tapping an order pushes a full-screen detail page for viewing, status
|
||||
/// updates, and note additions.
|
||||
///
|
||||
/// Reuses the shared [OrdersController] and all use cases from
|
||||
/// [feature_orders] — no business logic is forked for mobile.
|
||||
class MobileOrdersPage extends StatefulWidget {
|
||||
final OrdersRepository repository;
|
||||
|
||||
/// Optional callback to navigate to the Products page for a given SKU.
|
||||
final void Function(String sku)? onViewProduct;
|
||||
|
||||
/// Optional callback to navigate to the Inventory page for a given SKU.
|
||||
final void Function(String sku)? onViewInventory;
|
||||
|
||||
const MobileOrdersPage({
|
||||
super.key,
|
||||
required this.repository,
|
||||
this.onViewProduct,
|
||||
this.onViewInventory,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MobileOrdersPage> createState() => _MobileOrdersPageState();
|
||||
}
|
||||
|
||||
class _MobileOrdersPageState extends State<MobileOrdersPage> {
|
||||
late final OrdersController _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 = OrdersController(
|
||||
GetOrders(repo),
|
||||
updateOrderStatus: UpdateOrderStatus(repo),
|
||||
addOrderNote: AddOrderNote(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;
|
||||
showOrderActionSnackBar(context, result);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, _) {
|
||||
if (_controller.isLoading) {
|
||||
return const KcLoadingState(message: 'Loading orders…');
|
||||
}
|
||||
|
||||
if (_controller.error != null) {
|
||||
return KcErrorState(message: 'Failed to load orders.', onRetry: _controller.load);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildSearchBar(),
|
||||
_buildFilterChips(),
|
||||
_buildOrderCount(),
|
||||
Expanded(child: _buildOrderList()),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 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 customer or order ID…',
|
||||
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'),
|
||||
('pending', 'Pending'),
|
||||
('processing', 'Processing'),
|
||||
('shipped', 'Shipped'),
|
||||
('delivered', 'Delivered'),
|
||||
('cancelled', 'Cancelled'),
|
||||
];
|
||||
|
||||
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),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Order count row.
|
||||
Widget _buildOrderCount() {
|
||||
final count = _controller.orders.length;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: KcSpacing.md, vertical: KcSpacing.xs),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
'$count ${count == 1 ? 'order' : 'orders'}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: KcColors.neutral),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Scrollable order list using compact cards.
|
||||
Widget _buildOrderList() {
|
||||
final orders = _controller.orders;
|
||||
|
||||
if (orders.isEmpty) {
|
||||
return KcEmptyState(
|
||||
icon: Icons.receipt_long_outlined,
|
||||
message: _controller.searchQuery.isNotEmpty || _controller.activeFilter != null
|
||||
? 'No orders match your criteria.'
|
||||
: 'No orders available.',
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: _controller.load,
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(horizontal: KcSpacing.md),
|
||||
itemCount: orders.length,
|
||||
separatorBuilder: (_, _) => const SizedBox(height: KcSpacing.xs),
|
||||
itemBuilder: (context, index) {
|
||||
final order = orders[index];
|
||||
return SizedBox(
|
||||
height: 160,
|
||||
child: OrderCard(
|
||||
order: order,
|
||||
isSelected: order.id == _controller.selectedOrder?.id,
|
||||
onTap: () => _navigateToDetail(order),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Navigates to the full-screen order 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(Order order) {
|
||||
_controller.selectOrder(order);
|
||||
setState(() => _detailPageActive = true);
|
||||
Navigator.of(context)
|
||||
.push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => MobileOrderDetailPage(
|
||||
controller: _controller,
|
||||
onViewProduct: widget.onViewProduct,
|
||||
onViewInventory: widget.onViewInventory,
|
||||
),
|
||||
),
|
||||
)
|
||||
.then((_) {
|
||||
if (mounted) setState(() => _detailPageActive = false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:core/core.dart';
|
||||
import 'package:feature_inventory/feature_inventory.dart';
|
||||
import 'package:feature_orders/feature_orders.dart';
|
||||
import 'package:feature_policy/feature_policy.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
|
@ -9,7 +10,6 @@ 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_orders_page.dart';
|
||||
import '../pages/mobile_publishing_page.dart';
|
||||
|
||||
/// The main shell for the mobile app.
|
||||
|
|
@ -106,7 +106,7 @@ class _MobileShellState extends State<MobileShell> {
|
|||
case 2:
|
||||
return MobilePublishingPage(repository: services.productPublishingRepository);
|
||||
case 3:
|
||||
return MobileOrdersPage(
|
||||
return OrdersPage(
|
||||
repository: services.ordersRepository,
|
||||
onViewProduct: (_) {
|
||||
setState(() => _selectedIndex = 2);
|
||||
|
|
|
|||
|
|
@ -262,144 +262,4 @@ void main() {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── Stage 6B: Mobile orders surface tests ────────────────────────
|
||||
|
||||
testWidgets('Orders tab shows mobile orders page with search bar', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(_buildTestApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap the Orders destination
|
||||
await tester.tap(find.text('Orders').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Should show the search bar
|
||||
expect(find.byType(TextField), findsOneWidget);
|
||||
expect(find.text('Search by customer or order ID…'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Orders tab shows status filter chips', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(_buildTestApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Orders').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Should show filter chips for order status categories
|
||||
expect(find.widgetWithText(FilterChip, 'All'), findsOneWidget);
|
||||
expect(find.widgetWithText(FilterChip, 'Pending'), findsOneWidget);
|
||||
expect(find.widgetWithText(FilterChip, 'Shipped'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Orders tab shows order count', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(_buildTestApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Orders').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Fake data should produce an order count label
|
||||
expect(find.textContaining('order'), findsWidgets);
|
||||
});
|
||||
|
||||
testWidgets('tapping order card navigates to order detail page', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(_buildTestApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Navigate to Orders tab
|
||||
await tester.tap(find.text('Orders').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap the first 160-height order card
|
||||
final firstOrder = find.byWidgetPredicate(
|
||||
(widget) => widget is SizedBox && widget.height == 160 && widget.child != null,
|
||||
);
|
||||
if (firstOrder.evaluate().isNotEmpty) {
|
||||
await tester.tap(firstOrder.first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Should now be on the detail page — AppBar shows "Order ..."
|
||||
expect(find.byType(AppBar), findsOneWidget);
|
||||
expect(find.textContaining('Order'), findsWidgets);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('order detail page shows confirmation dialog for status change', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
await tester.pumpWidget(_buildTestApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Navigate to Orders tab
|
||||
await tester.tap(find.text('Orders').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap the first order card to navigate to detail
|
||||
final firstOrder = find.byWidgetPredicate(
|
||||
(widget) => widget is SizedBox && widget.height == 160 && widget.child != null,
|
||||
);
|
||||
if (firstOrder.evaluate().isNotEmpty) {
|
||||
await tester.tap(firstOrder.first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Scroll down to reveal the status dropdown
|
||||
await tester.drag(find.byType(SingleChildScrollView), const Offset(0, -300));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Look for the status dropdown and change it
|
||||
final dropdown = find.byType(DropdownButtonFormField<dynamic>);
|
||||
if (dropdown.evaluate().isNotEmpty) {
|
||||
await tester.tap(dropdown.first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Pick a different status from the dropdown menu
|
||||
final menuItems = find.byType(DropdownMenuItem<dynamic>);
|
||||
if (menuItems.evaluate().length > 1) {
|
||||
await tester.tap(menuItems.at(1));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Confirmation dialog should appear
|
||||
expect(find.byType(AlertDialog), findsOneWidget);
|
||||
expect(find.text('Cancel'), findsOneWidget);
|
||||
expect(find.text('Update'), findsOneWidget);
|
||||
|
||||
// Cancel should dismiss the dialog
|
||||
await tester.tap(find.text('Cancel'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(AlertDialog), findsNothing);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('order detail page back navigation returns to orders list', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
await tester.pumpWidget(_buildTestApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Navigate to Orders tab
|
||||
await tester.tap(find.text('Orders').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap the first order card
|
||||
final firstOrder = find.byWidgetPredicate(
|
||||
(widget) => widget is SizedBox && widget.height == 160 && widget.child != null,
|
||||
);
|
||||
if (firstOrder.evaluate().isNotEmpty) {
|
||||
await tester.tap(firstOrder.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 orders list with search bar
|
||||
expect(find.text('Search by customer or order ID…'), findsOneWidget);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue