import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; import 'package:jiu_client/core/models/page_result.dart'; import 'package:jiu_client/models/stock_in.dart'; import 'package:jiu_client/providers/stock_in_provider.dart'; import 'package:jiu_client/screens/stock_in/stock_in_list_screen.dart'; // --------------------------------------------------------------------------- // Fake notifier // --------------------------------------------------------------------------- class _FakeStockInNotifier extends StockInListNotifier { final AsyncValue> _fixed; _FakeStockInNotifier(this._fixed); @override Future> build() async { state = _fixed; return state.value ?? const PageResult(data: [], total: 0, page: 1, pageSize: 20); } @override void reload() {} @override void setPage(int page) {} @override void setStatus(String status) {} @override void setDateRange(String? startDate, String? endDate) {} @override Future createOrder(Map data) async {} @override Future submitOrder(int id) async {} @override Future approveOrder(int id) async {} @override Future rejectOrder(int id) async {} } /// A notifier that stays permanently in loading state (build never completes). class _FakeLoadingStockInNotifier extends StockInListNotifier { @override Future> build() { // Never completes — keeps the widget in CircularProgressIndicator state. return Completer>().future; } @override void reload() {} @override void setPage(int page) {} @override void setStatus(String status) {} @override void setDateRange(String? startDate, String? endDate) {} @override Future createOrder(Map data) async {} @override Future submitOrder(int id) async {} @override Future approveOrder(int id) async {} @override Future rejectOrder(int id) async {} } /// Build a testable app. GoRouter is needed because the screen uses /// `context.go('/stock-in/new')`. Widget _buildApp(AsyncValue> state) { final router = GoRouter( initialLocation: '/stock-in', routes: [ GoRoute( path: '/stock-in', builder: (_, __) => const Scaffold(body: StockInListScreen()), ), GoRoute( path: '/stock-in/new', builder: (_, __) => const Scaffold(body: Text('new order form')), ), ], ); return ProviderScope( overrides: [ stockInListProvider.overrideWith(() => _FakeStockInNotifier(state)), ], child: MaterialApp.router(routerConfig: router), ); } Widget _buildLoadingApp() { final router = GoRouter( initialLocation: '/stock-in', routes: [ GoRoute( path: '/stock-in', builder: (_, __) => const Scaffold(body: StockInListScreen()), ), GoRoute( path: '/stock-in/new', builder: (_, __) => const Scaffold(body: Text('new order form')), ), ], ); return ProviderScope( overrides: [ stockInListProvider.overrideWith(() => _FakeLoadingStockInNotifier()), ], child: MaterialApp.router(routerConfig: router), ); } StockInOrder _makeOrder({ int id = 1, String orderNo = 'SI2024010001', String status = 'draft', String? partnerName = '张家酒水', String? warehouseName = '主仓库', }) => StockInOrder( id: id, orderNo: orderNo, warehouseId: 1, warehouseName: warehouseName, partnerName: partnerName, status: status, totalAmount: 1000.0, orderDate: '2024-01-10', ); // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- void main() { group('StockInListScreen', () { 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 and retry button on failure', (tester) async { tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); await tester.pumpWidget( _buildApp(AsyncValue.error('连接超时', StackTrace.empty)), ); await tester.pump(); expect(find.text('暂无数据,网络不可用'), findsOneWidget); expect(find.text('重试'), findsOneWidget); }); testWidgets('shows 暂无入库单 when list is empty', (tester) async { tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); const empty = PageResult(data: [], total: 0, page: 1, pageSize: 20); await tester.pumpWidget(_buildApp(const AsyncValue.data(empty))); await tester.pump(); expect(find.text('暂无入库单'), findsOneWidget); }); testWidgets('shows order list with order number and supplier', (tester) async { tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); final orders = [ _makeOrder(id: 1, orderNo: 'SI2024010001', partnerName: '张家酒水'), _makeOrder(id: 2, orderNo: 'SI2024010002', partnerName: '李记酒行'), ]; final result = PageResult(data: orders, total: 2, page: 1, pageSize: 20); await tester.pumpWidget(_buildApp(AsyncValue.data(result))); await tester.pump(); // draft orders only appear in the "入库审核" tab (index 1) await tester.tap(find.text('入库审核')); await tester.pumpAndSettle(); expect(find.text('SI2024010001'), findsOneWidget); expect(find.text('SI2024010002'), findsOneWidget); 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); const empty = PageResult(data: [], total: 0, page: 1, pageSize: 20); await tester.pumpWidget(_buildApp(const AsyncValue.data(empty))); await tester.pump(); // "新建入库审核单" button only appears in the "入库审核" tab (index 1) await tester.tap(find.text('入库审核')); await tester.pumpAndSettle(); expect(find.text('新建入库审核单'), findsOneWidget); }); testWidgets('tapping 新建入库单 navigates to new order route', (tester) async { tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); const empty = PageResult(data: [], total: 0, page: 1, pageSize: 20); await tester.pumpWidget(_buildApp(const AsyncValue.data(empty))); await tester.pump(); // button only exists in the "入库审核" tab (index 1) await tester.tap(find.text('入库审核')); await tester.pumpAndSettle(); await tester.tap(find.text('新建入库审核单')); await tester.pumpAndSettle(); expect(find.text('new order form'), findsOneWidget); }); testWidgets('draft order shows 提交 button', (tester) async { tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); final orders = [_makeOrder(id: 1, status: 'draft')]; final result = PageResult(data: orders, total: 1, page: 1, pageSize: 20); await tester.pumpWidget(_buildApp(AsyncValue.data(result))); await tester.pump(); // draft orders only appear in the "入库审核" tab (index 1) await tester.tap(find.text('入库审核')); await tester.pumpAndSettle(); expect(find.text('提交'), findsOneWidget); }); testWidgets('pending order shows 通过 and 拒绝 buttons', (tester) async { tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); final orders = [_makeOrder(id: 1, status: 'pending')]; final result = PageResult(data: orders, total: 1, page: 1, pageSize: 20); await tester.pumpWidget(_buildApp(AsyncValue.data(result))); await tester.pump(); // pending orders only appear in the "入库审核" tab (index 1) await tester.tap(find.text('入库审核')); await tester.pumpAndSettle(); expect(find.byKey(const Key('btn_approve_1')), findsOneWidget); expect(find.byKey(const Key('btn_reject_1')), findsOneWidget); }); testWidgets('approved order shows no action buttons', (tester) async { tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); final orders = [_makeOrder(id: 1, status: 'approved')]; final result = PageResult(data: orders, total: 1, page: 1, pageSize: 20); await tester.pumpWidget(_buildApp(AsyncValue.data(result))); await tester.pump(); expect(find.byKey(const Key('btn_approve_1')), findsNothing); expect(find.byKey(const Key('btn_reject_1')), findsNothing); expect(find.text('提交'), findsNothing); }); testWidgets('tapping approve shows confirmation dialog', (tester) async { tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); final orders = [ _makeOrder(id: 1, status: 'pending', orderNo: 'SI2024010001') ]; final result = PageResult(data: orders, total: 1, page: 1, pageSize: 20); await tester.pumpWidget(_buildApp(AsyncValue.data(result))); await tester.pump(); // pending orders only appear in the "入库审核" tab (index 1) await tester.tap(find.text('入库审核')); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('btn_approve_1'))); await tester.pumpAndSettle(); expect(find.text('审核确认'), findsOneWidget); expect(find.textContaining('SI2024010001'), findsAtLeastNWidgets(1)); expect(find.text('通过'), findsAtLeastNWidgets(1)); }); testWidgets('tapping reject shows confirmation dialog', (tester) async { tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); final orders = [ _makeOrder(id: 1, status: 'pending', orderNo: 'SI2024010001') ]; final result = PageResult(data: orders, total: 1, page: 1, pageSize: 20); await tester.pumpWidget(_buildApp(AsyncValue.data(result))); await tester.pump(); // pending orders only appear in the "入库审核" tab (index 1) await tester.tap(find.text('入库审核')); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('btn_reject_1'))); await tester.pumpAndSettle(); expect(find.text('拒绝确认'), findsOneWidget); expect(find.textContaining('SI2024010001'), findsAtLeastNWidgets(1)); expect(find.text('拒绝'), findsAtLeastNWidgets(1)); }); testWidgets('shows status filter dropdown in first tab toolbar', (tester) async { tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); const empty = PageResult(data: [], total: 0, page: 1, pageSize: 20); await tester.pumpWidget(_buildApp(const AsyncValue.data(empty))); await tester.pump(); await tester.tap(find.text('入库单')); await tester.pumpAndSettle(); expect(find.text('全部状态'), findsOneWidget); }); testWidgets('shows total record count in pagination bar', (tester) async { tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); final orders = [_makeOrder(id: 1)]; final result = PageResult(data: orders, total: 35, page: 1, pageSize: 20); await tester.pumpWidget(_buildApp(AsyncValue.data(result))); await tester.pump(); expect(find.textContaining('共 35 条记录'), findsOneWidget); }); }); }