Files
jiu/client/test/stock_in_screen_test.dart
T
wangjia 6238b86dcb feat(client): 登录/注册页照原型重建,ds 真相源组件族统一全部屏
- 登录/注册(login.html/register.html 1:1):两栏卡片+品牌渐变面板+主题小衣服
  pill(onSurface 变体)+记住我(记录并预填最近账号)+原型式 toast 校验;
  登录 fidelity 1.4–2.1% 三主题全绿;注册暂不入闸(少 门店编号/兑换券 字段,
  已记 CONTRACT,screens.mjs 留存根)
- ds 原子补齐:DsToast(.toast 单例)/DsCheck(.check/.agree)/DsButton lg 档/
  DsSelect 替换全部旧 DropdownButton/DsField label 在上/DsInput 后缀与密码形态
- 全屏统一:对话框按钮全 DsButton、盒式输入主题钉死(visualDensity.standard、
  h38、InputDecorationTheme 渗漏修复)、图标全 lucide、JetBrains Mono 三端同源、
  BrandMark 真相源 logo、只读模式写操作全量守卫(WriteGuard+DsToast)
- 出入库列表:版式对齐原型(卡片对齐+搜索框居中)、KPI 近30天滚动、结清后
  失效财务应收应付表;商品编辑抽屉介绍库改搜索下拉、图片双击全屏预览
- 删除旧 UI 死代码:DataTableCard/FormDialog/PageScaffold/SearchChip/
  SelectProductDialog/tabStateProvider
- golden/fidelity:stock-in/out 补注册(9%)、login 入册(8%)、goldens 全量重打;
  修复 pubspec flutter_web_plugins 非法声明(CI pub get 阻断)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
2026-07-03 09:58:14 +08:00

