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
This commit is contained in:
@@ -5,14 +5,14 @@ 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/license.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';
|
||||
|
||||
/// WriteGuard 内层会 watch licenseProvider;测试里覆写成同步返回 normal 授权,
|
||||
/// 避免触发真实网络请求(否则留下 pending timer 导致测试失败)。
|
||||
/// 往来单位屏(Phase2 重建版:单表 + 类型 chips + 应收应付 + 详情抽屉 + frow 弹窗)。
|
||||
/// WriteGuard 内层会 watch licenseProvider;覆写成同步返回 normal 授权,避免真实网络请求。
|
||||
class _FakeLicenseNotifier extends LicenseNotifier {
|
||||
@override
|
||||
Future<LicenseInfo?> build() async => const LicenseInfo(
|
||||
@@ -24,14 +24,10 @@ class _FakeLicenseNotifier extends LicenseNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fake notifier
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class _FakePartnerNotifier extends PartnerListNotifier {
|
||||
final AsyncValue<PageResult<Partner>> _fixed;
|
||||
|
||||
_FakePartnerNotifier(String type, this._fixed) : super(type: type);
|
||||
_FakePartnerNotifier(this._fixed);
|
||||
|
||||
@override
|
||||
Future<PageResult<Partner>> build() async {
|
||||
@@ -42,69 +38,52 @@ class _FakePartnerNotifier extends PartnerListNotifier {
|
||||
|
||||
@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 {}
|
||||
}
|
||||
|
||||
/// A notifier that stays permanently in loading state (build never completes).
|
||||
/// 永久 loading(build 永不完成)。
|
||||
class _FakeLoadingPartnerNotifier extends PartnerListNotifier {
|
||||
_FakeLoadingPartnerNotifier(String type) : super(type: type);
|
||||
|
||||
@override
|
||||
Future<PageResult<Partner>> build() {
|
||||
// Never completes — keeps the widget in CircularProgressIndicator state.
|
||||
return Completer<PageResult<Partner>>().future;
|
||||
}
|
||||
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) {}
|
||||
|
||||
@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 {}
|
||||
}
|
||||
|
||||
Widget _buildApp({
|
||||
AsyncValue<PageResult<Partner>> supplierState =
|
||||
const AsyncValue.data(PageResult(data: [], total: 0, page: 1, pageSize: 20)),
|
||||
AsyncValue<PageResult<Partner>> customerState =
|
||||
const AsyncValue.data(PageResult(data: [], total: 0, page: 1, pageSize: 20)),
|
||||
AsyncValue<PageResult<Partner>> state = const AsyncValue.data(
|
||||
PageResult(data: [], total: 0, page: 1, pageSize: 1000)),
|
||||
}) {
|
||||
return ProviderScope(
|
||||
overrides: [
|
||||
licenseProvider.overrideWith(() => _FakeLicenseNotifier()),
|
||||
supplierListProvider.overrideWith(
|
||||
() => _FakePartnerNotifier('supplier', supplierState),
|
||||
),
|
||||
customerListProvider.overrideWith(
|
||||
() => _FakePartnerNotifier('customer', customerState),
|
||||
),
|
||||
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())),
|
||||
child: MaterialApp(
|
||||
theme: buildTheme('a'), home: const Scaffold(body: PartnersScreen())),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -112,242 +91,130 @@ Widget _buildLoadingApp() {
|
||||
return ProviderScope(
|
||||
overrides: [
|
||||
licenseProvider.overrideWith(() => _FakeLicenseNotifier()),
|
||||
supplierListProvider.overrideWith(
|
||||
() => _FakeLoadingPartnerNotifier('supplier'),
|
||||
),
|
||||
customerListProvider.overrideWith(
|
||||
() => _FakeLoadingPartnerNotifier('customer'),
|
||||
),
|
||||
partnersScreenProvider.overrideWith(() => _FakeLoadingPartnerNotifier()),
|
||||
partnerBalancesProvider.overrideWith((ref) async => {}),
|
||||
],
|
||||
child: MaterialApp(theme: buildTheme('a'), home: const Scaffold(body: PartnersScreen())),
|
||||
child: MaterialApp(
|
||||
theme: buildTheme('a'), home: const Scaffold(body: PartnersScreen())),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
void main() {
|
||||
group('PartnersScreen - Supplier tab', () {
|
||||
testWidgets('shows CircularProgressIndicator while loading', (tester) async {
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
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('shows error message on failure', (tester) async {
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
|
||||
testWidgets('错误态显示提示与重试', (tester) async {
|
||||
desktop(tester);
|
||||
await tester.pumpWidget(
|
||||
_buildApp(
|
||||
supplierState: AsyncValue.error('网络错误', StackTrace.empty),
|
||||
),
|
||||
);
|
||||
_buildApp(state: AsyncValue.error('网络错误', StackTrace.empty)));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('暂无数据,网络不可用'), findsOneWidget);
|
||||
expect(find.text('重试'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows 暂无供应商 when supplier list is empty', (tester) async {
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
|
||||
testWidgets('空列表显示原型空态文案', (tester) async {
|
||||
desktop(tester);
|
||||
await tester.pumpWidget(_buildApp());
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('暂无供应商'), findsOneWidget);
|
||||
expect(find.text('没有匹配的往来单位 · 试试调整筛选或搜索'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows supplier list with name and contact', (tester) async {
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
|
||||
const partners = [
|
||||
Partner(
|
||||
id: 1,
|
||||
code: 'S001',
|
||||
name: '张家酒水供应商',
|
||||
type: 'supplier',
|
||||
contact: '张三',
|
||||
phone: '13800138000',
|
||||
),
|
||||
];
|
||||
|
||||
testWidgets('列表显示 名称/联系人/电话/类型徽章/应收应付', (tester) async {
|
||||
desktop(tester);
|
||||
await tester.pumpWidget(_buildApp(
|
||||
supplierState: AsyncValue.data(
|
||||
const PageResult(data: partners, total: 1, page: 1, pageSize: 20),
|
||||
),
|
||||
state: const AsyncValue.data(
|
||||
PageResult(data: _partners, total: 1, page: 1, pageSize: 1000)),
|
||||
));
|
||||
await tester.pump();
|
||||
|
||||
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('shows 新建 button in toolbar', (tester) async {
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
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();
|
||||
|
||||
expect(find.text('新建'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('tapping 新建 opens 新建供应商 dialog', (tester) async {
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
|
||||
await tester.pumpWidget(_buildApp());
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.text('新建'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('新建供应商'), findsOneWidget);
|
||||
expect(find.text('供应商名称'), findsAtLeastNWidgets(1));
|
||||
});
|
||||
|
||||
testWidgets('supplier form shows 不能为空 when saving with empty name',
|
||||
(tester) async {
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
|
||||
await tester.pumpWidget(_buildApp());
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.text('新建'));
|
||||
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('shows delete confirmation when delete button tapped', (tester) async {
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
|
||||
const partners = [
|
||||
Partner(id: 1, name: '张家酒水供应商', type: 'supplier'),
|
||||
];
|
||||
|
||||
testWidgets('铅笔打开预填编辑弹窗,弹窗内可发起删除确认', (tester) async {
|
||||
desktop(tester);
|
||||
await tester.pumpWidget(_buildApp(
|
||||
supplierState: AsyncValue.data(
|
||||
const PageResult(data: partners, total: 1, page: 1, pageSize: 20),
|
||||
),
|
||||
state: const AsyncValue.data(
|
||||
PageResult(data: _partners, total: 1, page: 1, pageSize: 1000)),
|
||||
));
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byKey(const Key('btn_delete_1')));
|
||||
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));
|
||||
expect(find.text('取消'), findsOneWidget);
|
||||
// 删除 appears in both the list row action button and the dialog confirm button.
|
||||
expect(find.text('删除'), findsAtLeastNWidgets(1));
|
||||
});
|
||||
|
||||
testWidgets('tapping edit button opens prefilled edit dialog', (tester) async {
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
|
||||
const partners = [
|
||||
Partner(
|
||||
id: 1,
|
||||
code: 'S001',
|
||||
name: '张家酒水供应商',
|
||||
type: 'supplier',
|
||||
contact: '张三',
|
||||
),
|
||||
];
|
||||
|
||||
testWidgets('行点击打开详情抽屉(联系信息 + 应收/应付 + 近期单据)', (tester) async {
|
||||
desktop(tester);
|
||||
await tester.pumpWidget(_buildApp(
|
||||
supplierState: AsyncValue.data(
|
||||
const PageResult(data: partners, total: 1, page: 1, pageSize: 20),
|
||||
),
|
||||
state: const AsyncValue.data(
|
||||
PageResult(data: _partners, total: 1, page: 1, pageSize: 1000)),
|
||||
));
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byKey(const Key('btn_edit_1')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('编辑供应商'), findsOneWidget);
|
||||
expect(find.widgetWithText(TextFormField, '张家酒水供应商'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('PartnersScreen - Customer tab', () {
|
||||
testWidgets('shows 暂无客户 when customer list is empty', (tester) async {
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
|
||||
await tester.pumpWidget(_buildApp());
|
||||
await tester.pump();
|
||||
|
||||
// Switch to 客户 tab
|
||||
await tester.tap(find.text('客户'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('暂无客户'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows customer names in list', (tester) async {
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
|
||||
const customers = [
|
||||
Partner(id: 10, name: '王记饭店', type: 'customer', phone: '13900139000'),
|
||||
];
|
||||
|
||||
await tester.pumpWidget(_buildApp(
|
||||
customerState: AsyncValue.data(
|
||||
const PageResult(data: customers, total: 1, page: 1, pageSize: 20),
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
|
||||
// Switch to customer tab
|
||||
await tester.tap(find.text('客户'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('王记饭店'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('opens 新建客户 dialog from customer tab', (tester) async {
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
|
||||
await tester.pumpWidget(_buildApp());
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.text('客户'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('新建'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('新建客户'), findsOneWidget);
|
||||
expect(find.text('客户名称'), findsAtLeastNWidgets(1));
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user