Compare commits
2 Commits
591de0c5c4
...
4b5c96c5ec
| Author | SHA1 | Date |
|---|---|---|
|
|
4b5c96c5ec | |
|
|
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-30)
|
||||||
- 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
|
||||||
|
|
|
||||||
|
|
@ -112,11 +112,13 @@ Rules:
|
||||||
- `design_system` tests passing
|
- `design_system` tests passing
|
||||||
- `feature_wordpress` tests passing
|
- `feature_wordpress` tests passing
|
||||||
- `kell_web` tests passing
|
- `kell_web` tests passing
|
||||||
|
- `kell_mobile` tests passing
|
||||||
- latest reported count for `core`: `20/20 passed`
|
- latest reported count for `core`: `20/20 passed`
|
||||||
- latest reported count for `design_system`: `41/41 passed`
|
- latest reported count for `design_system`: `41/41 passed`
|
||||||
- latest reported count for `feature_wordpress`: `294/294 passed`
|
- latest reported count for `feature_wordpress`: `294/294 passed`
|
||||||
- latest reported count for `kell_web`: `24/24 passed`
|
- latest reported count for `kell_web`: `24/24 passed`
|
||||||
- baseline commit: merge of `feat/test-coverage-visibility` (2026-05-22)
|
- latest reported count for `kell_mobile`: `14/14 passed`
|
||||||
|
- baseline commit: merge of `feat/android-feedback-polish` (2026-05-30)
|
||||||
|
|
||||||
#### Baseline test coverage (established 2026-05-22)
|
#### Baseline test coverage (established 2026-05-22)
|
||||||
|
|
||||||
|
|
@ -132,8 +134,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 +305,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
|
||||||
|
|
||||||
|
|
@ -412,7 +407,7 @@ The following packages exist as scaffolded stubs with no implementation. Each ne
|
||||||
|
|
||||||
### ~~6. Build execution tracker synchronization~~ ✅ RESOLVED
|
### ~~6. Build execution tracker synchronization~~ ✅ RESOLVED
|
||||||
|
|
||||||
> The `build_execution_tracker.md` has been fully updated through Stage 4D. All 10 merged branches are tracked with status, dates, files changed, test counts, and analysis results. Current status correctly reflects Stage 4 complete and next branch as `feat/android-app-shell` (Stage 5A). Updated as part of the Stage 4C/4D merge cycle (2026-05-22).
|
> The `build_execution_tracker.md` has been fully updated through Stage 6A. All 13 merged branches are tracked with status, dates, files changed, test counts, and analysis results. Current status correctly reflects Stage 6A complete and next branch as `feat/android-mobile-ux-hardening` (Stage 6B). Last synchronized 2026-05-30.
|
||||||
|
|
||||||
### 7. Image and media management
|
### 7. Image and media management
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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