// 功能描述:草稿重开恢复、跨账户隔离、提交前持久化与存储失败不发单。 // 版本: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 = >{}; bool fail = false; @override Future?> read(String owner) async => records[owner]; @override Future write(String owner, Map draft) async { if (fail) throw Exception('storage'); records[owner] = draft; } @override Future 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 repairDraftOwner() async => owner; @override Future uploadedTicketPhoto(String uri) async => base64Decode( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aX1cAAAAASUVORK5CYII=', ); @override Future submitRepair({ required String requestNo, required String description, required String faultType, required String addressIdentity, DateTime? appointment, List> 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 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(); }); }