238 lines
8.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
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/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/screens/stock_in/stock_in_list_screen.dart';
/// 入库列表屏(照原型重建:单视图无 tab)的行为测试。
/// WriteGuard 内层 watch licenseProvider;覆写成同步 normal 授权,避免真实网络请求。
class _FakeLicenseNotifier extends LicenseNotifier {
@override
Future<LicenseInfo?> build() async => const LicenseInfo(
id: 1,
type: 'annual',
isActive: true,
maxDevices: 3,
phase: 'normal',
);
}
class _FakeStockInNotifier extends StockInListNotifier {
final AsyncValue<PageResult<StockInOrder>> _fixed;
_FakeStockInNotifier(this._fixed);
@override
Future<PageResult<StockInOrder>> build() async {
state = _fixed;
return state.value ??
const PageResult(data: [], total: 0, page: 1, pageSize: 20);
}
// 交互 setter 全部 no-op,避免命中真实 repo。
@override
void reload() {}
@override
void setPage(int page) {}
@override
void setPageSize(int pageSize) {}
@override
void setStatus(String status) {}
@override
void setKeyword(String keyword) {}
@override
void setDateRange(String? startDate, String? endDate) {}
@override
void setDetail(Map<String, String> detail) {}
@override
Future<void> createOrder(Map<String, dynamic> data) async {}
@override
Future<void> submitOrder(int id) async {}
@override
Future<void> approveOrder(int id) async {}
@override
Future<void> rejectOrder(int id) async {}
}
/// 永远停在 loadingbuild 不完成)。
class _FakeLoadingStockInNotifier extends StockInListNotifier {
@override
Future<PageResult<StockInOrder>> build() =>
Completer<PageResult<StockInOrder>>().future;
@override
void reload() {}
}
/// 持久化 provider 里已有残留关键词 'sfs'(模拟用户上次搜索后未清)。
/// build 仍返回数据——用来验证「进页面把残留筛选回填到 UI」而非隐形丢数据。
class _StaleKeywordStockInNotifier extends StockInListNotifier {
@override
Future<PageResult<StockInOrder>> build() async {
state = AsyncValue.data(
PageResult(data: [_makeOrder()], total: 1, page: 1, pageSize: 20));
return state.value!;
}
@override
String get currentKeyword => 'sfs';
@override
void reload() {}
@override
void setStatus(String status) {}
@override
void setKeyword(String keyword) {}
@override
void setDateRange(String? startDate, String? endDate) {}
@override
void setDetail(Map<String, String> detail) {}
}
Widget _appWith(Override overrideNotifier) {
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()),
overrideNotifier,
],
child: MaterialApp.router(theme: buildTheme('a'), routerConfig: router),
);
}
Widget _buildApp(AsyncValue<PageResult<StockInOrder>> state) => _appWith(
stockInListProvider.overrideWith(() => _FakeStockInNotifier(state)));
Widget _buildLoadingApp() => _appWith(
stockInListProvider.overrideWith(() => _FakeLoadingStockInNotifier()));
StockInOrder _makeOrder({
int id = 1,
String orderNo = 'SI2024010001',
String status = 'draft',
String? partnerName = '张家酒水',
String? warehouseName = '主仓库',
}) =>
StockInOrder(
id: id,
orderNo: orderNo,
warehouseId: 1,
warehouseName: warehouseName,
partnerName: partnerName,
status: status,
totalAmount: 1000.0,
orderDate: '2024-01-10',
);
void _bigView(WidgetTester tester) {
tester.view.physicalSize = const Size(1400, 1000);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
}
void main() {
group('StockInListScreen(单视图重建版)', () {
testWidgets('loading 显示进度圈', (tester) async {
_bigView(tester);
await tester.pumpWidget(_buildLoadingApp());
await tester.pump();
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
testWidgets('error 显示「加载失败」+ 重试', (tester) async {
_bigView(tester);
await tester
.pumpWidget(_buildApp(AsyncValue.error('连接超时', StackTrace.empty)));
await tester.pumpAndSettle();
expect(find.textContaining('加载失败'), findsOneWidget);
expect(find.text('重试'), findsOneWidget);
});
testWidgets('空列表显示空态提示', (tester) async {
_bigView(tester);
const empty =
PageResult<StockInOrder>(data: [], total: 0, page: 1, pageSize: 20);
await tester.pumpWidget(_buildApp(const AsyncValue.data(empty)));
await tester.pumpAndSettle();
expect(find.textContaining('没有匹配的入库单'), findsOneWidget);
});
testWidgets('列表直接展示单号与供应商(无 tab', (tester) async {
_bigView(tester);
final orders = [
_makeOrder(id: 1, orderNo: 'SI2024010001', partnerName: '张家酒水'),
_makeOrder(id: 2, orderNo: 'SI2024010002', partnerName: '李记酒行'),
];
await tester.pumpWidget(_buildApp(AsyncValue.data(
PageResult(data: orders, total: 2, page: 1, pageSize: 20))));
await tester.pumpAndSettle();
expect(find.text('SI2024010001'), findsOneWidget);
expect(find.text('李记酒行'), findsOneWidget);
});
testWidgets('工具栏有「新增入库」并可导航', (tester) async {
_bigView(tester);
const empty =
PageResult<StockInOrder>(data: [], total: 0, page: 1, pageSize: 20);
await tester.pumpWidget(_buildApp(const AsyncValue.data(empty)));
await tester.pumpAndSettle();
final newBtn = find.text('新增入库');
expect(newBtn, findsOneWidget);
await tester.tap(newBtn);
await tester.pumpAndSettle();
expect(find.text('new order form'), findsOneWidget);
});
// 操作已改为「行内只放眼睛,审核/退单/打印移入右侧详情抽屉」。
// 抽屉内容与动作布局由 order_detail_drawer 组件保真验证,这里只验行内行为。
testWidgets('各状态行内只显示眼睛(详情入口),无内联审核按钮', (tester) async {
_bigView(tester);
for (final status in ['draft', 'pending', 'approved']) {
final orders = [_makeOrder(id: 1, status: status)];
await tester.pumpWidget(_buildApp(AsyncValue.data(
PageResult(data: orders, total: 1, page: 1, pageSize: 20))));
await tester.pumpAndSettle();
expect(find.byTooltip('详情'), findsOneWidget); // 眼睛图标
expect(find.byKey(const Key('btn_approve_1')), findsNothing);
expect(find.byKey(const Key('btn_reject_1')), findsNothing);
expect(find.text('提交'), findsNothing);
}
});
testWidgets('进页面把 provider 残留关键词回填到搜索框(不隐形过滤)', (tester) async {
_bigView(tester);
await tester.pumpWidget(_appWith(stockInListProvider
.overrideWith(() => _StaleKeywordStockInNotifier())));
await tester.pumpAndSettle();
// 残留 'sfs' 必须显示在搜索框里,用户看得见可清除;
// 修复前入库屏无 initState 回填 → 框空但仍按 'sfs' 过滤 → 列表恒空。
expect(find.text('sfs'), findsOneWidget);
});
testWidgets('分页栏显示总数', (tester) async {
_bigView(tester);
final orders = [_makeOrder(id: 1)];
await tester.pumpWidget(_buildApp(AsyncValue.data(
PageResult(data: orders, total: 35, page: 1, pageSize: 20))));
await tester.pumpAndSettle();
expect(find.textContaining('共 35'), findsOneWidget);
});
});
}