6238b86dcb
- 登录/注册(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
221 lines
7.6 KiB
Dart
221 lines
7.6 KiB
Dart
import 'dart:async';
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:jiu_client/core/models/page_result.dart';
|
||
import 'package:jiu_client/core/theme/themes.dart';
|
||
import 'package:jiu_client/models/partner.dart';
|
||
import 'package:jiu_client/providers/license_provider.dart';
|
||
import 'package:jiu_client/providers/partner_provider.dart';
|
||
import 'package:jiu_client/screens/partners/partners_screen.dart';
|
||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||
|
||
/// 往来单位屏(Phase2 重建版:单表 + 类型 chips + 应收应付 + 详情抽屉 + frow 弹窗)。
|
||
/// 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 _FakePartnerNotifier extends PartnerListNotifier {
|
||
final AsyncValue<PageResult<Partner>> _fixed;
|
||
|
||
_FakePartnerNotifier(this._fixed);
|
||
|
||
@override
|
||
Future<PageResult<Partner>> build() async {
|
||
state = _fixed;
|
||
return state.value ??
|
||
const PageResult(data: [], total: 0, page: 1, pageSize: 20);
|
||
}
|
||
|
||
@override
|
||
void reload() {}
|
||
@override
|
||
void setKeyword(String keyword) {}
|
||
@override
|
||
void setType(String? t) {}
|
||
@override
|
||
void setPage(int page) {}
|
||
@override
|
||
Future<void> createPartner(Map<String, dynamic> data) async {}
|
||
@override
|
||
Future<void> updatePartner(int id, Map<String, dynamic> data) async {}
|
||
@override
|
||
Future<void> deletePartner(int id) async {}
|
||
}
|
||
|
||
/// 永久 loading(build 永不完成)。
|
||
class _FakeLoadingPartnerNotifier extends PartnerListNotifier {
|
||
@override
|
||
Future<PageResult<Partner>> build() =>
|
||
Completer<PageResult<Partner>>().future;
|
||
|
||
@override
|
||
void reload() {}
|
||
@override
|
||
void setKeyword(String keyword) {}
|
||
@override
|
||
void setType(String? t) {}
|
||
@override
|
||
void setPage(int page) {}
|
||
}
|
||
|
||
Widget _buildApp({
|
||
AsyncValue<PageResult<Partner>> state = const AsyncValue.data(
|
||
PageResult(data: [], total: 0, page: 1, pageSize: 1000)),
|
||
}) {
|
||
return ProviderScope(
|
||
overrides: [
|
||
licenseProvider.overrideWith(() => _FakeLicenseNotifier()),
|
||
partnersScreenProvider.overrideWith(() => _FakePartnerNotifier(state)),
|
||
partnerBalancesProvider.overrideWith((ref) async => {
|
||
1: (recv: 126800.0, pay: 0.0),
|
||
}),
|
||
// 抽屉「近期单据」:测试环境不发真实请求
|
||
partnerRecentDocsProvider.overrideWith((ref, id) async => const []),
|
||
],
|
||
child: MaterialApp(
|
||
theme: buildTheme('a'), home: const Scaffold(body: PartnersScreen())),
|
||
);
|
||
}
|
||
|
||
Widget _buildLoadingApp() {
|
||
return ProviderScope(
|
||
overrides: [
|
||
licenseProvider.overrideWith(() => _FakeLicenseNotifier()),
|
||
partnersScreenProvider.overrideWith(() => _FakeLoadingPartnerNotifier()),
|
||
partnerBalancesProvider.overrideWith((ref) async => {}),
|
||
],
|
||
child: MaterialApp(
|
||
theme: buildTheme('a'), home: const Scaffold(body: PartnersScreen())),
|
||
);
|
||
}
|
||
|
||
const _partners = [
|
||
Partner(
|
||
id: 1,
|
||
code: 'S001',
|
||
name: '张家酒水供应商',
|
||
type: 'supplier',
|
||
contact: '张三',
|
||
phone: '13800138000',
|
||
address: '北京市朝阳区',
|
||
),
|
||
];
|
||
|
||
void main() {
|
||
void desktop(WidgetTester tester) {
|
||
tester.view.physicalSize = const Size(1280, 800);
|
||
tester.view.devicePixelRatio = 1.0;
|
||
addTearDown(tester.view.resetPhysicalSize);
|
||
}
|
||
|
||
group('PartnersScreen', () {
|
||
testWidgets('loading 态显示 CircularProgressIndicator', (tester) async {
|
||
desktop(tester);
|
||
await tester.pumpWidget(_buildLoadingApp());
|
||
await tester.pump();
|
||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('错误态显示提示与重试', (tester) async {
|
||
desktop(tester);
|
||
await tester.pumpWidget(
|
||
_buildApp(state: AsyncValue.error('网络错误', StackTrace.empty)));
|
||
await tester.pump();
|
||
expect(find.text('暂无数据,网络不可用'), findsOneWidget);
|
||
expect(find.text('重试'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('空列表显示原型空态文案', (tester) async {
|
||
desktop(tester);
|
||
await tester.pumpWidget(_buildApp());
|
||
await tester.pump();
|
||
expect(find.text('没有匹配的往来单位 · 试试调整筛选或搜索'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('列表显示 名称/联系人/电话/类型徽章/应收应付', (tester) async {
|
||
desktop(tester);
|
||
await tester.pumpWidget(_buildApp(
|
||
state: const AsyncValue.data(
|
||
PageResult(data: _partners, total: 1, page: 1, pageSize: 1000)),
|
||
));
|
||
await tester.pumpAndSettle();
|
||
expect(find.text('张家酒水供应商'), findsOneWidget);
|
||
expect(find.text('张三'), findsOneWidget);
|
||
expect(find.text('13800138000'), findsOneWidget);
|
||
expect(find.text('供应商'), findsWidgets); // chips + 徽章
|
||
expect(find.text('¥126,800'), findsOneWidget); // 应收(balances 假数据)
|
||
});
|
||
|
||
testWidgets('类型 chips 与重置在 toolbar 中', (tester) async {
|
||
desktop(tester);
|
||
await tester.pumpWidget(_buildApp());
|
||
await tester.pump();
|
||
expect(find.text('全部'), findsOneWidget);
|
||
expect(find.text('供应商'), findsOneWidget);
|
||
expect(find.text('客户'), findsOneWidget);
|
||
expect(find.text('重置'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('点「新增往来单位」打开 frow 弹窗,空名保存校验', (tester) async {
|
||
desktop(tester);
|
||
await tester.pumpWidget(_buildApp());
|
||
await tester.pump();
|
||
|
||
await tester.tap(find.text('新增往来单位').first);
|
||
await tester.pumpAndSettle();
|
||
// DsField:label 在上(「名称」+ 独立红星),非浮动 labelText
|
||
expect(find.text('名称'), findsOneWidget);
|
||
expect(find.text('期初余额'), findsOneWidget);
|
||
|
||
await tester.tap(find.text('保存'));
|
||
await tester.pump();
|
||
expect(find.text('不能为空'), findsAtLeastNWidgets(1));
|
||
});
|
||
|
||
testWidgets('铅笔打开预填编辑弹窗,弹窗内可发起删除确认', (tester) async {
|
||
desktop(tester);
|
||
await tester.pumpWidget(_buildApp(
|
||
state: const AsyncValue.data(
|
||
PageResult(data: _partners, total: 1, page: 1, pageSize: 1000)),
|
||
));
|
||
await tester.pumpAndSettle();
|
||
|
||
await tester.tap(find.byIcon(LucideIcons.pencil).first);
|
||
await tester.pumpAndSettle();
|
||
expect(find.text('编辑往来单位'), findsOneWidget);
|
||
expect(find.widgetWithText(TextFormField, '张家酒水供应商'), findsOneWidget);
|
||
|
||
await tester.tap(find.text('删除'));
|
||
await tester.pumpAndSettle();
|
||
expect(find.text('确认删除'), findsOneWidget);
|
||
expect(find.textContaining('张家酒水供应商'), findsAtLeastNWidgets(1));
|
||
});
|
||
|
||
testWidgets('行点击打开详情抽屉(联系信息 + 应收/应付 + 近期单据)', (tester) async {
|
||
desktop(tester);
|
||
await tester.pumpWidget(_buildApp(
|
||
state: const AsyncValue.data(
|
||
PageResult(data: _partners, total: 1, page: 1, pageSize: 1000)),
|
||
));
|
||
await tester.pumpAndSettle();
|
||
|
||
await tester.tap(find.text('张家酒水供应商'));
|
||
await tester.pump(const Duration(milliseconds: 300));
|
||
expect(find.text('联系信息'), findsOneWidget);
|
||
expect(find.text('应收账款'), findsOneWidget);
|
||
expect(find.text('应付账款'), findsOneWidget);
|
||
expect(find.text('近期单据'), findsOneWidget);
|
||
});
|
||
});
|
||
}
|