// test/finance_summary_search_test.dart — 财务「应收/应付 汇总」往来单位筛选回归。 // // financePartnerRowsProvider(GET /finance/summary)不分页、一次性全量加载, // 搜索口径=显示口径: // 桌面(验收反馈改版):表头「往来单位」内嵌 ComboSearchField(与 _flowTable // 往来单位筛选同款),单选精确匹配 → 汇总只显示该行;× 清除 → 恢复全量。 // 移动 应收·应付 分段头仍是 MSearchRow 本地 contains 过滤,未改动: // 1. 输入关键字 → 汇总卡片按往来单位名过滤,未命中不显示; // 2. 关键字不匹配任何行 → 展示「没有匹配的往来单位」空态; // 3. 清空关键字 → 恢复全量展示。 import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lucide_icons_flutter/lucide_icons.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/combo_search_field.dart'; 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); // 定位表头内嵌的 ComboSearchField(按构造参数 hint 匹配,避免与 // _flowTable toolbar 的往来单位 ComboSearchField(hint:'往来单位') // 混淆;选中后触发器文案会变成往来单位名,故不能用显示文字定位)。 final combo = find.byWidgetPredicate( (w) => w is ComboSearchField && w.hint == '搜索往来单位', ); expect(combo, findsOneWidget); await tester.ensureVisible(combo); await tester.pumpAndSettle(); // 打开弹层,选中「金樽商贸」。 await tester.tap(combo); await tester.pumpAndSettle(); await tester.tap(find.widgetWithText(InkWell, '金樽商贸')); await tester.pumpAndSettle(); // 只剩「金樽商贸」一行。 expect(find.text('华东酒类批发'), findsNothing); expect(find.text('茅台华南经销'), findsNothing); expect(find.text('金樽商贸'), findsWidgets); // 点触发器上的 × 清除选择 → 恢复全量 3 行。 final clearIcon = find.descendant( of: combo, matching: find.byIcon(LucideIcons.x), ); expect(clearIcon, findsOneWidget); await tester.tap(clearIcon); 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); }); }); }