import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:pad_scanner/pages/boxing/boxing_api_actions.dart'; import 'package:pad_scanner/services/api_service.dart'; void main() { group('BoxingApiActions', () { test('returns missingApiUrl when API URL is empty', () async { final actions = BoxingApiActions( apiService: ApiService( client: _MockClient((_) async { fail('ApiService should not be called without a base URL'); }), ), loadApiUrl: () async => '', ); final result = await actions.fetchBoxInfo('26B1'); expect(result.success, isFalse); expect(result.errorKind, BoxingActionErrorKind.missingApiUrl); expect(result.errorMessage, BoxingApiActions.missingApiUrlMessage); }); test('fetchBoxInfo passes through successful data', () async { final actions = BoxingApiActions( apiService: ApiService( client: _MockClient((request) async { expect(request.url.path, '/CargoTrace/box/info'); expect(request.url.queryParameters['zongpai_no'], '26B1'); return http.Response( jsonEncode({ 'zongpai_no': '26B1', 'paichan_no': 'W00009', 'work_order_no': 'WO001', 'quantity': 8, 'current_zongpai_boxes': [ {'box_item_id': 1, 'box_no': 2, 'quantity': 3}, ], 'existing_boxes': [], 'max_box_no': 2, }), 200, headers: {'content-type': 'application/json; charset=utf-8'}, ); }), ), loadApiUrl: () async => 'http://localhost', ); final result = await actions.fetchBoxInfo('26B1'); expect(result.success, isTrue); expect(result.data?.paichanNo, 'W00009'); expect(result.data?.currentZongpaiBoxes.single.boxNo, 2); }); test('saveBoxRecord maps duplicate and preserves box number', () async { final actions = BoxingApiActions( apiService: ApiService( client: _MockClient((_) async { return http.Response( jsonEncode({ 'error_code': 'DUPLICATE_BOX_ITEM', 'message': '重复装箱', 'box_no': 7, }), 409, headers: {'content-type': 'application/json; charset=utf-8'}, ); }), ), loadApiUrl: () async => 'http://localhost', ); final result = await actions.saveBoxRecord( zongpaiNo: '26B1', boxNo: 7, quantity: 2, ); expect(result.success, isFalse); expect(result.errorKind, BoxingActionErrorKind.duplicate); expect(result.boxNo, 7); }); test('updateBoxRecord maps network errors', () async { final actions = BoxingApiActions( apiService: ApiService( client: _MockClient((_) async { throw Exception('Connection refused'); }), ), loadApiUrl: () async => 'http://localhost', ); final result = await actions.updateBoxRecord( boxItemId: 1, boxNo: 2, quantity: 3, ); expect(result.success, isFalse); expect(result.errorKind, BoxingActionErrorKind.network); expect(result.errorMessage, BoxingApiActions.networkErrorMessage); }); test('deleteBoxRecord passes through success and backend errors', () async { var calls = 0; final actions = BoxingApiActions( apiService: ApiService( client: _MockClient((_) async { calls += 1; if (calls == 1) { return http.Response( jsonEncode({'box_item_id': 1, 'deleted': true}), 200, headers: {'content-type': 'application/json; charset=utf-8'}, ); } return http.Response( jsonEncode({'message': '指定装箱明细不存在'}), 404, headers: {'content-type': 'application/json; charset=utf-8'}, ); }), ), loadApiUrl: () async => 'http://localhost', ); final ok = await actions.deleteBoxRecord(boxItemId: 1); final missing = await actions.deleteBoxRecord(boxItemId: 999); expect(ok.success, isTrue); expect(missing.success, isFalse); expect(missing.errorKind, BoxingActionErrorKind.backend); expect(missing.errorMessage, '指定装箱明细不存在'); }); }); } class _MockClient extends http.BaseClient { final Future Function(http.BaseRequest) _handler; _MockClient(this._handler); @override Future send(http.BaseRequest request) async { final response = await _handler(request); return http.StreamedResponse( http.ByteStream.fromBytes(response.bodyBytes), response.statusCode, headers: response.headers, reasonPhrase: response.reasonPhrase, ); } }