Compare commits
3 Commits
7acff83bf4
...
b81016df28
| Author | SHA1 | Date |
|---|---|---|
|
|
b81016df28 | |
|
|
02cc75c655 | |
|
|
73b4a49939 |
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
## Current status
|
## Current status
|
||||||
|
|
||||||
- main baseline updated through: category-only-edit
|
- main baseline updated through: post-write-consistency
|
||||||
- main baseline commit: `8e7e4cb` (2026-04-11)
|
- main baseline commit: `7acff83` (2026-04-11)
|
||||||
- next branch: feat/post-write-consistency
|
- next branch: feat/publishing-ux-hardening
|
||||||
- current stage: Stage 2 — Web application operational hardening
|
- current stage: Stage 2 — Web application operational hardening
|
||||||
|
|
||||||
## Slice tracker
|
## Slice tracker
|
||||||
|
|
@ -31,7 +31,8 @@
|
||||||
|
|
||||||
### feat/post-write-consistency
|
### feat/post-write-consistency
|
||||||
|
|
||||||
- status: implemented — ready for review / merge
|
- status: merged to main
|
||||||
|
- commit: `7acff83`
|
||||||
- inspection: complete
|
- inspection: complete
|
||||||
- implementation: complete
|
- implementation: complete
|
||||||
- files changed:
|
- files changed:
|
||||||
|
|
@ -40,3 +41,17 @@
|
||||||
- tests: passed (234/234 feature_wordpress)
|
- tests: passed (234/234 feature_wordpress)
|
||||||
- analyze: passed (dart analyze — no issues found)
|
- analyze: passed (dart analyze — no issues found)
|
||||||
- brief updated: yes
|
- brief updated: yes
|
||||||
|
|
||||||
|
### feat/publishing-ux-hardening
|
||||||
|
|
||||||
|
- status: in progress (Stage 2B)
|
||||||
|
- inspection: complete
|
||||||
|
- implementation: complete
|
||||||
|
- files changed:
|
||||||
|
- `feature_wordpress/lib/src/presentation/widgets/product_preview_panel.dart` — added inline validation error state (`_nameError`, `_priceError`, `_descriptionError`, `_categoryError`); error messages shown below text fields on empty/invalid submit; errors cleared on cancel; added `onCategoryChanged` callback; text fields and save buttons disabled during `isUpdating`; added tooltips (`Save price`, `Save category`, `Cancel`) for consistency
|
||||||
|
- `feature_wordpress/lib/src/presentation/widgets/status_action_snack_bar.dart` — failure SnackBar now appends `errorMessage` detail to the failure text for operator visibility
|
||||||
|
- `feature_wordpress/test/widgets/product_preview_panel_test.dart` — added `onCategoryChanged` to test helper; added 13 new tests: inline validation errors (name, price, description, category), error-clears-on-cancel, valid-input-clears-error, and text-field-disabled-during-isUpdating for all four edit fields
|
||||||
|
- `feature_wordpress/test/widgets/status_action_snack_bar_test.dart` — updated 2 failure SnackBar assertions to match new wording that includes error detail
|
||||||
|
- tests: passed (247/247 feature_wordpress)
|
||||||
|
- analyze: passed (dart analyze — no issues found)
|
||||||
|
- brief updated: yes
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ Rules:
|
||||||
- Name-only product edit landed.
|
- Name-only product edit landed.
|
||||||
- ✅ Description-only product edit landed (Stage 1A complete — merged `feat/description-only-edit` → `main` at `cebac4c`, 2026-04-11).
|
- ✅ Description-only product edit landed (Stage 1A complete — merged `feat/description-only-edit` → `main` at `cebac4c`, 2026-04-11).
|
||||||
- ✅ Category-only product edit landed (Stage 1B complete — merged `feat/category-only-edit` → `main` at `8e7e4cb`, 2026-04-11). Stage 1 complete.
|
- ✅ Category-only product edit landed (Stage 1B complete — merged `feat/category-only-edit` → `main` at `8e7e4cb`, 2026-04-11). Stage 1 complete.
|
||||||
|
- ✅ Post-write consistency hardening landed (Stage 2A complete — merged `feat/post-write-consistency` → `main` at `7acff83`, 2026-04-11).
|
||||||
|
|
||||||
### Current narrow edit capabilities on `main`
|
### Current narrow edit capabilities on `main`
|
||||||
|
|
||||||
|
|
@ -93,21 +94,22 @@ Rules:
|
||||||
- `dart analyze` clean
|
- `dart analyze` clean
|
||||||
- `feature_wordpress` tests passing
|
- `feature_wordpress` tests passing
|
||||||
- `kell_web` dashboard tests passing
|
- `kell_web` dashboard tests passing
|
||||||
- latest reported count for `feature_wordpress`: `223/223 passed`
|
- latest reported count for `feature_wordpress`: `234/234 passed`
|
||||||
- latest reported count for `kell_web` dashboard tests: `5/5 passed`
|
- latest reported count for `kell_web` dashboard tests: `5/5 passed`
|
||||||
- baseline commit: `8e7e4cb` (2026-04-11)
|
- baseline commit: `7acff83` (2026-04-11)
|
||||||
|
|
||||||
### Post-write consistency (Stage 2A) — on branch `feat/post-write-consistency`
|
### In-progress branch
|
||||||
|
|
||||||
- `dart analyze` clean
|
|
||||||
- `feature_wordpress` tests: `234/234 passed`
|
|
||||||
- controller `_refreshSelection()` preserves/refreshes selection after all writes
|
|
||||||
- 11 new post-write consistency tests added
|
|
||||||
|
|
||||||
### Next recommended branch
|
|
||||||
|
|
||||||
**`feat/publishing-ux-hardening`** — Stage 2B: Publishing workflow UX hardening.
|
**`feat/publishing-ux-hardening`** — Stage 2B: Publishing workflow UX hardening.
|
||||||
Branch from `main` after merging `feat/post-write-consistency`. Stage 2A is complete on branch.
|
Branch from `main` at `7acff83`. Implementation complete, pending merge.
|
||||||
|
|
||||||
|
- `dart analyze` clean
|
||||||
|
- `feature_wordpress` tests: `247/247 passed` (13 new tests added)
|
||||||
|
- Ready for PR into `main`.
|
||||||
|
|
||||||
|
### Next recommended branch (after 2B merges)
|
||||||
|
|
||||||
|
**`feat/multi-select-groundwork`** — Stage 3A: Multi-select groundwork (read/state only first).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -166,24 +168,10 @@ Improve operator consistency, predictability, and usability after writes.
|
||||||
- `feat/post-write-consistency`
|
- `feat/post-write-consistency`
|
||||||
- `feat/publishing-ux-hardening`
|
- `feat/publishing-ux-hardening`
|
||||||
|
|
||||||
#### Stage 2A — Post-write consistency hardening
|
#### ~~Stage 2A — Post-write consistency hardening~~ ✅ COMPLETE
|
||||||
|
|
||||||
##### Goal
|
> Merged `feat/post-write-consistency` → `main` at `7acff83` (2026-04-11).
|
||||||
|
> Added `_refreshSelection()` to `ProductPublishingController` to preserve/refresh selection after all write-triggered reloads. 11 new post-write consistency tests added (234 total `feature_wordpress` tests passing).
|
||||||
Make list/detail behavior predictable after edits and status changes.
|
|
||||||
|
|
||||||
##### Requirements
|
|
||||||
|
|
||||||
- preserve selection after update where sensible
|
|
||||||
- maintain search/filter/sort persistence after writes
|
|
||||||
- handle item repositioning under active sort cleanly
|
|
||||||
- ensure latest values refresh correctly
|
|
||||||
- verify last-modified updates behave consistently
|
|
||||||
|
|
||||||
##### Definition of done
|
|
||||||
|
|
||||||
- post-action behavior feels stable and predictable
|
|
||||||
- focused tests cover persistence and repositioning
|
|
||||||
|
|
||||||
#### Stage 2B — Publishing workflow UX hardening
|
#### Stage 2B — Publishing workflow UX hardening
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,16 +59,20 @@ class ProductPreviewPanel extends StatefulWidget {
|
||||||
class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
||||||
bool _editingPrice = false;
|
bool _editingPrice = false;
|
||||||
late TextEditingController _priceController;
|
late TextEditingController _priceController;
|
||||||
|
String? _priceError;
|
||||||
|
|
||||||
bool _editingName = false;
|
bool _editingName = false;
|
||||||
late TextEditingController _nameController;
|
late TextEditingController _nameController;
|
||||||
|
String? _nameError;
|
||||||
|
|
||||||
bool _editingDescription = false;
|
bool _editingDescription = false;
|
||||||
late TextEditingController _descriptionController;
|
late TextEditingController _descriptionController;
|
||||||
|
String? _descriptionError;
|
||||||
|
|
||||||
// ignore: prefer_final_fields
|
// ignore: prefer_final_fields
|
||||||
bool _editingCategory = false;
|
bool _editingCategory = false;
|
||||||
late TextEditingController _categoryController;
|
late TextEditingController _categoryController;
|
||||||
|
String? _categoryError;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -84,20 +88,24 @@ class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (oldWidget.draft.id != widget.draft.id || oldWidget.draft.price != widget.draft.price) {
|
if (oldWidget.draft.id != widget.draft.id || oldWidget.draft.price != widget.draft.price) {
|
||||||
_editingPrice = false;
|
_editingPrice = false;
|
||||||
|
_priceError = null;
|
||||||
_priceController.text = widget.draft.price.toStringAsFixed(2);
|
_priceController.text = widget.draft.price.toStringAsFixed(2);
|
||||||
}
|
}
|
||||||
if (oldWidget.draft.id != widget.draft.id || oldWidget.draft.name != widget.draft.name) {
|
if (oldWidget.draft.id != widget.draft.id || oldWidget.draft.name != widget.draft.name) {
|
||||||
_editingName = false;
|
_editingName = false;
|
||||||
|
_nameError = null;
|
||||||
_nameController.text = widget.draft.name;
|
_nameController.text = widget.draft.name;
|
||||||
}
|
}
|
||||||
if (oldWidget.draft.id != widget.draft.id ||
|
if (oldWidget.draft.id != widget.draft.id ||
|
||||||
oldWidget.draft.description != widget.draft.description) {
|
oldWidget.draft.description != widget.draft.description) {
|
||||||
_editingDescription = false;
|
_editingDescription = false;
|
||||||
|
_descriptionError = null;
|
||||||
_descriptionController.text = widget.draft.description;
|
_descriptionController.text = widget.draft.description;
|
||||||
}
|
}
|
||||||
if (oldWidget.draft.id != widget.draft.id ||
|
if (oldWidget.draft.id != widget.draft.id ||
|
||||||
oldWidget.draft.category != widget.draft.category) {
|
oldWidget.draft.category != widget.draft.category) {
|
||||||
_editingCategory = false;
|
_editingCategory = false;
|
||||||
|
_categoryError = null;
|
||||||
_categoryController.text = widget.draft.category;
|
_categoryController.text = widget.draft.category;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -113,54 +121,86 @@ class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
||||||
|
|
||||||
void _submitPrice() {
|
void _submitPrice() {
|
||||||
final parsed = double.tryParse(_priceController.text);
|
final parsed = double.tryParse(_priceController.text);
|
||||||
if (parsed != null && parsed >= 0) {
|
if (parsed == null || parsed < 0) {
|
||||||
widget.onPriceChanged?.call(parsed);
|
setState(() => _priceError = 'Enter a valid price.');
|
||||||
setState(() => _editingPrice = false);
|
return;
|
||||||
}
|
}
|
||||||
|
setState(() {
|
||||||
|
_priceError = null;
|
||||||
|
_editingPrice = false;
|
||||||
|
});
|
||||||
|
widget.onPriceChanged?.call(parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _cancelEdit() {
|
void _cancelEdit() {
|
||||||
_priceController.text = widget.draft.price.toStringAsFixed(2);
|
_priceController.text = widget.draft.price.toStringAsFixed(2);
|
||||||
setState(() => _editingPrice = false);
|
setState(() {
|
||||||
|
_priceError = null;
|
||||||
|
_editingPrice = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _submitName() {
|
void _submitName() {
|
||||||
final trimmed = _nameController.text.trim();
|
final trimmed = _nameController.text.trim();
|
||||||
if (trimmed.isNotEmpty) {
|
if (trimmed.isEmpty) {
|
||||||
widget.onNameChanged?.call(trimmed);
|
setState(() => _nameError = 'Name cannot be empty.');
|
||||||
setState(() => _editingName = false);
|
return;
|
||||||
}
|
}
|
||||||
|
setState(() {
|
||||||
|
_nameError = null;
|
||||||
|
_editingName = false;
|
||||||
|
});
|
||||||
|
widget.onNameChanged?.call(trimmed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _cancelNameEdit() {
|
void _cancelNameEdit() {
|
||||||
_nameController.text = widget.draft.name;
|
_nameController.text = widget.draft.name;
|
||||||
setState(() => _editingName = false);
|
setState(() {
|
||||||
|
_nameError = null;
|
||||||
|
_editingName = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _submitDescription() {
|
void _submitDescription() {
|
||||||
final trimmed = _descriptionController.text.trim();
|
final trimmed = _descriptionController.text.trim();
|
||||||
if (trimmed.isNotEmpty) {
|
if (trimmed.isEmpty) {
|
||||||
widget.onDescriptionChanged?.call(trimmed);
|
setState(() => _descriptionError = 'Description cannot be empty.');
|
||||||
setState(() => _editingDescription = false);
|
return;
|
||||||
}
|
}
|
||||||
|
setState(() {
|
||||||
|
_descriptionError = null;
|
||||||
|
_editingDescription = false;
|
||||||
|
});
|
||||||
|
widget.onDescriptionChanged?.call(trimmed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _cancelDescriptionEdit() {
|
void _cancelDescriptionEdit() {
|
||||||
_descriptionController.text = widget.draft.description;
|
_descriptionController.text = widget.draft.description;
|
||||||
setState(() => _editingDescription = false);
|
setState(() {
|
||||||
|
_descriptionError = null;
|
||||||
|
_editingDescription = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _submitCategory() {
|
void _submitCategory() {
|
||||||
final trimmed = _categoryController.text.trim();
|
final trimmed = _categoryController.text.trim();
|
||||||
if (trimmed.isNotEmpty) {
|
if (trimmed.isEmpty) {
|
||||||
widget.onCategoryChanged?.call(trimmed);
|
setState(() => _categoryError = 'Category cannot be empty.');
|
||||||
setState(() => _editingCategory = false);
|
return;
|
||||||
}
|
}
|
||||||
|
setState(() {
|
||||||
|
_categoryError = null;
|
||||||
|
_editingCategory = false;
|
||||||
|
});
|
||||||
|
widget.onCategoryChanged?.call(trimmed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _cancelCategoryEdit() {
|
void _cancelCategoryEdit() {
|
||||||
_categoryController.text = widget.draft.category;
|
_categoryController.text = widget.draft.category;
|
||||||
setState(() => _editingCategory = false);
|
setState(() {
|
||||||
|
_categoryError = null;
|
||||||
|
_editingCategory = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -246,15 +286,20 @@ class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
||||||
final draft = widget.draft;
|
final draft = widget.draft;
|
||||||
|
|
||||||
if (_editingName) {
|
if (_editingName) {
|
||||||
return Row(
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _nameController,
|
controller: _nameController,
|
||||||
|
enabled: !widget.isUpdating,
|
||||||
style: theme.textTheme.headlineMedium,
|
style: theme.textTheme.headlineMedium,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
isDense: true,
|
isDense: true,
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
|
errorText: _nameError,
|
||||||
),
|
),
|
||||||
onSubmitted: (_) => _submitName(),
|
onSubmitted: (_) => _submitName(),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
|
|
@ -263,7 +308,7 @@ class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
||||||
const SizedBox(width: KcSpacing.xs),
|
const SizedBox(width: KcSpacing.xs),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.check, size: 20),
|
icon: const Icon(Icons.check, size: 20),
|
||||||
onPressed: _submitName,
|
onPressed: widget.isUpdating ? null : _submitName,
|
||||||
tooltip: 'Save name',
|
tooltip: 'Save name',
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
|
|
@ -271,12 +316,14 @@ class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
||||||
const SizedBox(width: KcSpacing.xs),
|
const SizedBox(width: KcSpacing.xs),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.close, size: 20),
|
icon: const Icon(Icons.close, size: 20),
|
||||||
onPressed: _cancelNameEdit,
|
onPressed: widget.isUpdating ? null : _cancelNameEdit,
|
||||||
tooltip: 'Cancel',
|
tooltip: 'Cancel',
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -308,7 +355,10 @@ class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
||||||
if (_editingPrice) {
|
if (_editingPrice) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: KcSpacing.xs),
|
padding: const EdgeInsets.symmetric(vertical: KcSpacing.xs),
|
||||||
child: Row(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 120,
|
width: 120,
|
||||||
|
|
@ -324,12 +374,14 @@ class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
||||||
width: 100,
|
width: 100,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _priceController,
|
controller: _priceController,
|
||||||
|
enabled: !widget.isUpdating,
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[\d.]'))],
|
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[\d.]'))],
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
prefixText: '\$ ',
|
prefixText: '\$ ',
|
||||||
isDense: true,
|
isDense: true,
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
|
errorText: _priceError,
|
||||||
),
|
),
|
||||||
onSubmitted: (_) => _submitPrice(),
|
onSubmitted: (_) => _submitPrice(),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
|
|
@ -338,7 +390,7 @@ class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
||||||
const SizedBox(width: KcSpacing.xs),
|
const SizedBox(width: KcSpacing.xs),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.check, size: 20),
|
icon: const Icon(Icons.check, size: 20),
|
||||||
onPressed: _submitPrice,
|
onPressed: widget.isUpdating ? null : _submitPrice,
|
||||||
tooltip: 'Save price',
|
tooltip: 'Save price',
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
|
|
@ -346,13 +398,15 @@ class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
||||||
const SizedBox(width: KcSpacing.xs),
|
const SizedBox(width: KcSpacing.xs),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.close, size: 20),
|
icon: const Icon(Icons.close, size: 20),
|
||||||
onPressed: _cancelEdit,
|
onPressed: widget.isUpdating ? null : _cancelEdit,
|
||||||
tooltip: 'Cancel',
|
tooltip: 'Cancel',
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -395,7 +449,10 @@ class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
||||||
if (_editingCategory) {
|
if (_editingCategory) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: KcSpacing.xs),
|
padding: const EdgeInsets.symmetric(vertical: KcSpacing.xs),
|
||||||
child: Row(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 120,
|
width: 120,
|
||||||
|
|
@ -410,9 +467,11 @@ class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _categoryController,
|
controller: _categoryController,
|
||||||
decoration: const InputDecoration(
|
enabled: !widget.isUpdating,
|
||||||
|
decoration: InputDecoration(
|
||||||
isDense: true,
|
isDense: true,
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
|
errorText: _categoryError,
|
||||||
),
|
),
|
||||||
onSubmitted: (_) => _submitCategory(),
|
onSubmitted: (_) => _submitCategory(),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
|
|
@ -421,7 +480,7 @@ class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
||||||
const SizedBox(width: KcSpacing.xs),
|
const SizedBox(width: KcSpacing.xs),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.check, size: 20),
|
icon: const Icon(Icons.check, size: 20),
|
||||||
onPressed: _submitCategory,
|
onPressed: widget.isUpdating ? null : _submitCategory,
|
||||||
tooltip: 'Save category',
|
tooltip: 'Save category',
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
|
|
@ -429,13 +488,15 @@ class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
||||||
const SizedBox(width: KcSpacing.xs),
|
const SizedBox(width: KcSpacing.xs),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.close, size: 20),
|
icon: const Icon(Icons.close, size: 20),
|
||||||
onPressed: _cancelCategoryEdit,
|
onPressed: widget.isUpdating ? null : _cancelCategoryEdit,
|
||||||
tooltip: 'Cancel',
|
tooltip: 'Cancel',
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -484,7 +545,7 @@ class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.check, size: 20),
|
icon: const Icon(Icons.check, size: 20),
|
||||||
onPressed: _submitDescription,
|
onPressed: widget.isUpdating ? null : _submitDescription,
|
||||||
tooltip: 'Save description',
|
tooltip: 'Save description',
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
|
|
@ -492,7 +553,7 @@ class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
||||||
const SizedBox(width: KcSpacing.xs),
|
const SizedBox(width: KcSpacing.xs),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.close, size: 20),
|
icon: const Icon(Icons.close, size: 20),
|
||||||
onPressed: _cancelDescriptionEdit,
|
onPressed: widget.isUpdating ? null : _cancelDescriptionEdit,
|
||||||
tooltip: 'Cancel description edit',
|
tooltip: 'Cancel description edit',
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
|
|
@ -502,13 +563,15 @@ class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
||||||
const SizedBox(height: KcSpacing.sm),
|
const SizedBox(height: KcSpacing.sm),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _descriptionController,
|
controller: _descriptionController,
|
||||||
|
enabled: !widget.isUpdating,
|
||||||
maxLines: 5,
|
maxLines: 5,
|
||||||
minLines: 3,
|
minLines: 3,
|
||||||
style: theme.textTheme.bodyLarge,
|
style: theme.textTheme.bodyLarge,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
isDense: true,
|
isDense: true,
|
||||||
contentPadding: EdgeInsets.all(8),
|
contentPadding: const EdgeInsets.all(8),
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
|
errorText: _descriptionError,
|
||||||
),
|
),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,12 @@ import 'package:flutter/material.dart';
|
||||||
import '../../application/product_publishing_controller.dart';
|
import '../../application/product_publishing_controller.dart';
|
||||||
import '../../domain/publish_status.dart';
|
import '../../domain/publish_status.dart';
|
||||||
|
|
||||||
|
/// Truncates [text] to [maxLength] characters, appending '…' if truncated.
|
||||||
|
String _truncate(String text, {int maxLength = 60}) {
|
||||||
|
if (text.length <= maxLength) return text;
|
||||||
|
return '${text.substring(0, maxLength)}…';
|
||||||
|
}
|
||||||
|
|
||||||
/// Shows a [SnackBar] for the given [StatusActionResult].
|
/// Shows a [SnackBar] for the given [StatusActionResult].
|
||||||
///
|
///
|
||||||
/// Uses [KcColors.success] / [KcColors.danger] to match the design system.
|
/// Uses [KcColors.success] / [KcColors.danger] to match the design system.
|
||||||
|
|
@ -23,7 +29,8 @@ void showStatusActionSnackBar(BuildContext context, StatusActionResult result) {
|
||||||
icon = Icons.check_circle_outline;
|
icon = Icons.check_circle_outline;
|
||||||
} else {
|
} else {
|
||||||
final verb = _infinitiveVerbForStatus(result.targetStatus);
|
final verb = _infinitiveVerbForStatus(result.targetStatus);
|
||||||
message = 'Failed to $verb ${result.productName}.';
|
final detail = result.errorMessage != null ? ' ${_truncate(result.errorMessage!)}' : '';
|
||||||
|
message = 'Failed to $verb ${result.productName}.$detail';
|
||||||
backgroundColor = KcColors.danger;
|
backgroundColor = KcColors.danger;
|
||||||
icon = Icons.error_outline;
|
icon = Icons.error_outline;
|
||||||
}
|
}
|
||||||
|
|
@ -55,11 +62,12 @@ void showDescriptionActionSnackBar(BuildContext context, DescriptionActionResult
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
message = '${result.productName} description updated.';
|
message = '${result.productName} description updated successfully.';
|
||||||
backgroundColor = KcColors.success;
|
backgroundColor = KcColors.success;
|
||||||
icon = Icons.check_circle_outline;
|
icon = Icons.check_circle_outline;
|
||||||
} else {
|
} else {
|
||||||
message = 'Failed to update description for ${result.productName}.';
|
final detail = result.errorMessage != null ? ' ${_truncate(result.errorMessage!)}' : '';
|
||||||
|
message = 'Failed to update description for ${result.productName}.$detail';
|
||||||
backgroundColor = KcColors.danger;
|
backgroundColor = KcColors.danger;
|
||||||
icon = Icons.error_outline;
|
icon = Icons.error_outline;
|
||||||
}
|
}
|
||||||
|
|
@ -119,11 +127,13 @@ void showPriceActionSnackBar(BuildContext context, PriceActionResult result) {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
message = '${result.productName} price updated to \$${result.newPrice.toStringAsFixed(2)}.';
|
message =
|
||||||
|
'${result.productName} price updated to \$${result.newPrice.toStringAsFixed(2)} successfully.';
|
||||||
backgroundColor = KcColors.success;
|
backgroundColor = KcColors.success;
|
||||||
icon = Icons.check_circle_outline;
|
icon = Icons.check_circle_outline;
|
||||||
} else {
|
} else {
|
||||||
message = 'Failed to update price for ${result.productName}.';
|
final detail = result.errorMessage != null ? ' ${_truncate(result.errorMessage!)}' : '';
|
||||||
|
message = 'Failed to update price for ${result.productName}.$detail';
|
||||||
backgroundColor = KcColors.danger;
|
backgroundColor = KcColors.danger;
|
||||||
icon = Icons.error_outline;
|
icon = Icons.error_outline;
|
||||||
}
|
}
|
||||||
|
|
@ -155,11 +165,12 @@ void showCategoryActionSnackBar(BuildContext context, CategoryActionResult resul
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
message = '${result.productName} category updated to ${result.newCategory}.';
|
message = '${result.productName} category updated to ${result.newCategory} successfully.';
|
||||||
backgroundColor = KcColors.success;
|
backgroundColor = KcColors.success;
|
||||||
icon = Icons.check_circle_outline;
|
icon = Icons.check_circle_outline;
|
||||||
} else {
|
} else {
|
||||||
message = 'Failed to update category for ${result.productName}.';
|
final detail = result.errorMessage != null ? ' ${_truncate(result.errorMessage!)}' : '';
|
||||||
|
message = 'Failed to update category for ${result.productName}.$detail';
|
||||||
backgroundColor = KcColors.danger;
|
backgroundColor = KcColors.danger;
|
||||||
icon = Icons.error_outline;
|
icon = Icons.error_outline;
|
||||||
}
|
}
|
||||||
|
|
@ -191,11 +202,12 @@ void showNameActionSnackBar(BuildContext context, NameActionResult result) {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
message = 'Product renamed to ${result.newName}.';
|
message = '${result.productName} renamed to ${result.newName} successfully.';
|
||||||
backgroundColor = KcColors.success;
|
backgroundColor = KcColors.success;
|
||||||
icon = Icons.check_circle_outline;
|
icon = Icons.check_circle_outline;
|
||||||
} else {
|
} else {
|
||||||
message = 'Failed to rename ${result.productName}.';
|
final detail = result.errorMessage != null ? ' ${_truncate(result.errorMessage!)}' : '';
|
||||||
|
message = 'Failed to rename ${result.productName}.$detail';
|
||||||
backgroundColor = KcColors.danger;
|
backgroundColor = KcColors.danger;
|
||||||
icon = Icons.error_outline;
|
icon = Icons.error_outline;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ void main() {
|
||||||
ValueChanged<double>? onPriceChanged,
|
ValueChanged<double>? onPriceChanged,
|
||||||
ValueChanged<String>? onNameChanged,
|
ValueChanged<String>? onNameChanged,
|
||||||
ValueChanged<String>? onDescriptionChanged,
|
ValueChanged<String>? onDescriptionChanged,
|
||||||
|
ValueChanged<String>? onCategoryChanged,
|
||||||
bool isUpdating = false,
|
bool isUpdating = false,
|
||||||
}) {
|
}) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
|
|
@ -86,6 +87,7 @@ void main() {
|
||||||
onPriceChanged: onPriceChanged,
|
onPriceChanged: onPriceChanged,
|
||||||
onNameChanged: onNameChanged,
|
onNameChanged: onNameChanged,
|
||||||
onDescriptionChanged: onDescriptionChanged,
|
onDescriptionChanged: onDescriptionChanged,
|
||||||
|
onCategoryChanged: onCategoryChanged,
|
||||||
isUpdating: isUpdating,
|
isUpdating: isUpdating,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -560,4 +562,221 @@ void main() {
|
||||||
expect(find.byType(TextField), findsOneWidget);
|
expect(find.byType(TextField), findsOneWidget);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Inline validation error messages (Stage 2B) ─────────────────────
|
||||||
|
|
||||||
|
group('inline validation errors', () {
|
||||||
|
testWidgets('shows error when submitting empty name', (tester) async {
|
||||||
|
await tester.pumpWidget(buildTestWidget(sampleDraft, onNameChanged: (_) {}));
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Edit name'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.enterText(find.byType(TextField), '');
|
||||||
|
await tester.tap(find.byTooltip('Save name'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('Name cannot be empty.'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('clears name error on cancel', (tester) async {
|
||||||
|
await tester.pumpWidget(buildTestWidget(sampleDraft, onNameChanged: (_) {}));
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Edit name'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Trigger the error.
|
||||||
|
await tester.enterText(find.byType(TextField), '');
|
||||||
|
await tester.tap(find.byTooltip('Save name'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Name cannot be empty.'), findsOneWidget);
|
||||||
|
|
||||||
|
// Cancel should clear the error and exit edit mode.
|
||||||
|
await tester.tap(find.byTooltip('Cancel'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Name cannot be empty.'), findsNothing);
|
||||||
|
expect(find.byType(TextField), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('shows error when submitting invalid price', (tester) async {
|
||||||
|
await tester.pumpWidget(buildTestWidget(sampleDraft, onPriceChanged: (_) {}));
|
||||||
|
|
||||||
|
await tester.tap(find.byIcon(Icons.edit));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Clear the field (empty string is not a valid price).
|
||||||
|
await tester.enterText(find.byType(TextField), '');
|
||||||
|
await tester.tap(find.byIcon(Icons.check));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('Enter a valid price.'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('clears price error on cancel', (tester) async {
|
||||||
|
await tester.pumpWidget(buildTestWidget(sampleDraft, onPriceChanged: (_) {}));
|
||||||
|
|
||||||
|
await tester.tap(find.byIcon(Icons.edit));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.enterText(find.byType(TextField), '');
|
||||||
|
await tester.tap(find.byIcon(Icons.check));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Enter a valid price.'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.byIcon(Icons.close));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Enter a valid price.'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('shows error when submitting empty description', (tester) async {
|
||||||
|
await tester.pumpWidget(buildTestWidget(sampleDraft, onDescriptionChanged: (_) {}));
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Edit description'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.enterText(find.byType(TextField), '');
|
||||||
|
await tester.tap(find.byTooltip('Save description'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('Description cannot be empty.'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('clears description error on cancel', (tester) async {
|
||||||
|
await tester.pumpWidget(buildTestWidget(sampleDraft, onDescriptionChanged: (_) {}));
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Edit description'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.enterText(find.byType(TextField), '');
|
||||||
|
await tester.tap(find.byTooltip('Save description'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Description cannot be empty.'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Cancel description edit'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Description cannot be empty.'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('shows error when submitting empty category', (tester) async {
|
||||||
|
await tester.pumpWidget(buildTestWidget(sampleDraft, onCategoryChanged: (_) {}));
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Edit category'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.enterText(find.byType(TextField), '');
|
||||||
|
await tester.tap(find.byTooltip('Save category'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('Category cannot be empty.'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('clears category error on cancel', (tester) async {
|
||||||
|
await tester.pumpWidget(buildTestWidget(sampleDraft, onCategoryChanged: (_) {}));
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Edit category'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.enterText(find.byType(TextField), '');
|
||||||
|
await tester.tap(find.byTooltip('Save category'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Category cannot be empty.'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Cancel'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Category cannot be empty.'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('valid input clears previous error', (tester) async {
|
||||||
|
String? receivedName;
|
||||||
|
await tester.pumpWidget(buildTestWidget(sampleDraft, onNameChanged: (n) => receivedName = n));
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Edit name'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Trigger error first.
|
||||||
|
await tester.enterText(find.byType(TextField), '');
|
||||||
|
await tester.tap(find.byTooltip('Save name'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Name cannot be empty.'), findsOneWidget);
|
||||||
|
|
||||||
|
// Now enter valid input and submit.
|
||||||
|
await tester.enterText(find.byType(TextField), 'Valid Name');
|
||||||
|
await tester.tap(find.byTooltip('Save name'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('Name cannot be empty.'), findsNothing);
|
||||||
|
expect(receivedName, 'Valid Name');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Disabled state during isUpdating (Stage 2B) ─────────────────────
|
||||||
|
|
||||||
|
group('edit fields disabled during isUpdating', () {
|
||||||
|
testWidgets('name text field disabled while updating', (tester) async {
|
||||||
|
// Start with isUpdating = false to enter edit mode.
|
||||||
|
await tester.pumpWidget(buildTestWidget(sampleDraft, onNameChanged: (_) {}));
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Edit name'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.byType(TextField), findsOneWidget);
|
||||||
|
|
||||||
|
// Rebuild with isUpdating = true.
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildTestWidget(sampleDraft, onNameChanged: (_) {}, isUpdating: true),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// The text field should be disabled.
|
||||||
|
final textField = tester.widget<TextField>(find.byType(TextField));
|
||||||
|
expect(textField.enabled, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('price text field disabled while updating', (tester) async {
|
||||||
|
await tester.pumpWidget(buildTestWidget(sampleDraft, onPriceChanged: (_) {}));
|
||||||
|
|
||||||
|
await tester.tap(find.byIcon(Icons.edit));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Rebuild with isUpdating = true.
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildTestWidget(sampleDraft, onPriceChanged: (_) {}, isUpdating: true),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final textField = tester.widget<TextField>(find.byType(TextField));
|
||||||
|
expect(textField.enabled, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('description text field disabled while updating', (tester) async {
|
||||||
|
await tester.pumpWidget(buildTestWidget(sampleDraft, onDescriptionChanged: (_) {}));
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Edit description'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Rebuild with isUpdating = true.
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildTestWidget(sampleDraft, onDescriptionChanged: (_) {}, isUpdating: true),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final textField = tester.widget<TextField>(find.byType(TextField));
|
||||||
|
expect(textField.enabled, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('category text field disabled while updating', (tester) async {
|
||||||
|
await tester.pumpWidget(buildTestWidget(sampleDraft, onCategoryChanged: (_) {}));
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Edit category'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Rebuild with isUpdating = true.
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildTestWidget(sampleDraft, onCategoryChanged: (_) {}, isUpdating: true),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final textField = tester.widget<TextField>(find.byType(TextField));
|
||||||
|
expect(textField.enabled, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ void main() {
|
||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.text('Failed to publish Floral Bowl Cozy.'), findsOneWidget);
|
expect(find.text('Failed to publish Floral Bowl Cozy. Network error'), findsOneWidget);
|
||||||
expect(find.byIcon(Icons.error_outline), findsOneWidget);
|
expect(find.byIcon(Icons.error_outline), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -130,7 +130,7 @@ void main() {
|
||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.text('Failed to move to draft Ocean Nightlight.'), findsOneWidget);
|
expect(find.text('Failed to move to draft Ocean Nightlight. Server error'), findsOneWidget);
|
||||||
expect(find.byIcon(Icons.error_outline), findsOneWidget);
|
expect(find.byIcon(Icons.error_outline), findsOneWidget);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue