Compare commits
No commits in common. "b81016df280a74fb9378d416cde3dbde8320774a" and "7acff83bf4eff5a13f89c762357d6b771be32dec" have entirely different histories.
b81016df28
...
7acff83bf4
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
## Current status
|
## Current status
|
||||||
|
|
||||||
- main baseline updated through: post-write-consistency
|
- main baseline updated through: category-only-edit
|
||||||
- main baseline commit: `7acff83` (2026-04-11)
|
- main baseline commit: `8e7e4cb` (2026-04-11)
|
||||||
- next branch: feat/publishing-ux-hardening
|
- next branch: feat/post-write-consistency
|
||||||
- current stage: Stage 2 — Web application operational hardening
|
- current stage: Stage 2 — Web application operational hardening
|
||||||
|
|
||||||
## Slice tracker
|
## Slice tracker
|
||||||
|
|
@ -31,8 +31,7 @@
|
||||||
|
|
||||||
### feat/post-write-consistency
|
### feat/post-write-consistency
|
||||||
|
|
||||||
- status: merged to main
|
- status: implemented — ready for review / merge
|
||||||
- commit: `7acff83`
|
|
||||||
- inspection: complete
|
- inspection: complete
|
||||||
- implementation: complete
|
- implementation: complete
|
||||||
- files changed:
|
- files changed:
|
||||||
|
|
@ -41,17 +40,3 @@
|
||||||
- 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,7 +79,6 @@ 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`
|
||||||
|
|
||||||
|
|
@ -94,22 +93,21 @@ 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`: `234/234 passed`
|
- latest reported count for `feature_wordpress`: `223/223 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: `7acff83` (2026-04-11)
|
- baseline commit: `8e7e4cb` (2026-04-11)
|
||||||
|
|
||||||
### In-progress branch
|
### Post-write consistency (Stage 2A) — on branch `feat/post-write-consistency`
|
||||||
|
|
||||||
**`feat/publishing-ux-hardening`** — Stage 2B: Publishing workflow UX hardening.
|
|
||||||
Branch from `main` at `7acff83`. Implementation complete, pending merge.
|
|
||||||
|
|
||||||
- `dart analyze` clean
|
- `dart analyze` clean
|
||||||
- `feature_wordpress` tests: `247/247 passed` (13 new tests added)
|
- `feature_wordpress` tests: `234/234 passed`
|
||||||
- Ready for PR into `main`.
|
- controller `_refreshSelection()` preserves/refreshes selection after all writes
|
||||||
|
- 11 new post-write consistency tests added
|
||||||
|
|
||||||
### Next recommended branch (after 2B merges)
|
### Next recommended branch
|
||||||
|
|
||||||
**`feat/multi-select-groundwork`** — Stage 3A: Multi-select groundwork (read/state only first).
|
**`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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -168,10 +166,24 @@ 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~~ ✅ COMPLETE
|
#### Stage 2A — Post-write consistency hardening
|
||||||
|
|
||||||
> Merged `feat/post-write-consistency` → `main` at `7acff83` (2026-04-11).
|
##### Goal
|
||||||
> 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,20 +59,16 @@ 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() {
|
||||||
|
|
@ -88,24 +84,20 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -121,86 +113,54 @@ 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) {
|
||||||
setState(() => _priceError = 'Enter a valid price.');
|
widget.onPriceChanged?.call(parsed);
|
||||||
return;
|
setState(() => _editingPrice = false);
|
||||||
}
|
}
|
||||||
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(() {
|
setState(() => _editingPrice = false);
|
||||||
_priceError = null;
|
|
||||||
_editingPrice = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _submitName() {
|
void _submitName() {
|
||||||
final trimmed = _nameController.text.trim();
|
final trimmed = _nameController.text.trim();
|
||||||
if (trimmed.isEmpty) {
|
if (trimmed.isNotEmpty) {
|
||||||
setState(() => _nameError = 'Name cannot be empty.');
|
widget.onNameChanged?.call(trimmed);
|
||||||
return;
|
setState(() => _editingName = false);
|
||||||
}
|
}
|
||||||
setState(() {
|
|
||||||
_nameError = null;
|
|
||||||
_editingName = false;
|
|
||||||
});
|
|
||||||
widget.onNameChanged?.call(trimmed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _cancelNameEdit() {
|
void _cancelNameEdit() {
|
||||||
_nameController.text = widget.draft.name;
|
_nameController.text = widget.draft.name;
|
||||||
setState(() {
|
setState(() => _editingName = false);
|
||||||
_nameError = null;
|
|
||||||
_editingName = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _submitDescription() {
|
void _submitDescription() {
|
||||||
final trimmed = _descriptionController.text.trim();
|
final trimmed = _descriptionController.text.trim();
|
||||||
if (trimmed.isEmpty) {
|
if (trimmed.isNotEmpty) {
|
||||||
setState(() => _descriptionError = 'Description cannot be empty.');
|
widget.onDescriptionChanged?.call(trimmed);
|
||||||
return;
|
setState(() => _editingDescription = false);
|
||||||
}
|
}
|
||||||
setState(() {
|
|
||||||
_descriptionError = null;
|
|
||||||
_editingDescription = false;
|
|
||||||
});
|
|
||||||
widget.onDescriptionChanged?.call(trimmed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _cancelDescriptionEdit() {
|
void _cancelDescriptionEdit() {
|
||||||
_descriptionController.text = widget.draft.description;
|
_descriptionController.text = widget.draft.description;
|
||||||
setState(() {
|
setState(() => _editingDescription = false);
|
||||||
_descriptionError = null;
|
|
||||||
_editingDescription = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _submitCategory() {
|
void _submitCategory() {
|
||||||
final trimmed = _categoryController.text.trim();
|
final trimmed = _categoryController.text.trim();
|
||||||
if (trimmed.isEmpty) {
|
if (trimmed.isNotEmpty) {
|
||||||
setState(() => _categoryError = 'Category cannot be empty.');
|
widget.onCategoryChanged?.call(trimmed);
|
||||||
return;
|
setState(() => _editingCategory = false);
|
||||||
}
|
}
|
||||||
setState(() {
|
|
||||||
_categoryError = null;
|
|
||||||
_editingCategory = false;
|
|
||||||
});
|
|
||||||
widget.onCategoryChanged?.call(trimmed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _cancelCategoryEdit() {
|
void _cancelCategoryEdit() {
|
||||||
_categoryController.text = widget.draft.category;
|
_categoryController.text = widget.draft.category;
|
||||||
setState(() {
|
setState(() => _editingCategory = false);
|
||||||
_categoryError = null;
|
|
||||||
_editingCategory = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -286,42 +246,35 @@ class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
||||||
final draft = widget.draft;
|
final draft = widget.draft;
|
||||||
|
|
||||||
if (_editingName) {
|
if (_editingName) {
|
||||||
return Column(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Expanded(
|
||||||
children: [
|
child: TextField(
|
||||||
Expanded(
|
controller: _nameController,
|
||||||
child: TextField(
|
style: theme.textTheme.headlineMedium,
|
||||||
controller: _nameController,
|
decoration: const InputDecoration(
|
||||||
enabled: !widget.isUpdating,
|
isDense: true,
|
||||||
style: theme.textTheme.headlineMedium,
|
contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
decoration: InputDecoration(
|
|
||||||
isDense: true,
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
|
||||||
errorText: _nameError,
|
|
||||||
),
|
|
||||||
onSubmitted: (_) => _submitName(),
|
|
||||||
autofocus: true,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: KcSpacing.xs),
|
onSubmitted: (_) => _submitName(),
|
||||||
IconButton(
|
autofocus: true,
|
||||||
icon: const Icon(Icons.check, size: 20),
|
),
|
||||||
onPressed: widget.isUpdating ? null : _submitName,
|
),
|
||||||
tooltip: 'Save name',
|
const SizedBox(width: KcSpacing.xs),
|
||||||
padding: EdgeInsets.zero,
|
IconButton(
|
||||||
constraints: const BoxConstraints(),
|
icon: const Icon(Icons.check, size: 20),
|
||||||
),
|
onPressed: _submitName,
|
||||||
const SizedBox(width: KcSpacing.xs),
|
tooltip: 'Save name',
|
||||||
IconButton(
|
padding: EdgeInsets.zero,
|
||||||
icon: const Icon(Icons.close, size: 20),
|
constraints: const BoxConstraints(),
|
||||||
onPressed: widget.isUpdating ? null : _cancelNameEdit,
|
),
|
||||||
tooltip: 'Cancel',
|
const SizedBox(width: KcSpacing.xs),
|
||||||
padding: EdgeInsets.zero,
|
IconButton(
|
||||||
constraints: const BoxConstraints(),
|
icon: const Icon(Icons.close, size: 20),
|
||||||
),
|
onPressed: _cancelNameEdit,
|
||||||
],
|
tooltip: 'Cancel',
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
@ -355,55 +308,48 @@ 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: Column(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
Row(
|
SizedBox(
|
||||||
children: [
|
width: 120,
|
||||||
SizedBox(
|
child: Text(
|
||||||
width: 120,
|
'Price',
|
||||||
child: Text(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
'Price',
|
color: KcColors.neutral,
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
fontWeight: FontWeight.w600,
|
||||||
color: KcColors.neutral,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
SizedBox(
|
),
|
||||||
width: 100,
|
),
|
||||||
child: TextField(
|
SizedBox(
|
||||||
controller: _priceController,
|
width: 100,
|
||||||
enabled: !widget.isUpdating,
|
child: TextField(
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
controller: _priceController,
|
||||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[\d.]'))],
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
decoration: InputDecoration(
|
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[\d.]'))],
|
||||||
prefixText: '\$ ',
|
decoration: const InputDecoration(
|
||||||
isDense: true,
|
prefixText: '\$ ',
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
isDense: true,
|
||||||
errorText: _priceError,
|
contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
),
|
|
||||||
onSubmitted: (_) => _submitPrice(),
|
|
||||||
autofocus: true,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: KcSpacing.xs),
|
onSubmitted: (_) => _submitPrice(),
|
||||||
IconButton(
|
autofocus: true,
|
||||||
icon: const Icon(Icons.check, size: 20),
|
),
|
||||||
onPressed: widget.isUpdating ? null : _submitPrice,
|
),
|
||||||
tooltip: 'Save price',
|
const SizedBox(width: KcSpacing.xs),
|
||||||
padding: EdgeInsets.zero,
|
IconButton(
|
||||||
constraints: const BoxConstraints(),
|
icon: const Icon(Icons.check, size: 20),
|
||||||
),
|
onPressed: _submitPrice,
|
||||||
const SizedBox(width: KcSpacing.xs),
|
tooltip: 'Save price',
|
||||||
IconButton(
|
padding: EdgeInsets.zero,
|
||||||
icon: const Icon(Icons.close, size: 20),
|
constraints: const BoxConstraints(),
|
||||||
onPressed: widget.isUpdating ? null : _cancelEdit,
|
),
|
||||||
tooltip: 'Cancel',
|
const SizedBox(width: KcSpacing.xs),
|
||||||
padding: EdgeInsets.zero,
|
IconButton(
|
||||||
constraints: const BoxConstraints(),
|
icon: const Icon(Icons.close, size: 20),
|
||||||
),
|
onPressed: _cancelEdit,
|
||||||
],
|
tooltip: 'Cancel',
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -449,51 +395,44 @@ 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: Column(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
Row(
|
SizedBox(
|
||||||
children: [
|
width: 120,
|
||||||
SizedBox(
|
child: Text(
|
||||||
width: 120,
|
'Category',
|
||||||
child: Text(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
'Category',
|
color: KcColors.neutral,
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
fontWeight: FontWeight.w600,
|
||||||
color: KcColors.neutral,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Expanded(
|
),
|
||||||
child: TextField(
|
),
|
||||||
controller: _categoryController,
|
Expanded(
|
||||||
enabled: !widget.isUpdating,
|
child: TextField(
|
||||||
decoration: InputDecoration(
|
controller: _categoryController,
|
||||||
isDense: true,
|
decoration: const InputDecoration(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
isDense: true,
|
||||||
errorText: _categoryError,
|
contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
),
|
|
||||||
onSubmitted: (_) => _submitCategory(),
|
|
||||||
autofocus: true,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: KcSpacing.xs),
|
onSubmitted: (_) => _submitCategory(),
|
||||||
IconButton(
|
autofocus: true,
|
||||||
icon: const Icon(Icons.check, size: 20),
|
),
|
||||||
onPressed: widget.isUpdating ? null : _submitCategory,
|
),
|
||||||
tooltip: 'Save category',
|
const SizedBox(width: KcSpacing.xs),
|
||||||
padding: EdgeInsets.zero,
|
IconButton(
|
||||||
constraints: const BoxConstraints(),
|
icon: const Icon(Icons.check, size: 20),
|
||||||
),
|
onPressed: _submitCategory,
|
||||||
const SizedBox(width: KcSpacing.xs),
|
tooltip: 'Save category',
|
||||||
IconButton(
|
padding: EdgeInsets.zero,
|
||||||
icon: const Icon(Icons.close, size: 20),
|
constraints: const BoxConstraints(),
|
||||||
onPressed: widget.isUpdating ? null : _cancelCategoryEdit,
|
),
|
||||||
tooltip: 'Cancel',
|
const SizedBox(width: KcSpacing.xs),
|
||||||
padding: EdgeInsets.zero,
|
IconButton(
|
||||||
constraints: const BoxConstraints(),
|
icon: const Icon(Icons.close, size: 20),
|
||||||
),
|
onPressed: _cancelCategoryEdit,
|
||||||
],
|
tooltip: 'Cancel',
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -545,7 +484,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: widget.isUpdating ? null : _submitDescription,
|
onPressed: _submitDescription,
|
||||||
tooltip: 'Save description',
|
tooltip: 'Save description',
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
|
|
@ -553,7 +492,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: widget.isUpdating ? null : _cancelDescriptionEdit,
|
onPressed: _cancelDescriptionEdit,
|
||||||
tooltip: 'Cancel description edit',
|
tooltip: 'Cancel description edit',
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
|
|
@ -563,15 +502,13 @@ 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: InputDecoration(
|
decoration: const InputDecoration(
|
||||||
isDense: true,
|
isDense: true,
|
||||||
contentPadding: const EdgeInsets.all(8),
|
contentPadding: EdgeInsets.all(8),
|
||||||
border: const OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
errorText: _descriptionError,
|
|
||||||
),
|
),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,6 @@ 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.
|
||||||
|
|
@ -29,8 +23,7 @@ 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);
|
||||||
final detail = result.errorMessage != null ? ' ${_truncate(result.errorMessage!)}' : '';
|
message = 'Failed to $verb ${result.productName}.';
|
||||||
message = 'Failed to $verb ${result.productName}.$detail';
|
|
||||||
backgroundColor = KcColors.danger;
|
backgroundColor = KcColors.danger;
|
||||||
icon = Icons.error_outline;
|
icon = Icons.error_outline;
|
||||||
}
|
}
|
||||||
|
|
@ -62,12 +55,11 @@ void showDescriptionActionSnackBar(BuildContext context, DescriptionActionResult
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
message = '${result.productName} description updated successfully.';
|
message = '${result.productName} description updated.';
|
||||||
backgroundColor = KcColors.success;
|
backgroundColor = KcColors.success;
|
||||||
icon = Icons.check_circle_outline;
|
icon = Icons.check_circle_outline;
|
||||||
} else {
|
} else {
|
||||||
final detail = result.errorMessage != null ? ' ${_truncate(result.errorMessage!)}' : '';
|
message = 'Failed to update description for ${result.productName}.';
|
||||||
message = 'Failed to update description for ${result.productName}.$detail';
|
|
||||||
backgroundColor = KcColors.danger;
|
backgroundColor = KcColors.danger;
|
||||||
icon = Icons.error_outline;
|
icon = Icons.error_outline;
|
||||||
}
|
}
|
||||||
|
|
@ -127,13 +119,11 @@ void showPriceActionSnackBar(BuildContext context, PriceActionResult result) {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
message =
|
message = '${result.productName} price updated to \$${result.newPrice.toStringAsFixed(2)}.';
|
||||||
'${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 {
|
||||||
final detail = result.errorMessage != null ? ' ${_truncate(result.errorMessage!)}' : '';
|
message = 'Failed to update price for ${result.productName}.';
|
||||||
message = 'Failed to update price for ${result.productName}.$detail';
|
|
||||||
backgroundColor = KcColors.danger;
|
backgroundColor = KcColors.danger;
|
||||||
icon = Icons.error_outline;
|
icon = Icons.error_outline;
|
||||||
}
|
}
|
||||||
|
|
@ -165,12 +155,11 @@ 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} successfully.';
|
message = '${result.productName} category updated to ${result.newCategory}.';
|
||||||
backgroundColor = KcColors.success;
|
backgroundColor = KcColors.success;
|
||||||
icon = Icons.check_circle_outline;
|
icon = Icons.check_circle_outline;
|
||||||
} else {
|
} else {
|
||||||
final detail = result.errorMessage != null ? ' ${_truncate(result.errorMessage!)}' : '';
|
message = 'Failed to update category for ${result.productName}.';
|
||||||
message = 'Failed to update category for ${result.productName}.$detail';
|
|
||||||
backgroundColor = KcColors.danger;
|
backgroundColor = KcColors.danger;
|
||||||
icon = Icons.error_outline;
|
icon = Icons.error_outline;
|
||||||
}
|
}
|
||||||
|
|
@ -202,12 +191,11 @@ void showNameActionSnackBar(BuildContext context, NameActionResult result) {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
message = '${result.productName} renamed to ${result.newName} successfully.';
|
message = 'Product renamed to ${result.newName}.';
|
||||||
backgroundColor = KcColors.success;
|
backgroundColor = KcColors.success;
|
||||||
icon = Icons.check_circle_outline;
|
icon = Icons.check_circle_outline;
|
||||||
} else {
|
} else {
|
||||||
final detail = result.errorMessage != null ? ' ${_truncate(result.errorMessage!)}' : '';
|
message = 'Failed to rename ${result.productName}.';
|
||||||
message = 'Failed to rename ${result.productName}.$detail';
|
|
||||||
backgroundColor = KcColors.danger;
|
backgroundColor = KcColors.danger;
|
||||||
icon = Icons.error_outline;
|
icon = Icons.error_outline;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,6 @@ 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(
|
||||||
|
|
@ -87,7 +86,6 @@ void main() {
|
||||||
onPriceChanged: onPriceChanged,
|
onPriceChanged: onPriceChanged,
|
||||||
onNameChanged: onNameChanged,
|
onNameChanged: onNameChanged,
|
||||||
onDescriptionChanged: onDescriptionChanged,
|
onDescriptionChanged: onDescriptionChanged,
|
||||||
onCategoryChanged: onCategoryChanged,
|
|
||||||
isUpdating: isUpdating,
|
isUpdating: isUpdating,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -562,221 +560,4 @@ 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. Network error'), findsOneWidget);
|
expect(find.text('Failed to publish Floral Bowl Cozy.'), 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. Server error'), findsOneWidget);
|
expect(find.text('Failed to move to draft Ocean Nightlight.'), findsOneWidget);
|
||||||
expect(find.byIcon(Icons.error_outline), findsOneWidget);
|
expect(find.byIcon(Icons.error_outline), findsOneWidget);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue