import 'package:sensors_plus/sensors_plus.dart'; import 'package:fake_cloud_firestore/fake_cloud_firestore.dart'; import 'package:firebase_auth_mocks/firebase_auth_mocks.dart'; import 'package:fancake/core/data/backend.dart'; import 'package:fancake/features/planet/planet_repository_firestore.dart'; import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:fancake/core/data/planet_visual_state.dart'; import 'package:fancake/features/planet/hero_participation_sheet.dart'; import 'package:fancake/features/planet/planet_data.dart'; class _Claims extends SamplePlanetRepository { final ids = []; final replies = >[]; @override Future claimHeroDrop(String id) { ids.add(id); final reply = Completer(); replies.add(reply); return reply.future; } } class _HeroBackend extends Backend { _HeroBackend() : super(firestore: FakeFirebaseFirestore(), auth: MockFirebaseAuth(signedIn: true, mockUser: MockUser(uid: 'viewer'))); Map response = {'claimedPop': 5, 'txId': 'receipt'}; Map? payload; @override Future> call(String name, [Map data = const {}]) async { expect(name, 'fnHeroDropClaim'); payload = data; return response; } } void main() { for (final code in ['E-POP-02', 'E-POP-03', 'E-AUTH-03', 'E-PERM-01']) { testWidgets('서버 확정 거절 $code는 성공이나 무한 재시도로 표시하지 않는다', (tester) async { final repo = _Claims(); await tester.pumpWidget(MaterialApp(home: Scaffold(body: HeroParticipationSheet( fandomId: 'target', repository: repo, accelerometer: const Stream.empty(), watch: (_) => Stream.value(PlanetVisualState(isHeroTime: true, heroEventIds: const {'gift'}, heroEndsAt: DateTime.now().add(const Duration(minutes: 1)))), )))); await tester.pumpAndSettle(); await tester.tap(find.text('흔들기 대신 버튼으로 참여')); repo.replies.single.completeError(BackendException(code, 'internal details')); await tester.pumpAndSettle(); expect(find.text('같은 선물 다시 확인'), findsNothing); expect(find.textContaining('POP 수령 확인'), findsNothing); expect(find.text('internal details'), findsNothing); expect(tester.widget(find.byType(FilledButton)).onPressed, isNull); expect(repo.ids, ['gift']); await tester.pumpWidget(const SizedBox()); }); } test('수령 요청에 서버 판정용 팬덤·이벤트만 보내며 잘못된 응답은 성공으로 읽지 않는다', () async { final backend = _HeroBackend(); final repo = FirestorePlanetRepository(backend, fandomId: 'target'); expect((await repo.claimHeroDrop('gift')).amount, 5); expect(backend.payload, {'fandomId': 'target', 'dropId': 'gift'}); backend.response = {'claimedPop': 0, 'txId': 'receipt'}; await expectLater(repo.claimHeroDrop('gift'), throwsStateError); }); testWidgets('지급 전 성공 없음·연타 중복 없음·종료 후에도 같은 선물 재확인', (tester) async { final repo = _Claims(); final sensor = StreamController(); addTearDown(sensor.close); final states = StreamController.broadcast(); addTearDown(states.close); await tester.pumpWidget(MaterialApp(home: Scaffold(body: HeroParticipationSheet( fandomId: 'test-fandom', watch: (_) => states.stream, repository: repo, accelerometer: sensor.stream, )))); states.add(PlanetVisualState(isHeroTime: true, heroEventIds: const {'gift'}, heroEndsAt: DateTime.now().add(const Duration(minutes: 1)))); await tester.pumpAndSettle(); sensor.add(AccelerometerEvent(25, 0, 0, DateTime.now())); await tester.pump(); await tester.pump(); await tester.tap(find.text('지급 확인 중…')); expect(repo.ids, ['gift']); expect(find.textContaining('POP 수령 확인'), findsNothing); repo.replies.first.completeError(StateError('response lost')); await tester.pumpAndSettle(); states.add(const PlanetVisualState(isHeroTime: false)); await tester.pumpAndSettle(); await tester.tap(find.text('같은 선물 다시 확인')); expect(repo.ids, ['gift', 'gift']); repo.replies.last.complete(const HeroDropReceipt(txId: 'confirmed', amount: 5)); await tester.pumpAndSettle(); expect(find.text('5 POP 수령 확인'), findsOneWidget); expect(tester.widget(find.byType(FilledButton)).onPressed, isNull); await tester.pumpWidget(const SizedBox()); }); }