import 'package:fancake/features/home/home_screen.dart'; import 'package:fancake/features/home/home_data.dart'; import 'package:fancake/features/quest/quest_data.dart'; import 'package:fancake/features/galaxy/widgets/zap_feedback.dart'; import 'dart:async'; import 'package:fancake/core/data/backend.dart'; import 'package:fake_cloud_firestore/fake_cloud_firestore.dart'; import 'package:firebase_auth_mocks/firebase_auth_mocks.dart'; import 'package:fancake/features/galaxy/galaxy_repository_firestore.dart'; import 'package:fancake/core/theme/app_theme.dart'; import 'package:fancake/features/galaxy/galaxy_data.dart'; import 'package:fancake/features/galaxy/galaxy_screen.dart'; import 'package:fancake/features/galaxy/widgets/zap_button.dart'; import 'package:fancake/features/planet/planet_data.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'eclipse_stubs.dart'; class _DelayedGalaxy extends SampleGalaxyRepository { final replies = >[]; final keys = []; final counts = []; final fandoms = []; @override Future zap({ required String fandomId, required int count, required String clientSeq, }) { keys.add(clientSeq); counts.add(count); fandoms.add(fandomId); final reply = Completer(); replies.add(reply); return reply.future; } } Future _show(WidgetTester tester, GalaxyRepository repository) async { await tester.pumpWidget( MaterialApp( theme: buildAppTheme(), home: GalaxyScreen( repository: repository, planetRepository: samplePlanetRepository, eclipse: const NoEclipseRepository(), ), ), ); await tester.pump(); await tester.pump(const Duration(seconds: 1)); } class _LedgerReply extends Backend { _LedgerReply(FakeFirebaseFirestore db) : super(firestore: db, auth: MockFirebaseAuth()); @override Future> call( String name, [ Map payload = const {}, ]) async => { 'txId': 'receipt', 'balanceAfter': 963, 'lotRefs': [ {'lotId': 'a', 'delta': -12}, {'lotId': 'b', 'delta': -25}, ], }; } void main() { testWidgets('HOME ZAP 연타·응답유실·확정 결과가 실제 행성에 연결된다', (tester) async { tester.view.physicalSize = const Size(1080, 2400); tester.view.devicePixelRatio = 3; addTearDown(tester.view.reset); final repository = _DelayedGalaxy(); final home = await const SampleHomeRepository().load(); await tester.pumpWidget(MaterialApp(theme: buildAppTheme(), home: HomeScreen( repository: const SampleHomeRepository(), questRepository: const SampleQuestRepository(), eclipse: const NoEclipseRepository(), galaxyRepository: repository, ))); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(find.byType(ZapButton), findsOneWidget); await tester.tap(find.byType(ZapButton)); await tester.pump(const Duration(milliseconds: 100)); await tester.tap(find.byType(ZapButton)); await tester.pump(const Duration(milliseconds: 350)); expect(repository.counts, [2]); expect(repository.fandoms, [home.fandomId]); expect(find.textContaining('기여 완료'), findsNothing); final fx = tester.widget(find.byType(ZapFeedback)); expect(fx.source.dy, greaterThan(fx.target.dy)); expect(fx.target.dx, greaterThan(0)); repository.replies.single.completeError(const BackendException('unavailable', 'private error')); await tester.pump(); await tester.tap(find.text('같은 기여 다시 확인')); await tester.pump(); expect(repository.keys.last, repository.keys.first); expect(repository.counts, [2, 2]); repository.replies.last.complete(const ZapResult(txId: 'home-confirmed', balanceAfter: 900, contributedPop: 37)); await tester.pump(); expect(find.textContaining('37 POP 이 '), findsOneWidget); expect(tester.widget(find.byType(ZapButton)).myPop, 900); expect(tester.takeException(), isNull); await tester.pumpWidget(const SizedBox()); }); testWidgets('Reduce Motion은 갤럭시 자전도 정지하고 해제 시 복구한다', (tester) async { Future show(bool reduced) => tester.pumpWidget( MaterialApp( home: MediaQuery( data: MediaQueryData(disableAnimations: reduced), child: GalaxyScreen( repository: const SampleGalaxyRepository(), planetRepository: samplePlanetRepository, eclipse: const NoEclipseRepository(), ), ), ), ); await show(true); await tester.pump(); await tester.pump(const Duration(seconds: 3)); await tester.pump(const Duration(seconds: 3)); expect(tester.binding.transientCallbackCount, 0); await show(false); await tester.pump(const Duration(milliseconds: 50)); expect(tester.binding.transientCallbackCount, greaterThan(0)); await show(true); await tester.pump(const Duration(seconds: 1)); expect(tester.binding.transientCallbackCount, 0); }); test('갤럭시 Zap 은 고른 행성이 가입 팬덤이면 그 행성, 아니면 대표 행성으로 간다 (10/3 #10)', () { const mine = PlanetNode(id: 'a', name: 'A', rank: 1, fpi: 1, members: 1, online: 1, isMine: true); const joined = PlanetNode(id: 'b', name: 'B', rank: 2, fpi: 1, members: 1, online: 1); const other = PlanetNode(id: 'c', name: 'C', rank: 3, fpi: 1, members: 1, online: 1); const snap = GalaxySnapshot(planets: [mine, joined, other], myPop: 0, joinedIds: {'a', 'b'}); expect(snap.zapTargetFor(joined)?.id, 'b'); expect(snap.zapTargetFor(other)?.id, 'a'); expect(snap.zapTargetFor(null)?.id, 'a'); expect(snap.rankingView.zapTargetFor(joined)?.id, 'b'); }); test('기여 결과는 현재 단가가 아니라 서버가 확정한 차감 lot 합계다', () async { final db = FakeFirebaseFirestore(); await db.doc('config/policy').set({'zapPop': 10}); final result = await FirestoreGalaxyRepository( _LedgerReply(db), ).zap(fandomId: 'bts', count: 1, clientSeq: 'retry'); expect(result.contributedPop, 37); }); testWidgets('실제 기여량을 표시하고 같은 거래 응답의 성공 연출은 반복하지 않는다', (tester) async { final repository = _DelayedGalaxy(); await _show(tester, repository); await tester.tap(find.byType(ZapButton)); await tester.pump(const Duration(milliseconds: 500)); const receipt = ZapResult( txId: 'receipt', balanceAfter: 963, contributedPop: 37, ); repository.replies.last.complete(receipt); await tester.pump(); expect(find.textContaining('37 POP 이 '), findsOneWidget); await tester.pump(const Duration(seconds: 2)); expect(find.textContaining('37 POP 이 '), findsNothing); await tester.tap(find.byType(ZapButton)); await tester.pump(const Duration(milliseconds: 500)); repository.replies.last.complete(receipt); await tester.pump(); expect(find.textContaining('37 POP 이 '), findsNothing); }); testWidgets('ZAP 옵션은 2초 전에 열리지 않고 길게 누르면 차감하지 않는다', (tester) async { var taps = 0; await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center( child: ZapButton(zapPop: 10, myPop: 100, onZap: () => taps++), ), ), ), ); final hold = await tester.startGesture( tester.getCenter(find.byType(ZapButton)), ); await tester.pump(const Duration(milliseconds: 1999)); expect(find.text('아이템'), findsNothing); await tester.pump(const Duration(milliseconds: 1)); expect(find.text('아이템'), findsOneWidget); await hold.up(); expect(taps, 0); await tester.pumpAndSettle(); }); testWidgets('전송 중 성공을 보여주지 않고, 전송 중 연타는 앞 묶음 확정 뒤 새 키의 다음 묶음으로 한 번만 보낸다', (tester) async { final repository = _DelayedGalaxy(); await _show(tester, repository); await tester.tap(find.byType(ZapButton)); await tester.pump(const Duration(milliseconds: 500)); expect(repository.replies, hasLength(1)); expect(find.textContaining('기여 반영 중'), findsWidgets); expect(find.textContaining('기여 완료'), findsNothing); await tester.tap(find.byType(ZapButton)); await tester.pump(const Duration(milliseconds: 500)); expect(repository.replies, hasLength(1)); repository.replies.single.complete( const ZapResult(txId: 'confirmed', balanceAfter: 12530), ); await tester.pump(); expect(repository.counts, [1, 1]); expect(repository.keys[1], isNot(repository.keys[0])); expect(find.textContaining('기여 완료'), findsNothing); repository.replies.last.complete( const ZapResult(txId: 'confirmed-2', balanceAfter: 12520), ); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(find.textContaining('기여 완료'), findsOneWidget); expect(repository.replies, hasLength(2)); }); testWidgets('응답 유실 재확인은 같은 멱등키를 쓰고 실패에는 성공 FX가 없다', (tester) async { final repository = _DelayedGalaxy(); await _show(tester, repository); await tester.tap(find.byType(ZapButton)); await tester.pump(const Duration(milliseconds: 500)); repository.replies.single.completeError( const BackendException('unavailable', '연결 실패'), ); await tester.pump(); expect(find.textContaining('기여 완료'), findsNothing); await tester.tap(find.text('다시 확인')); await tester.pump(); expect(repository.keys, hasLength(2)); expect(repository.keys[1], repository.keys[0]); repository.replies.last.complete( const ZapResult(txId: 'confirmed', balanceAfter: 12530), ); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(find.textContaining('기여 완료'), findsOneWidget); }); }