Files
pad_scanner/test/pages/boxing/boxing_api_actions_test.dart
Misaka_Company b9f17d5d49 refactor: extract API actions into BoxingApiActions with error handling
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>
2026-05-21 09:54:16 +08:00

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,
);
}
}