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/models/partner.dart'; import 'package:jiu_client/providers/partner_provider.dart'; import 'package:jiu_client/screens/partners/partners_screen.dart'; // --------------------------------------------------------------------------- // Fake notifier // --------------------------------------------------------------------------- class _FakePartnerNotifier extends PartnerListNotifier { final AsyncValue> _fixed; _FakePartnerNotifier(String type, this._fixed) : super(type: type); @override Future> 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 setPage(int page) {} @override Future createPartner(Map data) async {} @override Future updatePartner(int id, Map data) async {} @override Future deletePartner(int id) async {} } /// A notifier that stays permanently in loading state (build never completes). class _FakeLoadingPartnerNotifier extends PartnerListNotifier { _FakeLoadingPartnerNotifier(String type) : super(type: type); @override Future> build() { // Never completes — keeps the widget in CircularProgressIndicator state. return Completer>().future; } @override void reload() {} @override void setKeyword(String keyword) {} @override void setPage(int page) {} @override Future createPartner(Map data) async {} @override Future updatePartner(int id, Map data) async {} @override Future deletePartner(int id) async {} } Widget _buildApp({ AsyncValue> supplierState = const AsyncValue.data(PageResult(data: [], total: 0, page: 1, pageSize: 20)), AsyncValue> customerState = const AsyncValue.data(PageResult(data: [], total: 0, page: 1, pageSize: 20)), }) { return ProviderScope( overrides: [ supplierListProvider.overrideWith( () => _FakePartnerNotifier('supplier', supplierState), ), customerListProvider.overrideWith( () => _FakePartnerNotifier('customer', customerState), ), ], child: const MaterialApp(home: Scaffold(body: PartnersScreen())), ); } Widget _buildLoadingApp() { return ProviderScope( overrides: [ supplierListProvider.overrideWith( () => _FakeLoadingPartnerNotifier('supplier'), ), customerListProvider.overrideWith( () => _FakeLoadingPartnerNotifier('customer'), ), ], child: const MaterialApp(home: 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); 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); await tester.pumpWidget( _buildApp( supplierState: 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); await tester.pumpWidget(_buildApp()); await tester.pump(); 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', ), ]; await tester.pumpWidget(_buildApp( supplierState: AsyncValue.data( const PageResult(data: partners, total: 1, page: 1, pageSize: 20), ), )); await tester.pump(); expect(find.text('张家酒水供应商'), findsOneWidget); expect(find.text('张三'), findsOneWidget); }); testWidgets('shows 新建 button in toolbar', (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(); 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.pumpAndSettle(); 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'), ]; await tester.pumpWidget(_buildApp( supplierState: AsyncValue.data( const PageResult(data: partners, total: 1, page: 1, pageSize: 20), ), )); await tester.pump(); await tester.tap(find.byKey(const Key('btn_delete_1'))); 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: '张三', ), ]; await tester.pumpWidget(_buildApp( supplierState: AsyncValue.data( const PageResult(data: partners, total: 1, page: 1, pageSize: 20), ), )); 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)); }); }); }