test(client): 出入库状态多选语义与默认仓库填充回归用例
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,13 +2,18 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:jiu_client/core/api/api_client.dart';
|
||||
import 'package:jiu_client/core/auth/auth_state.dart';
|
||||
import 'package:jiu_client/core/theme/themes.dart';
|
||||
import 'package:jiu_client/models/product_option.dart';
|
||||
import 'package:jiu_client/models/stock_in.dart';
|
||||
import 'package:jiu_client/models/warehouse.dart';
|
||||
import 'package:jiu_client/providers/stock_in_provider.dart';
|
||||
import 'package:jiu_client/providers/warehouse_provider.dart';
|
||||
import 'package:jiu_client/providers/product_option_provider.dart';
|
||||
import 'package:jiu_client/repositories/stock_in_repository.dart';
|
||||
import 'package:jiu_client/screens/stock_in/stock_in_form_screen.dart';
|
||||
import 'package:jiu_client/widgets/ds/grid_combo_cell.dart';
|
||||
|
||||
class _FakeWarehouses extends WarehouseListNotifier {
|
||||
@override
|
||||
@@ -16,6 +21,23 @@ class _FakeWarehouses extends WarehouseListNotifier {
|
||||
const [Warehouse(id: 1, name: '主仓', isDefault: true)];
|
||||
}
|
||||
|
||||
/// 可配置仓库列表的假 notifier(T5 默认仓填充回归用)。
|
||||
class _ConfigurableWarehouses extends WarehouseListNotifier {
|
||||
final List<Warehouse> _list;
|
||||
_ConfigurableWarehouses(this._list);
|
||||
@override
|
||||
Future<List<Warehouse>> build() async => _list;
|
||||
}
|
||||
|
||||
/// 编辑模式假仓库:get() 返回固定单据(供 _loadEditOrder 消费)。
|
||||
class _FakeStockInRepoForEdit extends StockInRepository {
|
||||
final StockInOrder order;
|
||||
_FakeStockInRepoForEdit(this.order) : super(ApiClient(token: 't'));
|
||||
|
||||
@override
|
||||
Future<StockInOrder> get(int id) async => order;
|
||||
}
|
||||
|
||||
class _FakeNames extends ProductNameListNotifier {
|
||||
@override
|
||||
Future<List<ProductNameOption>> build() async => const [
|
||||
@@ -85,4 +107,102 @@ void main() {
|
||||
expect(find.text('选择商品名称'), findsNothing,
|
||||
reason: 'hint should be replaced by the picked name');
|
||||
});
|
||||
|
||||
// ── T5:新建入库单默认填充默认仓库(2026-07-14)──────────────────────
|
||||
//
|
||||
// _initWarehouseDefault 语义:仓库列表加载完成后,
|
||||
// isDefault 的仓 优先;无默认时仅剩 1 个仓才取它;否则保持未选(null)。
|
||||
// 编辑模式不受影响(_loadEditOrder 已从单据自身仓库回填)。
|
||||
|
||||
int? _warehouseSelectedId(WidgetTester tester) => tester
|
||||
.widgetList<GridComboCell>(find.byType(GridComboCell))
|
||||
.firstWhere((c) => c.hint == '选择仓库…')
|
||||
.selectedId;
|
||||
|
||||
Future<void> _pumpForm(WidgetTester tester, List<Override> overrides,
|
||||
{int? editOrderId}) async {
|
||||
tester.view.physicalSize = const Size(1400, 1000);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
await tester.pumpWidget(ProviderScope(
|
||||
overrides: overrides,
|
||||
child: MaterialApp(
|
||||
theme: buildTheme('a'),
|
||||
home: Scaffold(body: StockInFormScreen(editOrderId: editOrderId)),
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
// _initWarehouseDefault / _loadEditOrder 都是异步(await provider.future),
|
||||
// 多 pump 几拍确保 setState 落地。
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
}
|
||||
|
||||
testWidgets('新建入库单:有默认仓(isDefault=true)时自动选中默认仓',
|
||||
(tester) async {
|
||||
await _pumpForm(tester, [
|
||||
warehouseListProvider.overrideWith(() => _FakeWarehouses()), // 主仓 isDefault
|
||||
productNameListProvider.overrideWith(() => _FakeNames()),
|
||||
productSeriesListProvider.overrideWith(() => _FakeSeries()),
|
||||
productSpecListProvider.overrideWith(() => _FakeSpecs()),
|
||||
isReadonlyProvider.overrideWithValue(false),
|
||||
]);
|
||||
expect(_warehouseSelectedId(tester), 1);
|
||||
});
|
||||
|
||||
testWidgets('新建入库单:无默认仓且多仓时保持未选', (tester) async {
|
||||
await _pumpForm(tester, [
|
||||
warehouseListProvider.overrideWith(() => _ConfigurableWarehouses(const [
|
||||
Warehouse(id: 1, name: '主仓', isDefault: false),
|
||||
Warehouse(id: 2, name: '分仓', isDefault: false),
|
||||
])),
|
||||
productNameListProvider.overrideWith(() => _FakeNames()),
|
||||
productSeriesListProvider.overrideWith(() => _FakeSeries()),
|
||||
productSpecListProvider.overrideWith(() => _FakeSpecs()),
|
||||
isReadonlyProvider.overrideWithValue(false),
|
||||
]);
|
||||
expect(_warehouseSelectedId(tester), isNull);
|
||||
});
|
||||
|
||||
testWidgets('新建入库单:无默认仓但仅有 1 个仓时自动选中该仓', (tester) async {
|
||||
await _pumpForm(tester, [
|
||||
warehouseListProvider.overrideWith(() => _ConfigurableWarehouses(const [
|
||||
Warehouse(id: 7, name: '唯一仓', isDefault: false),
|
||||
])),
|
||||
productNameListProvider.overrideWith(() => _FakeNames()),
|
||||
productSeriesListProvider.overrideWith(() => _FakeSeries()),
|
||||
productSpecListProvider.overrideWith(() => _FakeSpecs()),
|
||||
isReadonlyProvider.overrideWithValue(false),
|
||||
]);
|
||||
expect(_warehouseSelectedId(tester), 7);
|
||||
});
|
||||
|
||||
testWidgets('编辑模式:不被默认仓覆盖,仍显示单据原仓库', (tester) async {
|
||||
const order = StockInOrder(
|
||||
id: 99,
|
||||
orderNo: 'SI2024010099',
|
||||
warehouseId: 2, // 单据原仓库 = 分仓(非默认仓)
|
||||
status: 'draft',
|
||||
items: [],
|
||||
);
|
||||
await _pumpForm(
|
||||
tester,
|
||||
[
|
||||
// 主仓(id=1)isDefault=true,若默认仓填充逻辑误在编辑模式生效会把它选中,
|
||||
// 但单据自身仓库是 id=2,回归断言二者不一致时以单据为准。
|
||||
warehouseListProvider.overrideWith(() => _ConfigurableWarehouses(const [
|
||||
Warehouse(id: 1, name: '主仓', isDefault: true),
|
||||
Warehouse(id: 2, name: '分仓', isDefault: false),
|
||||
])),
|
||||
stockInRepositoryProvider
|
||||
.overrideWithValue(_FakeStockInRepoForEdit(order)),
|
||||
productNameListProvider.overrideWith(() => _FakeNames()),
|
||||
productSeriesListProvider.overrideWith(() => _FakeSeries()),
|
||||
productSpecListProvider.overrideWith(() => _FakeSpecs()),
|
||||
isReadonlyProvider.overrideWithValue(false),
|
||||
],
|
||||
editOrderId: 99,
|
||||
);
|
||||
expect(_warehouseSelectedId(tester), 2);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:jiu_client/core/api/api_client.dart';
|
||||
import 'package:jiu_client/core/models/page_result.dart';
|
||||
import 'package:jiu_client/core/theme/themes.dart';
|
||||
import 'package:jiu_client/models/stock_in.dart';
|
||||
import 'package:jiu_client/providers/license_provider.dart';
|
||||
import 'package:jiu_client/providers/stock_in_provider.dart';
|
||||
import 'package:jiu_client/repositories/stock_in_repository.dart';
|
||||
import 'package:jiu_client/screens/stock_in/stock_in_list_screen.dart';
|
||||
import 'package:jiu_client/widgets/ds/ds_atoms.dart';
|
||||
|
||||
/// 出入库状态多选语义回归(T4)——深测入库屏,出库屏走冒烟(见
|
||||
/// stock_out_status_filter_smoke_test.dart),两屏 initState 归一逻辑同构。
|
||||
///
|
||||
/// 背景:状态筛选 2026-07-14 改多选 + 默认「草稿+待审核」,initState 里逗号拆分
|
||||
/// 逐值校验归一的逻辑历史上出过回归(默认值刚设就被误清)。这里只测逻辑(provider
|
||||
/// 状态归一 / 多选交互结果),不碰像素。
|
||||
|
||||
/// 授权检查覆写:同步返回正常授权,避免 WriteGuard 命中真实网络请求。
|
||||
class _FakeLicenseNotifier extends LicenseNotifier {
|
||||
@override
|
||||
Future<LicenseInfo?> build() async => const LicenseInfo(
|
||||
id: 1,
|
||||
type: 'annual',
|
||||
isActive: true,
|
||||
maxDevices: 3,
|
||||
phase: 'normal',
|
||||
);
|
||||
}
|
||||
|
||||
/// 记录 provider 收到的 setStatus/setDetail 调用参数的假 notifier;
|
||||
/// build() 直接返回空列表数据、其余交互 setter 全部 no-op 避免命中真实网络。
|
||||
/// [initialStatus] 模拟「进屏前 provider 里已生效的状态筛选」,
|
||||
/// 用于驱动 initState 归一分支(关键词/详细搜索本组测试不涉及,固定空)。
|
||||
class _RecordingStockInNotifier extends StockInListNotifier {
|
||||
final String initialStatus;
|
||||
|
||||
String? lastSetStatus;
|
||||
Map<String, String>? lastSetDetail;
|
||||
|
||||
_RecordingStockInNotifier({this.initialStatus = 'draft,pending'});
|
||||
|
||||
@override
|
||||
Future<PageResult<StockInOrder>> build() async {
|
||||
state = const AsyncValue.data(
|
||||
PageResult<StockInOrder>(data: [], total: 0, page: 1, pageSize: 20));
|
||||
return state.value!;
|
||||
}
|
||||
|
||||
@override
|
||||
String get currentStatus => initialStatus;
|
||||
@override
|
||||
String get currentKeyword => '';
|
||||
@override
|
||||
Map<String, String> get currentDetail => const {};
|
||||
|
||||
@override
|
||||
void reload() {}
|
||||
@override
|
||||
void setPage(int page) {}
|
||||
@override
|
||||
void setPageSize(int pageSize) {}
|
||||
@override
|
||||
void setKeyword(String keyword) {}
|
||||
@override
|
||||
void setDateRange(String? startDate, String? endDate) {}
|
||||
|
||||
@override
|
||||
void setStatus(String status) {
|
||||
lastSetStatus = status;
|
||||
}
|
||||
|
||||
@override
|
||||
void setDetail(Map<String, String> detail) {
|
||||
lastSetDetail = detail;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _appWith(_RecordingStockInNotifier notifier) {
|
||||
final router = GoRouter(
|
||||
initialLocation: '/stock-in',
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/stock-in',
|
||||
builder: (_, __) => const Scaffold(body: StockInListScreen()),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/stock-in/new',
|
||||
builder: (_, __) => const Scaffold(body: Text('new order form')),
|
||||
),
|
||||
],
|
||||
);
|
||||
return ProviderScope(
|
||||
overrides: [
|
||||
licenseProvider.overrideWith(() => _FakeLicenseNotifier()),
|
||||
stockInListProvider.overrideWith(() => notifier),
|
||||
],
|
||||
child: MaterialApp.router(theme: buildTheme('a'), routerConfig: router),
|
||||
);
|
||||
}
|
||||
|
||||
void _bigView(WidgetTester tester) {
|
||||
tester.view.physicalSize = const Size(1400, 1000);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
}
|
||||
|
||||
/// 工具栏「状态」DsChip(多选摘要 chip,唯一 label='状态')。
|
||||
DsChip _statusChip(WidgetTester tester) => tester
|
||||
.widgetList<DsChip>(find.byType(DsChip))
|
||||
.firstWhere((c) => c.label == '状态');
|
||||
|
||||
/// 统计 list() 调用参数的假仓库,用于 provider 单测(不经过屏幕)。
|
||||
class _CountingStockInRepo extends StockInRepository {
|
||||
String? lastStatus;
|
||||
_CountingStockInRepo() : super(ApiClient(token: 't'));
|
||||
|
||||
@override
|
||||
Future<PageResult<StockInOrder>> list({
|
||||
String? status,
|
||||
String? startDate,
|
||||
String? endDate,
|
||||
String? keyword,
|
||||
Map<String, String>? detail,
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
}) async {
|
||||
lastStatus = status;
|
||||
return const PageResult<StockInOrder>(
|
||||
data: [], total: 0, page: 1, pageSize: 20);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('1. StockInListNotifier 默认状态', () {
|
||||
test('未做任何设置时 currentStatus == draft,pending,且首次拉取即带上该值', () async {
|
||||
final repo = _CountingStockInRepo();
|
||||
final container = ProviderContainer(
|
||||
overrides: [stockInRepositoryProvider.overrideWithValue(repo)],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(stockInListProvider.future);
|
||||
expect(
|
||||
container.read(stockInListProvider.notifier).currentStatus,
|
||||
'draft,pending');
|
||||
expect(repo.lastStatus, 'draft,pending');
|
||||
});
|
||||
});
|
||||
|
||||
group('2. initState 归一逻辑', () {
|
||||
testWidgets('合法多值 pending,draft(顺序不同)原样保留为 2 项,不回写 provider',
|
||||
(tester) async {
|
||||
_bigView(tester);
|
||||
final notifier =
|
||||
_RecordingStockInNotifier(initialStatus: 'pending,draft');
|
||||
await tester.pumpWidget(_appWith(notifier));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
expect(_statusChip(tester).value, '2 项');
|
||||
expect(notifier.lastSetStatus, isNull,
|
||||
reason: '合法值不应触发归一回写');
|
||||
});
|
||||
|
||||
testWidgets("'' 保留为「全部」(不视为非法、不被重置为默认)", (tester) async {
|
||||
_bigView(tester);
|
||||
final notifier = _RecordingStockInNotifier(initialStatus: '');
|
||||
await tester.pumpWidget(_appWith(notifier));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
expect(_statusChip(tester).value, isNull, reason: '空值=全部,chip 不应显示选中值');
|
||||
expect(notifier.lastSetStatus, isNull);
|
||||
});
|
||||
|
||||
testWidgets('全部非法(如 foo)→ 归默认 draft,pending 并回写 provider', (tester) async {
|
||||
_bigView(tester);
|
||||
final notifier = _RecordingStockInNotifier(initialStatus: 'foo');
|
||||
await tester.pumpWidget(_appWith(notifier));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
expect(_statusChip(tester).value, '2 项');
|
||||
expect(notifier.lastSetStatus, 'draft,pending');
|
||||
});
|
||||
|
||||
testWidgets('部分非法(如 foo,draft)→ 按实现语义只保留合法子集,不回写 provider',
|
||||
(tester) async {
|
||||
_bigView(tester);
|
||||
final notifier = _RecordingStockInNotifier(initialStatus: 'foo,draft');
|
||||
await tester.pumpWidget(_appWith(notifier));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
// 实现里 valid 非空即直接采用该合法子集,不触发 setStatus 纠正——
|
||||
// 这里按真实代码语义断言,避免测试臆造「归默认」预期。
|
||||
expect(_statusChip(tester).value, '草稿');
|
||||
expect(notifier.lastSetStatus, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('3. 多选面板全不选', () {
|
||||
testWidgets('依次取消草稿+待审核 → provider 收到空串(=全部)', (tester) async {
|
||||
_bigView(tester);
|
||||
final notifier = _RecordingStockInNotifier(); // 默认 draft,pending
|
||||
await tester.pumpWidget(_appWith(notifier));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
expect(_statusChip(tester).value, '2 项');
|
||||
|
||||
// 打开状态多选菜单。
|
||||
await tester.tap(find.byWidgetPredicate(
|
||||
(w) => w is DsChip && w.label == '状态'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
// .last:菜单项与 chip 摘要值可能同文案(如切到单值后 chip 显示该状态名),
|
||||
// 菜单项在覆盖层里后挂载,取 .last 精确定位菜单项而非 chip 本身。
|
||||
await tester.tap(find.text('草稿').last);
|
||||
await tester.pump();
|
||||
expect(notifier.lastSetStatus, 'pending');
|
||||
|
||||
await tester.tap(find.text('待审核').last);
|
||||
await tester.pump();
|
||||
expect(notifier.lastSetStatus, '');
|
||||
});
|
||||
});
|
||||
|
||||
group('4. KPI 点击覆盖为单值', () {
|
||||
testWidgets('先多选(默认草稿+待审核)再点「待审核」KPI → 变单值 pending',
|
||||
(tester) async {
|
||||
_bigView(tester);
|
||||
final notifier = _RecordingStockInNotifier(); // 默认多选 draft,pending
|
||||
await tester.pumpWidget(_appWith(notifier));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
expect(_statusChip(tester).value, '2 项', reason: '起点须为多选态');
|
||||
|
||||
await tester.tap(find.text('待审核单数 · 点击筛选'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
expect(notifier.lastSetStatus, 'pending');
|
||||
expect(_statusChip(tester).value, '待审核');
|
||||
});
|
||||
});
|
||||
|
||||
group('5. 退单互斥(双向)', () {
|
||||
testWidgets('选 ret:partial 清主状态集;再选主状态清 ret:*', (tester) async {
|
||||
_bigView(tester);
|
||||
final notifier = _RecordingStockInNotifier(); // 默认多选 draft,pending
|
||||
await tester.pumpWidget(_appWith(notifier));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
await tester.tap(find.byWidgetPredicate(
|
||||
(w) => w is DsChip && w.label == '状态'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
// 5a:选退单态 → 清空主状态集。
|
||||
await tester.tap(find.text('部分退单').last);
|
||||
await tester.pump();
|
||||
expect(notifier.lastSetStatus, '',
|
||||
reason: '选中退单伪状态后主状态应清空(互斥)');
|
||||
expect(notifier.lastSetDetail, {'return_state': 'partial'});
|
||||
|
||||
// 5b:再选主状态 → 清掉 return_state。
|
||||
await tester.tap(find.text('草稿').last);
|
||||
await tester.pump();
|
||||
expect(notifier.lastSetStatus, 'draft');
|
||||
expect(notifier.lastSetDetail, isNot(contains('return_state')));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:jiu_client/core/api/api_client.dart';
|
||||
import 'package:jiu_client/core/models/page_result.dart';
|
||||
import 'package:jiu_client/core/theme/themes.dart';
|
||||
import 'package:jiu_client/models/stock_out.dart';
|
||||
import 'package:jiu_client/providers/license_provider.dart';
|
||||
import 'package:jiu_client/providers/stock_out_provider.dart';
|
||||
import 'package:jiu_client/repositories/stock_out_repository.dart';
|
||||
import 'package:jiu_client/screens/stock_out/stock_out_list_screen.dart';
|
||||
import 'package:jiu_client/widgets/ds/ds_atoms.dart';
|
||||
|
||||
/// 出库屏状态多选语义冒烟测试(T4)。深测在入库屏
|
||||
/// stock_in_status_filter_test.dart(两屏 initState 归一逻辑同构,
|
||||
/// 出库屏只做最小抽样:默认值 + 归一分支 + 一次多选/KPI/退单互斥的组合冒烟)。
|
||||
class _FakeLicenseNotifier extends LicenseNotifier {
|
||||
@override
|
||||
Future<LicenseInfo?> build() async => const LicenseInfo(
|
||||
id: 1,
|
||||
type: 'annual',
|
||||
isActive: true,
|
||||
maxDevices: 3,
|
||||
phase: 'normal',
|
||||
);
|
||||
}
|
||||
|
||||
class _RecordingStockOutNotifier extends StockOutListNotifier {
|
||||
final String initialStatus;
|
||||
|
||||
String? lastSetStatus;
|
||||
Map<String, String>? lastSetDetail;
|
||||
|
||||
_RecordingStockOutNotifier({this.initialStatus = 'draft,pending'});
|
||||
|
||||
@override
|
||||
Future<PageResult<StockOutOrder>> build() async {
|
||||
state = const AsyncValue.data(
|
||||
PageResult<StockOutOrder>(data: [], total: 0, page: 1, pageSize: 20));
|
||||
return state.value!;
|
||||
}
|
||||
|
||||
@override
|
||||
String get currentStatus => initialStatus;
|
||||
@override
|
||||
String get currentKeyword => '';
|
||||
@override
|
||||
Map<String, String> get currentDetail => const {};
|
||||
|
||||
@override
|
||||
void reload() {}
|
||||
@override
|
||||
void setPage(int page) {}
|
||||
@override
|
||||
void setPageSize(int pageSize) {}
|
||||
@override
|
||||
void setKeyword(String keyword) {}
|
||||
@override
|
||||
void setProductCode(String code) {}
|
||||
@override
|
||||
void setDateRange(String? startDate, String? endDate) {}
|
||||
|
||||
@override
|
||||
void setStatus(String status) {
|
||||
lastSetStatus = status;
|
||||
}
|
||||
|
||||
@override
|
||||
void setDetail(Map<String, String> detail) {
|
||||
lastSetDetail = detail;
|
||||
}
|
||||
}
|
||||
|
||||
class _CountingStockOutRepo extends StockOutRepository {
|
||||
String? lastStatus;
|
||||
_CountingStockOutRepo() : super(ApiClient(token: 't'));
|
||||
|
||||
@override
|
||||
Future<PageResult<StockOutOrder>> list({
|
||||
String? status,
|
||||
String? startDate,
|
||||
String? endDate,
|
||||
String? keyword,
|
||||
String? productCode,
|
||||
Map<String, String>? detail,
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
}) async {
|
||||
lastStatus = status;
|
||||
return const PageResult<StockOutOrder>(
|
||||
data: [], total: 0, page: 1, pageSize: 20);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _appWith(_RecordingStockOutNotifier notifier) {
|
||||
final router = GoRouter(
|
||||
initialLocation: '/stock-out',
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/stock-out',
|
||||
builder: (_, __) => const Scaffold(body: StockOutListScreen()),
|
||||
),
|
||||
],
|
||||
);
|
||||
return ProviderScope(
|
||||
overrides: [
|
||||
licenseProvider.overrideWith(() => _FakeLicenseNotifier()),
|
||||
stockOutListProvider.overrideWith(() => notifier),
|
||||
],
|
||||
child: MaterialApp.router(theme: buildTheme('a'), routerConfig: router),
|
||||
);
|
||||
}
|
||||
|
||||
void _bigView(WidgetTester tester) {
|
||||
tester.view.physicalSize = const Size(1400, 1000);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
}
|
||||
|
||||
DsChip _statusChip(WidgetTester tester) => tester
|
||||
.widgetList<DsChip>(find.byType(DsChip))
|
||||
.firstWhere((c) => c.label == '状态');
|
||||
|
||||
void main() {
|
||||
test('1. StockOutListNotifier 默认状态 == draft,pending', () async {
|
||||
final repo = _CountingStockOutRepo();
|
||||
final container = ProviderContainer(
|
||||
overrides: [stockOutRepositoryProvider.overrideWithValue(repo)],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(stockOutListProvider.future);
|
||||
expect(container.read(stockOutListProvider.notifier).currentStatus,
|
||||
'draft,pending');
|
||||
expect(repo.lastStatus, 'draft,pending');
|
||||
});
|
||||
|
||||
testWidgets('2. initState 归一:全部非法 → 归默认并回写;合法多值原样保留',
|
||||
(tester) async {
|
||||
_bigView(tester);
|
||||
final invalid = _RecordingStockOutNotifier(initialStatus: 'foo');
|
||||
await tester.pumpWidget(_appWith(invalid));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
expect(_statusChip(tester).value, '2 项');
|
||||
expect(invalid.lastSetStatus, 'draft,pending');
|
||||
});
|
||||
|
||||
testWidgets("2b. initState 归一:'' 保留为全部", (tester) async {
|
||||
_bigView(tester);
|
||||
final all = _RecordingStockOutNotifier(initialStatus: '');
|
||||
await tester.pumpWidget(_appWith(all));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
expect(_statusChip(tester).value, isNull);
|
||||
expect(all.lastSetStatus, isNull);
|
||||
});
|
||||
|
||||
testWidgets('3+4+5 组合冒烟:多选全清→KPI 覆盖单值→退单互斥双向', (tester) async {
|
||||
_bigView(tester);
|
||||
final notifier = _RecordingStockOutNotifier(); // 默认 draft,pending
|
||||
await tester.pumpWidget(_appWith(notifier));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
expect(_statusChip(tester).value, '2 项');
|
||||
|
||||
// 3:多选面板全不选 → provider 收到空串。
|
||||
await tester.tap(find.byWidgetPredicate(
|
||||
(w) => w is DsChip && w.label == '状态'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.tap(find.text('草稿').last);
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('待审核').last);
|
||||
await tester.pump();
|
||||
expect(notifier.lastSetStatus, '');
|
||||
// 关闭菜单,回到干净状态。
|
||||
await tester.tapAt(const Offset(10, 10));
|
||||
await tester.pump();
|
||||
|
||||
// 4:KPI 点击覆盖为单值。
|
||||
await tester.tap(find.text('待审核单数 · 点击筛选'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
expect(notifier.lastSetStatus, 'pending');
|
||||
expect(_statusChip(tester).value, '待审核');
|
||||
|
||||
// 5:退单互斥双向。
|
||||
await tester.tap(find.byWidgetPredicate(
|
||||
(w) => w is DsChip && w.label == '状态'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.tap(find.text('部分退单').last);
|
||||
await tester.pump();
|
||||
expect(notifier.lastSetStatus, '', reason: '选中退单态应清空主状态(互斥)');
|
||||
expect(notifier.lastSetDetail, {'return_state': 'partial'});
|
||||
|
||||
await tester.tap(find.text('草稿').last);
|
||||
await tester.pump();
|
||||
expect(notifier.lastSetStatus, 'draft');
|
||||
expect(notifier.lastSetDetail, isNot(contains('return_state')));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user