// test/finance_summary_search_test.dart — 财务「应收/应付 汇总」往来单位搜索回归。 // // financePartnerRowsProvider(GET /finance/summary)不分页、一次性全量加载, // 搜索口径=显示口径:仅需验证本地 contains 过滤在已加载行上生效——桌面 // _summaryTable 工具头 DsSearchBox / 移动 应收·应付 分段头 MSearchRow 均覆盖: // 1. 输入关键字 → 汇总行按往来单位名过滤,未命中不显示; // 2. 关键字不匹配任何行 → 展示「没有匹配的往来单位」空态; // 3. 清空关键字 → 恢复全量展示。 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/models/page_result.dart'; import 'package:jiu_client/core/theme/themes.dart'; import 'package:jiu_client/models/finance.dart'; import 'package:jiu_client/models/partner.dart'; import 'package:jiu_client/models/stock_summary.dart'; import 'package:jiu_client/providers/finance_provider.dart'; import 'package:jiu_client/providers/partner_provider.dart'; import 'package:jiu_client/providers/stock_in_provider.dart'; import 'package:jiu_client/providers/stock_out_provider.dart'; import 'package:jiu_client/repositories/finance_repository.dart'; import 'package:jiu_client/screens/finance/finance_screen.dart'; import 'package:jiu_client/widgets/ds/ds_atoms.dart' show DsSearchBox; import 'package:jiu_client/widgets/ds/m_search_row.dart' show MSearchRow; /// 不打真实网络:仅用于满足 FinanceRepository 构造签名,方法全部被覆写。 class _DummyApiClient extends ApiClient { _DummyApiClient() : super(token: 'test-token'); } /// 固定 3 条往来单位汇总(2 应收 + 1 应付),流水列表恒空(本测试不关心流水)。 class _FakeFinanceRepository extends FinanceRepository { _FakeFinanceRepository() : super(_DummyApiClient()); @override Future> listRecords({ String? type, String? month, String? startDate, String? endDate, int? partnerId, int page = 1, int pageSize = 50, }) async => const PageResult(data: [], total: 0, page: 1, pageSize: 50); @override Future> partnerSummary() async => const [ PartnerFinanceSummary( partnerId: 1, partnerName: '华东酒类批发', type: 'receivable', recordCount: 2, totalAmount: 128400), PartnerFinanceSummary( partnerId: 2, partnerName: '金樽商贸', type: 'receivable', recordCount: 1, totalAmount: 86200), PartnerFinanceSummary( partnerId: 3, partnerName: '茅台华南经销', type: 'payable', recordCount: 1, totalAmount: 142800), ]; @override Future> trend({int months = 6}) async => const []; } class _FakeAllPartners extends PartnerListNotifier { @override Future> build() async => const PageResult( data: [ Partner(id: 1, name: '华东酒类批发', type: 'customer'), Partner(id: 2, name: '金樽商贸', type: 'customer'), Partner(id: 3, name: '茅台华南经销', type: 'supplier'), ], total: 3, page: 1, pageSize: 1000, ); } List _overrides() => [ financeRepositoryProvider.overrideWith((ref) => _FakeFinanceRepository()), allPartnersProvider.overrideWith(() => _FakeAllPartners()), stockOutSummaryProvider.overrideWith( (ref) async => const StockSummary(monthAmount: 0, lastMonthAmount: 0)), stockInSummaryProvider.overrideWith( (ref) async => const StockSummary(monthAmount: 0, lastMonthAmount: 0)), isReadonlyProvider.overrideWithValue(false), ]; Future _pumpFinance( WidgetTester tester, { required Size size, }) async { tester.view.physicalSize = size; tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget(ProviderScope( overrides: _overrides(), child: MaterialApp( debugShowCheckedModeBanner: false, theme: buildTheme('a'), home: const Scaffold(body: FinanceScreen()), ), )); await tester.pumpAndSettle(); } void main() { TestWidgetsFlutterBinding.ensureInitialized(); SharedPreferences.setMockInitialValues({}); group('桌面汇总卡往来单位搜索', () { testWidgets('输入关键字过滤汇总行,无匹配报空态,清空恢复全量', (tester) async { await _pumpFinance(tester, size: const Size(1280, 900)); // 初始:3 条汇总行全部展示。 expect(find.text('华东酒类批发'), findsOneWidget); expect(find.text('金樽商贸'), findsOneWidget); expect(find.text('茅台华南经销'), findsOneWidget); final searchField = find.descendant( of: find.byType(DsSearchBox), matching: find.byType(TextField), ); expect(searchField, findsOneWidget); // 输入「金樽」:只剩「金樽商贸」。 await tester.enterText(searchField, '金樽'); await tester.pumpAndSettle(); expect(find.text('金樽商贸'), findsOneWidget); expect(find.text('华东酒类批发'), findsNothing); expect(find.text('茅台华南经销'), findsNothing); // 输入不存在的关键字:空态提示。 await tester.enterText(searchField, '不存在的单位名'); await tester.pumpAndSettle(); expect(find.text('没有匹配的往来单位'), findsOneWidget); expect(find.text('金樽商贸'), findsNothing); // 清空:恢复全量 3 行。 await tester.enterText(searchField, ''); await tester.pumpAndSettle(); expect(find.text('华东酒类批发'), findsOneWidget); expect(find.text('金樽商贸'), findsOneWidget); expect(find.text('茅台华南经销'), findsOneWidget); }); }); group('移动应收/应付分段往来单位搜索', () { testWidgets('应收分段输入关键字过滤卡片,清空恢复全量', (tester) async { await _pumpFinance(tester, size: const Size(390, 1600)); // 默认「应收」分段:仅展示 recv>0 的两条(华东/金樽),不含应付的茅台。 expect(find.text('华东酒类批发'), findsOneWidget); expect(find.text('金樽商贸'), findsOneWidget); expect(find.text('茅台华南经销'), findsNothing); final searchField = find.descendant( of: find.byType(MSearchRow), matching: find.byType(TextField), ); expect(searchField, findsOneWidget); await tester.enterText(searchField, '金樽'); await tester.pumpAndSettle(); expect(find.text('金樽商贸'), findsOneWidget); expect(find.text('华东酒类批发'), findsNothing); // 无匹配 → 分段空态。 await tester.enterText(searchField, '不存在的单位名'); await tester.pumpAndSettle(); expect(find.text('没有匹配的往来单位'), findsOneWidget); // 清空恢复。 await tester.enterText(searchField, ''); await tester.pumpAndSettle(); expect(find.text('华东酒类批发'), findsOneWidget); expect(find.text('金樽商贸'), findsOneWidget); }); }); }