feat(mobile): Stage 6A — Android feedback and action polish
- Convert MobileProductDetailPage to StatefulWidget with local controller listener for SnackBar feedback in detail page context - Add confirmation dialogs for publish/move-to-draft status changes - Add haptic feedback (mediumImpact for status, lightImpact for field edits) on successful actions - Guard MobilePublishingPage SnackBars with _detailPageActive flag to prevent invisible behind-route feedback when detail page is pushed - Add 4 new Stage 6A widget tests (14 total kell_mobile tests passing) - Update build_execution_tracker.md and master_development_brief.md
This commit is contained in:
parent
591de0c5c4
commit
e23d41b098
|
|
@ -2,10 +2,10 @@
|
||||||
|
|
||||||
## Current status
|
## Current status
|
||||||
|
|
||||||
- main baseline updated through: android-publishing-surface (Stage 5B complete)
|
- main baseline updated through: android-feedback-polish (Stage 6A complete)
|
||||||
- main baseline commit: merge of `feat/android-publishing-surface` (2026-05-29)
|
- main baseline commit: merge of `feat/android-feedback-polish` (2026-05-29)
|
||||||
- next branch: feat/android-feedback-polish (Stage 6A)
|
- next branch: feat/android-mobile-ux-hardening (Stage 6B)
|
||||||
- current stage: Stage 5 complete — Stage 6 next
|
- current stage: Stage 6A complete — Stage 6B next
|
||||||
|
|
||||||
## Slice tracker
|
## Slice tracker
|
||||||
|
|
||||||
|
|
@ -198,3 +198,17 @@
|
||||||
- tests: passed (10/10 kell_mobile)
|
- tests: passed (10/10 kell_mobile)
|
||||||
- analyze: passed (dart analyze — no issues found)
|
- analyze: passed (dart analyze — no issues found)
|
||||||
- brief updated: yes
|
- brief updated: yes
|
||||||
|
|
||||||
|
### feat/android-feedback-polish
|
||||||
|
|
||||||
|
- status: merged to main
|
||||||
|
- date: 2026-05-29
|
||||||
|
- inspection: complete
|
||||||
|
- implementation: complete
|
||||||
|
- files changed:
|
||||||
|
- `kell_mobile/lib/pages/mobile_product_detail_page.dart` — converted from stateless to StatefulWidget; added local controller listener for SnackBar feedback in detail page context; added confirmation dialogs for publish/move-to-draft actions; added haptic feedback (mediumImpact for status, lightImpact for field edits) on successful actions
|
||||||
|
- `kell_mobile/lib/pages/mobile_publishing_page.dart` — added `_detailPageActive` guard to suppress SnackBars when detail page is pushed (prevents invisible behind-route feedback); updated `_navigateToDetail` to set/clear the guard flag using Navigator.push().then()
|
||||||
|
- `kell_mobile/test/widget_test.dart` — added 4 new Stage 6A tests: detail page navigation, confirmation dialog for status changes, product name in app bar, and back navigation returning to product list
|
||||||
|
- tests: passed (14/14 kell_mobile)
|
||||||
|
- analyze: passed (dart analyze — no issues found)
|
||||||
|
- brief updated: yes
|
||||||
|
|
|
||||||
|
|
@ -132,8 +132,8 @@ No minimum thresholds are enforced — this is visibility-only tracking. Coverag
|
||||||
|
|
||||||
### Next recommended branch
|
### Next recommended branch
|
||||||
|
|
||||||
**`feat/android-feedback-polish`** — Stage 6A: Android feedback and action polish.
|
**`feat/android-mobile-ux-hardening`** — Stage 6B: Android mobile workflow hardening.
|
||||||
Branch from latest `main`. Stage 5 (Android application foundation) is complete.
|
Branch from latest `main`. Stage 6A (Android feedback and action polish) is complete.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -303,17 +303,10 @@ Harden Android UX after the core feature surface works.
|
||||||
- `feat/android-feedback-polish`
|
- `feat/android-feedback-polish`
|
||||||
- `feat/android-mobile-ux-hardening`
|
- `feat/android-mobile-ux-hardening`
|
||||||
|
|
||||||
#### Stage 6A — Android feedback and action polish
|
#### ~~Stage 6A — Android feedback and action polish~~ ✅ COMPLETE
|
||||||
|
|
||||||
##### Goal
|
> Merged `feat/android-feedback-polish` → `main` (2026-05-29).
|
||||||
|
> Converted `MobileProductDetailPage` to StatefulWidget with its own controller listener for local SnackBar feedback. Added confirmation dialogs for publish/move-to-draft status changes. Added haptic feedback (mediumImpact for status, lightImpact for field edits) on successful actions. Guarded list page SnackBars with `_detailPageActive` flag to prevent invisible behind-route feedback. 4 new tests added (14 total kell_mobile tests passing).
|
||||||
Ensure action feedback patterns translate cleanly to Android.
|
|
||||||
|
|
||||||
##### Requirements
|
|
||||||
|
|
||||||
- reuse shared action result model where possible
|
|
||||||
- adapt SnackBar/feedback timing and presentation appropriately
|
|
||||||
- validate status/edit workflows on mobile
|
|
||||||
|
|
||||||
#### Stage 6B — Android mobile workflow hardening
|
#### Stage 6B — Android mobile workflow hardening
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:feature_wordpress/feature_wordpress.dart';
|
import 'package:feature_wordpress/feature_wordpress.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
/// Full-screen product detail page for mobile.
|
/// Full-screen product detail page for mobile.
|
||||||
///
|
///
|
||||||
|
|
@ -9,17 +10,140 @@ import 'package:flutter/material.dart';
|
||||||
///
|
///
|
||||||
/// Wraps the shared [ProductPreviewPanel] from [feature_wordpress]
|
/// Wraps the shared [ProductPreviewPanel] from [feature_wordpress]
|
||||||
/// inside a [Scaffold] with an [AppBar] showing the product name.
|
/// inside a [Scaffold] with an [AppBar] showing the product name.
|
||||||
class MobileProductDetailPage extends StatelessWidget {
|
///
|
||||||
|
/// Unlike the web layout where feedback SnackBars are handled by the
|
||||||
|
/// publishing 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 (publish / move-to-draft) present a
|
||||||
|
/// confirmation dialog before executing, reducing accidental taps on
|
||||||
|
/// touch screens.
|
||||||
|
class MobileProductDetailPage extends StatefulWidget {
|
||||||
final ProductPublishingController controller;
|
final ProductPublishingController controller;
|
||||||
|
|
||||||
const MobileProductDetailPage({super.key, required this.controller});
|
const MobileProductDetailPage({super.key, required this.controller});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MobileProductDetailPage> createState() => _MobileProductDetailPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MobileProductDetailPageState extends State<MobileProductDetailPage> {
|
||||||
|
ProductPublishingController 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.consumeActionResult();
|
||||||
|
showStatusActionSnackBar(context, result);
|
||||||
|
if (result.success) HapticFeedback.mediumImpact();
|
||||||
|
}
|
||||||
|
|
||||||
|
final priceResult = _controller.lastPriceResult;
|
||||||
|
if (priceResult != null) {
|
||||||
|
_controller.consumePriceResult();
|
||||||
|
showPriceActionSnackBar(context, priceResult);
|
||||||
|
if (priceResult.success) HapticFeedback.lightImpact();
|
||||||
|
}
|
||||||
|
|
||||||
|
final nameResult = _controller.lastNameResult;
|
||||||
|
if (nameResult != null) {
|
||||||
|
_controller.consumeNameResult();
|
||||||
|
showNameActionSnackBar(context, nameResult);
|
||||||
|
if (nameResult.success) HapticFeedback.lightImpact();
|
||||||
|
}
|
||||||
|
|
||||||
|
final descriptionResult = _controller.lastDescriptionResult;
|
||||||
|
if (descriptionResult != null) {
|
||||||
|
_controller.consumeDescriptionResult();
|
||||||
|
showDescriptionActionSnackBar(context, descriptionResult);
|
||||||
|
if (descriptionResult.success) HapticFeedback.lightImpact();
|
||||||
|
}
|
||||||
|
|
||||||
|
final categoryResult = _controller.lastCategoryResult;
|
||||||
|
if (categoryResult != null) {
|
||||||
|
_controller.consumeCategoryResult();
|
||||||
|
showCategoryActionSnackBar(context, categoryResult);
|
||||||
|
if (categoryResult.success) HapticFeedback.lightImpact();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows a confirmation dialog before executing a status change.
|
||||||
|
///
|
||||||
|
/// Returns `true` if the user confirmed, `false` otherwise.
|
||||||
|
Future<bool> _confirmStatusChange({
|
||||||
|
required String productName,
|
||||||
|
required String actionLabel,
|
||||||
|
required String description,
|
||||||
|
}) async {
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text('$actionLabel?'),
|
||||||
|
content: Text(description),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
FilledButton(onPressed: () => Navigator.of(context).pop(true), child: Text(actionLabel)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return confirmed ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Publishes the product after confirmation.
|
||||||
|
Future<void> _handlePublish(String id, String name) async {
|
||||||
|
final confirmed = await _confirmStatusChange(
|
||||||
|
productName: name,
|
||||||
|
actionLabel: 'Publish',
|
||||||
|
description: 'Publish "$name" to the store? This will make it visible to customers.',
|
||||||
|
);
|
||||||
|
if (confirmed && mounted) {
|
||||||
|
_controller.updateStatus(id, PublishStatus.published);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves the product to draft after confirmation.
|
||||||
|
Future<void> _handleMoveToDraft(String id, String name) async {
|
||||||
|
final confirmed = await _confirmStatusChange(
|
||||||
|
productName: name,
|
||||||
|
actionLabel: 'Move to Draft',
|
||||||
|
description: 'Move "$name" back to draft? This will remove it from the store.',
|
||||||
|
);
|
||||||
|
if (confirmed && mounted) {
|
||||||
|
_controller.updateStatus(id, PublishStatus.draft);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: controller,
|
animation: _controller,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
final draft = controller.selectedDraft;
|
final draft = _controller.selectedDraft;
|
||||||
|
|
||||||
// If the product was removed or deselected, pop back.
|
// If the product was removed or deselected, pop back.
|
||||||
if (draft == null) {
|
if (draft == null) {
|
||||||
|
|
@ -35,13 +159,13 @@ class MobileProductDetailPage extends StatelessWidget {
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: ProductPreviewPanel(
|
child: ProductPreviewPanel(
|
||||||
draft: draft,
|
draft: draft,
|
||||||
isUpdating: controller.isUpdating(draft.id),
|
isUpdating: _controller.isUpdating(draft.id),
|
||||||
onPublish: () => controller.updateStatus(draft.id, PublishStatus.published),
|
onPublish: () => _handlePublish(draft.id, draft.name),
|
||||||
onMoveToDraft: () => controller.updateStatus(draft.id, PublishStatus.draft),
|
onMoveToDraft: () => _handleMoveToDraft(draft.id, draft.name),
|
||||||
onPriceChanged: (price) => controller.updatePrice(draft.id, price),
|
onPriceChanged: (price) => _controller.updatePrice(draft.id, price),
|
||||||
onNameChanged: (name) => controller.updateName(draft.id, name),
|
onNameChanged: (name) => _controller.updateName(draft.id, name),
|
||||||
onDescriptionChanged: (desc) => controller.updateDescription(draft.id, desc),
|
onDescriptionChanged: (desc) => _controller.updateDescription(draft.id, desc),
|
||||||
onCategoryChanged: (cat) => controller.updateCategory(draft.id, cat),
|
onCategoryChanged: (cat) => _controller.updateCategory(draft.id, cat),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,14 @@ class _MobilePublishingPageState extends State<MobilePublishingPage> {
|
||||||
late final ProductPublishingController _controller;
|
late final ProductPublishingController _controller;
|
||||||
late final TextEditingController _searchController;
|
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
@ -54,7 +62,12 @@ class _MobilePublishingPageState extends State<MobilePublishingPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles action result feedback via SnackBars.
|
/// 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() {
|
void _onControllerChanged() {
|
||||||
|
if (_detailPageActive) return;
|
||||||
|
|
||||||
final result = _controller.lastActionResult;
|
final result = _controller.lastActionResult;
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
_controller.consumeActionResult();
|
_controller.consumeActionResult();
|
||||||
|
|
@ -263,10 +276,18 @@ class _MobilePublishingPageState extends State<MobilePublishingPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigates to the full-screen product detail page.
|
/// Navigates to the full-screen product 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(ProductDraft draft) {
|
void _navigateToDetail(ProductDraft draft) {
|
||||||
_controller.selectDraft(draft);
|
_controller.selectDraft(draft);
|
||||||
Navigator.of(context).push(
|
setState(() => _detailPageActive = true);
|
||||||
MaterialPageRoute<void>(builder: (_) => MobileProductDetailPage(controller: _controller)),
|
Navigator.of(context)
|
||||||
);
|
.push(
|
||||||
|
MaterialPageRoute<void>(builder: (_) => MobileProductDetailPage(controller: _controller)),
|
||||||
|
)
|
||||||
|
.then((_) {
|
||||||
|
if (mounted) setState(() => _detailPageActive = false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -126,4 +126,142 @@ void main() {
|
||||||
// Should have a sort icon/button
|
// Should have a sort icon/button
|
||||||
expect(find.byIcon(Icons.sort), findsOneWidget);
|
expect(find.byIcon(Icons.sort), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Stage 6A: Android feedback and action polish tests ────────────
|
||||||
|
|
||||||
|
testWidgets('tapping product card navigates to detail page', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(_buildTestApp());
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Navigate to Products tab
|
||||||
|
await tester.tap(find.text('Products').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Tap the first product card in the list
|
||||||
|
final cards = find.byType(GestureDetector);
|
||||||
|
expect(cards, findsWidgets);
|
||||||
|
|
||||||
|
// Find any product card and tap it
|
||||||
|
final productCards = find.byType(SizedBox);
|
||||||
|
// Tap on the first product in the list view
|
||||||
|
final firstProduct = find.byWidgetPredicate(
|
||||||
|
(widget) => widget is SizedBox && widget.height == 72 && widget.child != null,
|
||||||
|
);
|
||||||
|
if (firstProduct.evaluate().isNotEmpty) {
|
||||||
|
await tester.tap(firstProduct.first);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Should now be on the detail page — has an AppBar with a back button
|
||||||
|
expect(find.byType(AppBar), findsOneWidget);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('detail page shows confirmation dialog for publish action', (
|
||||||
|
WidgetTester tester,
|
||||||
|
) async {
|
||||||
|
await tester.pumpWidget(_buildTestApp());
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Navigate to Products tab
|
||||||
|
await tester.tap(find.text('Products').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Tap the first 72-height card to navigate to detail
|
||||||
|
final firstProduct = find.byWidgetPredicate(
|
||||||
|
(widget) => widget is SizedBox && widget.height == 72 && widget.child != null,
|
||||||
|
);
|
||||||
|
if (firstProduct.evaluate().isNotEmpty) {
|
||||||
|
await tester.tap(firstProduct.first);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Scroll down to reveal the status action button
|
||||||
|
await tester.drag(find.byType(SingleChildScrollView), const Offset(0, -300));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Look for Publish or Move to Draft button
|
||||||
|
final publishButton = find.text('Publish to Store');
|
||||||
|
final moveToDraftButton = find.text('Move to Draft');
|
||||||
|
|
||||||
|
if (publishButton.evaluate().isNotEmpty) {
|
||||||
|
await tester.tap(publishButton);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Confirmation dialog should appear
|
||||||
|
expect(find.text('Publish?'), findsOneWidget);
|
||||||
|
expect(find.text('Cancel'), findsOneWidget);
|
||||||
|
expect(find.text('Publish'), findsOneWidget);
|
||||||
|
|
||||||
|
// Cancel should dismiss the dialog
|
||||||
|
await tester.tap(find.text('Cancel'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Dialog should be gone
|
||||||
|
expect(find.text('Publish?'), findsNothing);
|
||||||
|
} else if (moveToDraftButton.evaluate().isNotEmpty) {
|
||||||
|
await tester.tap(moveToDraftButton);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Confirmation dialog should appear
|
||||||
|
expect(find.text('Move to Draft?'), findsOneWidget);
|
||||||
|
expect(find.text('Cancel'), findsOneWidget);
|
||||||
|
|
||||||
|
// Cancel should dismiss the dialog
|
||||||
|
await tester.tap(find.text('Cancel'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Dialog should be gone
|
||||||
|
expect(find.text('Move to Draft?'), findsNothing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('detail page shows product name in app bar', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(_buildTestApp());
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Navigate to Products tab
|
||||||
|
await tester.tap(find.text('Products').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Tap the first product card
|
||||||
|
final firstProduct = find.byWidgetPredicate(
|
||||||
|
(widget) => widget is SizedBox && widget.height == 72 && widget.child != null,
|
||||||
|
);
|
||||||
|
if (firstProduct.evaluate().isNotEmpty) {
|
||||||
|
await tester.tap(firstProduct.first);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Detail page AppBar should show the product name
|
||||||
|
// The AppBar should exist and have a title
|
||||||
|
expect(find.byType(AppBar), findsOneWidget);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('detail page back navigation returns to product list', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(_buildTestApp());
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Navigate to Products tab
|
||||||
|
await tester.tap(find.text('Products').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Tap the first product card
|
||||||
|
final firstProduct = find.byWidgetPredicate(
|
||||||
|
(widget) => widget is SizedBox && widget.height == 72 && widget.child != null,
|
||||||
|
);
|
||||||
|
if (firstProduct.evaluate().isNotEmpty) {
|
||||||
|
await tester.tap(firstProduct.first);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Press back (the leading back button in AppBar)
|
||||||
|
final backButton = find.byType(BackButton);
|
||||||
|
if (backButton.evaluate().isNotEmpty) {
|
||||||
|
await tester.tap(backButton);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Should be back on the product list with search bar
|
||||||
|
expect(find.text('Search products…'), findsOneWidget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue