7fb9031f8d
CI / flutter-gates (push) Successful in 1m12s
- pet_models.dart:pets 域 DTO 逐字段对齐冻结契约(宠物/品种/体重/ 疫苗目录/疫苗/健康事件/照护提醒/聚合摘要 + cursor 分页信封), 枚举严格解析,请求体部分更新语义(缺席字段不出现) - pets_repository.dart:PetsRepository 抽象 + ApiPetsRepository, 12 路径 18 操作全覆盖;四个 POST 携带 Idempotency-Key; 复用既有 ApiClient token 拦截与单飞刷新重放 - pet_exceptions.dart:新 8 错误码映射为类型化异常 (40300/40401/40402/40902/40903/40904/42201/42202), 仍可按 ApiBusinessException 基类捕获 - pets_controller.dart:宠物档案状态自 AppState 拆出 (Controller → Repository → API Client 分层,四态就绪供 T2-12) - money.dart:元/分换算工具(DTO 层保持整数分,UI 层 T2-14 使用) - api_client.dart:新增 patbondPetApiBaseUrl(:8083,--dart-define 可覆盖)与 query 参数支持;api_exception.dart 补 pets 域错误码 - 测试 64 → 126:DTO 映射、错误类型化映射、请求线路 (路径/方法/鉴权/分页/幂等键/tz)、控制器状态机、金额换算 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
168 lines
5.5 KiB
Dart
168 lines
5.5 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:patbond_flutter/core/network/api_exception.dart';
|
|
import 'package:patbond_flutter/features/pets/pet_exceptions.dart';
|
|
import 'package:patbond_flutter/features/pets/pet_models.dart';
|
|
import 'package:patbond_flutter/features/pets/pets_controller.dart';
|
|
import 'package:patbond_flutter/features/pets/pets_repository.dart';
|
|
|
|
import '../../helpers/pet_test_helpers.dart';
|
|
|
|
/// 假仓库:各方法可注入行为,未注入的方法抛 UnimplementedError。
|
|
class FakePetsRepository implements PetsRepository {
|
|
Future<List<Pet>> Function()? listPetsHandler;
|
|
Future<Pet> Function(CreatePetRequest)? createPetHandler;
|
|
Future<Pet> Function(String)? getPetHandler;
|
|
Future<Pet> Function(String, UpdatePetRequest)? updatePetHandler;
|
|
|
|
@override
|
|
Future<List<Pet>> listPets() =>
|
|
listPetsHandler?.call() ?? Future.value(const []);
|
|
|
|
@override
|
|
Future<Pet> createPet(CreatePetRequest request) => createPetHandler!(request);
|
|
|
|
@override
|
|
Future<Pet> getPet(String petId) => getPetHandler!(petId);
|
|
|
|
@override
|
|
Future<Pet> updatePet(String petId, UpdatePetRequest request) =>
|
|
updatePetHandler!(petId, request);
|
|
|
|
@override
|
|
dynamic noSuchMethod(Invocation invocation) =>
|
|
throw UnimplementedError('${invocation.memberName}');
|
|
}
|
|
|
|
Pet pet(String id, {String name = '豆豆', int version = 1}) => Pet.fromJson(
|
|
samplePetJson(overrides: {'id': id, 'name': name, 'version': version}),
|
|
);
|
|
|
|
void main() {
|
|
late FakePetsRepository repository;
|
|
late PetsController controller;
|
|
|
|
setUp(() {
|
|
repository = FakePetsRepository();
|
|
controller = PetsController(repository: repository);
|
|
});
|
|
|
|
test('初始态为 initial,不发请求', () {
|
|
expect(controller.phase, PetsLoadPhase.initial);
|
|
expect(controller.pets, isEmpty);
|
|
expect(controller.isEmpty, isFalse);
|
|
});
|
|
|
|
test('refresh 成功:loading → ready,列表就位', () async {
|
|
repository.listPetsHandler = () async => [pet('p-1'), pet('p-2')];
|
|
final phases = <PetsLoadPhase>[];
|
|
controller.addListener(() => phases.add(controller.phase));
|
|
|
|
await controller.refresh();
|
|
|
|
expect(phases, [PetsLoadPhase.loading, PetsLoadPhase.ready]);
|
|
expect(controller.pets.map((p) => p.id), ['p-1', 'p-2']);
|
|
expect(controller.isEmpty, isFalse);
|
|
expect(controller.lastError, isNull);
|
|
});
|
|
|
|
test('refresh 空列表:ready 且 isEmpty(新用户空态)', () async {
|
|
repository.listPetsHandler = () async => [];
|
|
|
|
await controller.refresh();
|
|
|
|
expect(controller.phase, PetsLoadPhase.ready);
|
|
expect(controller.isEmpty, isTrue);
|
|
});
|
|
|
|
test('refresh 失败:error 态保留类型化错误,重试可恢复', () async {
|
|
repository.listPetsHandler = () async =>
|
|
throw const ApiNetworkException('断网');
|
|
|
|
await controller.refresh();
|
|
|
|
expect(controller.phase, PetsLoadPhase.error);
|
|
expect(controller.lastError, isA<ApiNetworkException>());
|
|
|
|
repository.listPetsHandler = () async => [pet('p-1')];
|
|
await controller.refresh();
|
|
|
|
expect(controller.phase, PetsLoadPhase.ready);
|
|
expect(controller.lastError, isNull);
|
|
expect(controller.pets, hasLength(1));
|
|
});
|
|
|
|
test('createPet:成功后插入列表头', () async {
|
|
repository.listPetsHandler = () async => [pet('p-1')];
|
|
await controller.refresh();
|
|
repository.createPetHandler = (request) async => pet('p-2', name: '咪咪');
|
|
|
|
final created = await controller.createPet(
|
|
const CreatePetRequest(
|
|
name: '咪咪',
|
|
species: PetSpecies.cat,
|
|
sex: PetSex.female,
|
|
customBreedName: '狸花',
|
|
),
|
|
);
|
|
|
|
expect(created.id, 'p-2');
|
|
expect(controller.pets.map((p) => p.id), ['p-2', 'p-1']);
|
|
});
|
|
|
|
test('createPet 失败(40903):类型化异常外抛,列表不变', () async {
|
|
repository.listPetsHandler = () async => [pet('p-1')];
|
|
await controller.refresh();
|
|
repository.createPetHandler = (request) async =>
|
|
throw const MicrochipTakenException(message: '芯片号已被登记');
|
|
|
|
await expectLater(
|
|
controller.createPet(
|
|
const CreatePetRequest(
|
|
name: '咪咪',
|
|
species: PetSpecies.cat,
|
|
sex: PetSex.female,
|
|
customBreedName: '狸花',
|
|
),
|
|
),
|
|
throwsA(isA<MicrochipTakenException>()),
|
|
);
|
|
expect(controller.pets, hasLength(1));
|
|
});
|
|
|
|
test('updatePet:成功后同步列表内存副本', () async {
|
|
repository.listPetsHandler = () async => [pet('p-1', version: 1)];
|
|
await controller.refresh();
|
|
repository.updatePetHandler = (petId, request) async =>
|
|
pet('p-1', name: '豆豆二世', version: 2);
|
|
|
|
await controller.updatePet(
|
|
'p-1',
|
|
const UpdatePetRequest(version: 1, name: '豆豆二世'),
|
|
);
|
|
|
|
expect(controller.pets.single.name, '豆豆二世');
|
|
expect(controller.pets.single.version, 2);
|
|
});
|
|
|
|
test('updatePet 版本冲突(40902):异常外抛供页面提示刷新重提', () async {
|
|
repository.updatePetHandler = (petId, request) async =>
|
|
throw const PetVersionConflictException(message: '数据已被修改');
|
|
|
|
await expectLater(
|
|
controller.updatePet('p-1', const UpdatePetRequest(version: 1)),
|
|
throwsA(isA<PetVersionConflictException>()),
|
|
);
|
|
});
|
|
|
|
test('getPet:详情结果回写列表副本', () async {
|
|
repository.listPetsHandler = () async => [pet('p-1', version: 1)];
|
|
await controller.refresh();
|
|
repository.getPetHandler = (petId) async => pet('p-1', version: 5);
|
|
|
|
final detail = await controller.getPet('p-1');
|
|
|
|
expect(detail.version, 5);
|
|
expect(controller.pets.single.version, 5);
|
|
});
|
|
}
|