Centralize all API call logic (fetchBoxInfo, saveBoxRecord, updateBoxRecord, deleteBoxRecord) into BoxingApiActions class with unified error categorization (network, duplicate, missingApiUrl). Add helper methods to reduce repetitive error handling in boxing_page. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
160 lines
5.0 KiB
Dart
160 lines
5.0 KiB
Dart
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<http.Response> Function(http.BaseRequest) _handler;
|
|
|
|
_MockClient(this._handler);
|
|
|
|
@override
|
|
Future<http.StreamedResponse> 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,
|
|
);
|
|
}
|
|
}
|