feat(client): 财务应收应付汇总增加往来单位搜索(两端,原型同步)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@@ -0,0 +1,194 @@
|
||||
// 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<PageResult<FinanceRecord>> 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<List<PartnerFinanceSummary>> 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<List<TrendPoint>> trend({int months = 6}) async => const [];
|
||||
}
|
||||
|
||||
class _FakeAllPartners extends PartnerListNotifier {
|
||||
@override
|
||||
Future<PageResult<Partner>> 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<Override> _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<void> _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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
Before Width: | Height: | Size: 280 KiB After Width: | Height: | Size: 285 KiB |
|
Before Width: | Height: | Size: 277 KiB After Width: | Height: | Size: 282 KiB |
|
Before Width: | Height: | Size: 281 KiB After Width: | Height: | Size: 286 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 108 KiB |