已完成用户APP首期功能开发
交付用户端首期页面、配套接口、后台资源及测试文档。用户APP构建、静态分析和三个管理后台构建通过;完整测试仍有2项失败,后端模型注释检查未通过,详见交付记录。
This commit is contained in:
214
apps/user_app/test/ui/repair_draft_test.dart
Normal file
214
apps/user_app/test/ui/repair_draft_test.dart
Normal file
@@ -0,0 +1,214 @@
|
||||
// 功能描述:草稿重开恢复、跨账户隔离、提交前持久化与存储失败不发单。
|
||||
// 版本:1.0.0。
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:user_app/data/repositories/client_repository.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/data/services/repair_draft_store.dart';
|
||||
import 'package:user_app/ui/features/tickets/repair_page.dart';
|
||||
|
||||
class _Drafts implements RepairDraftStore {
|
||||
final records = <String, Map<String, Object?>>{};
|
||||
bool fail = false;
|
||||
@override
|
||||
Future<Map<String, Object?>?> read(String owner) async => records[owner];
|
||||
@override
|
||||
Future<void> write(String owner, Map<String, Object?> draft) async {
|
||||
if (fail) throw Exception('storage');
|
||||
records[owner] = draft;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(String owner) async {
|
||||
records.remove(owner);
|
||||
}
|
||||
}
|
||||
|
||||
class _Repository extends ClientRepository {
|
||||
_Repository(this.store) : super(ApiClient(() => '', baseUrl: 'https://api.example.com'));
|
||||
final _Drafts store;
|
||||
String owner = 'api#alice';
|
||||
int submissions = 0;
|
||||
@override
|
||||
Future<String> repairDraftOwner() async => owner;
|
||||
@override
|
||||
Future<Uint8List?> uploadedTicketPhoto(String uri) async => base64Decode(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aX1cAAAAASUVORK5CYII=',
|
||||
);
|
||||
@override
|
||||
Future<String> submitRepair({
|
||||
required String requestNo,
|
||||
required String description,
|
||||
required String faultType,
|
||||
required String addressIdentity,
|
||||
DateTime? appointment,
|
||||
List<Map<String, Object?>> photos = const [],
|
||||
}) async {
|
||||
expect(requestNo, 'persisted-request');
|
||||
expect(store.records[owner]!['attempted'], true);
|
||||
expect(store.records[owner]!['request_no'], requestNo);
|
||||
expect(photos.length, 1);
|
||||
submissions++;
|
||||
if (submissions == 1) throw const ApiException(500, '结果待确认');
|
||||
return 'ticket-1';
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object?> draft({bool pending = true}) => {
|
||||
'schema': 1,
|
||||
'request_no': 'persisted-request',
|
||||
'description': '已保存故障',
|
||||
'fault_type': 'valve',
|
||||
'step': pending ? 2 : 0,
|
||||
'attempted': pending,
|
||||
'address': pending
|
||||
? {
|
||||
'identity': 'address-1',
|
||||
'address': '草稿地址',
|
||||
'contact_name': '联系人',
|
||||
'contact_phone': '13800000001',
|
||||
}
|
||||
: null,
|
||||
'photos': pending
|
||||
? [
|
||||
{
|
||||
'uri': '/uploads/ticket-photos/owned/photo.png',
|
||||
'source': 'gallery',
|
||||
'added_at': '2026-09-07T01:00:00Z',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
};
|
||||
GoRouter routerFor(ClientRepository repository, RepairDraftStore store) => GoRouter(
|
||||
initialLocation: '/repair',
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/repair',
|
||||
builder: (_, s) => RepairPage(repository: repository, draftStore: store),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/me',
|
||||
builder: (_, s) => const Scaffold(body: Text('已暂存退出')),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/tickets/:identity',
|
||||
builder: (_, s) => const Scaffold(body: Text('已提交')),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/orders',
|
||||
builder: (_, s) => const Scaffold(body: Text('工单列表')),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
void main() {
|
||||
testWidgets('损坏草稿不会自动丢弃,明确清除后才可重新填写', (tester) async {
|
||||
final store = _Drafts()..records['api#alice'] = {'schema': 1};
|
||||
final repository = _Repository(store);
|
||||
final router = routerFor(repository, store);
|
||||
await tester.pumpWidget(MaterialApp.router(routerConfig: router));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('恢复草稿'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(store.records['api#alice'], isNotNull);
|
||||
await tester.tap(find.text('清除无法读取的草稿'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(store.records['api#alice'], isNotNull);
|
||||
await tester.tap(find.text('清除草稿'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(store.records['api#alice'], isNull);
|
||||
expect(find.byType(TextField), findsOneWidget);
|
||||
expect(repository.submissions, 0);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
router.dispose();
|
||||
});
|
||||
testWidgets('结果未知重开后恢复原请求,成功清理草稿', (tester) async {
|
||||
final store = _Drafts()..records['api#alice'] = draft();
|
||||
final repository = _Repository(store);
|
||||
var router = routerFor(repository, store);
|
||||
await tester.pumpWidget(MaterialApp.router(routerConfig: router));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.submissions, 0);
|
||||
await tester.tap(find.text('恢复草稿'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.ensureVisible(find.text('重试提交'));
|
||||
await tester.tap(find.text('重试提交'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.submissions, 1);
|
||||
expect(store.records['api#alice']!['request_no'], 'persisted-request');
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
router.dispose();
|
||||
router = routerFor(repository, store);
|
||||
await tester.pumpWidget(MaterialApp.router(routerConfig: router));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('恢复草稿'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('已保存故障'), findsOneWidget);
|
||||
expect(repository.submissions, 1);
|
||||
await tester.ensureVisible(find.text('重试提交'));
|
||||
await tester.tap(find.text('重试提交'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.submissions, 2);
|
||||
expect(store.records['api#alice'], isNull);
|
||||
expect(find.text('已提交'), findsOneWidget);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
router.dispose();
|
||||
});
|
||||
testWidgets('保存失败不发送工单且保留输入', (tester) async {
|
||||
final store = _Drafts()
|
||||
..records['api#alice'] = draft()
|
||||
..fail = true;
|
||||
final repository = _Repository(store);
|
||||
final router = routerFor(repository, store);
|
||||
await tester.pumpWidget(MaterialApp.router(routerConfig: router));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('恢复草稿'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.ensureVisible(find.text('重试提交'));
|
||||
await tester.tap(find.text('重试提交'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(repository.submissions, 0);
|
||||
expect(find.textContaining('提交前保存失败'), findsOneWidget);
|
||||
expect(find.text('已保存故障'), findsOneWidget);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
router.dispose();
|
||||
});
|
||||
testWidgets('其他账户不会出现已有草稿', (tester) async {
|
||||
final store = _Drafts()..records['api#alice'] = draft();
|
||||
final repository = _Repository(store)..owner = 'api#bob';
|
||||
final router = routerFor(repository, store);
|
||||
await tester.pumpWidget(MaterialApp.router(routerConfig: router));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('发现报修草稿'), findsNothing);
|
||||
expect(find.text('已保存故障'), findsNothing);
|
||||
expect(store.records['api#alice'], isNotNull);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
router.dispose();
|
||||
});
|
||||
testWidgets('暂存退出保存编辑和同一个请求号', (tester) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
final store = _Drafts()..records['api#alice'] = draft(pending: false);
|
||||
final repository = _Repository(store);
|
||||
final router = routerFor(repository, store);
|
||||
await tester.pumpWidget(MaterialApp.router(routerConfig: router));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('恢复草稿'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField), '修改后暂存');
|
||||
await tester.ensureVisible(find.text('暂存并退出'));
|
||||
await tester.tap(find.text('暂存并退出'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('已暂存退出'), findsOneWidget);
|
||||
expect(store.records['api#alice']!['description'], '修改后暂存');
|
||||
expect(store.records['api#alice']!['request_no'], 'persisted-request');
|
||||
expect(repository.submissions, 0);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
router.dispose();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user