Files
fanuzu/app/test/planet_evolution_test.dart

303 lines
12 KiB
Dart

import 'package:cloud_firestore/cloud_firestore.dart';
import 'dart:async';
import 'package:fancake/core/data/planet_visual_state.dart';
import 'package:fancake/core/ui/live_planet.dart';
import 'package:fancake/core/data/backend.dart';
import 'package:fancake/core/data/repositories.dart';
import 'package:fake_cloud_firestore/fake_cloud_firestore.dart';
import 'package:firebase_auth_mocks/firebase_auth_mocks.dart';
import 'package:fancake/core/ui/planet_25d.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
for (final reduced in [false, true]) {
testWidgets('진화 완료 기록 버튼: 첫 조회/Level 제외, Reduce Motion=$reduced', (tester) async {
var opened = 0;
Future<void> show(int stage, int level) => tester.pumpWidget(MaterialApp(
home: MediaQuery(data: MediaQueryData(disableAnimations: reduced),
child: Scaffold(body: Center(child: Planet25D(
image: 'assets/figma/planet-home.png', size: 240,
stage: stage, level: level, onOpenHistory: () => opened++,
)))),
));
await show(1, 1);
await tester.pump(const Duration(seconds: 5));
expect(find.text('PASSPORT 기록 보기'), findsNothing);
await show(1, 2);
await tester.pump(const Duration(seconds: 2));
expect(find.text('PASSPORT 기록 보기'), findsNothing);
await show(2, 3);
await tester.pump(const Duration(milliseconds: 500));
expect(find.text('PASSPORT 기록 보기'), findsNothing);
await tester.pump(const Duration(seconds: 4));
expect(find.text('PASSPORT 기록 보기'), findsOneWidget);
await tester.tap(find.text('PASSPORT 기록 보기'));
await tester.pump();
expect(opened, 1);
expect(find.text('PASSPORT 기록 보기'), findsNothing);
await show(2, 3);
await tester.pump(const Duration(seconds: 4));
expect(find.text('PASSPORT 기록 보기'), findsNothing);
});
}
test('팬덤 서버 스트림은 유효 Stage/Level 변화만 전달한다', () async {
final db = FakeFirebaseFirestore();
await db.doc('fandoms/bts').set({'stage': 1, 'level': 2});
final repos = Repositories(
live: true,
backend: Backend(firestore: db, auth: MockFirebaseAuth()),
);
final states = <PlanetVisualState>[];
final sub = repos.watchPlanet('bts').listen(states.add);
await Future<void>.delayed(Duration.zero);
expect(states.last.stage, 1);
expect(states.last.level, 2);
await db.doc('fandoms/bts').update({'stage': 2, 'level': 10});
await Future<void>.delayed(Duration.zero);
expect(states.last.stage, 2);
expect(states.last.level, 10);
await db.doc('fandoms/bts').update({'stage': 9, 'level': -1});
await Future<void>.delayed(Duration.zero);
expect(states.last.stage, isNull);
expect(states.last.level, isNull);
await sub.cancel();
});
test('Eclipse 성공은 행성 조명만 회복하고 Stage/Level은 유지한다', () async {
final db = FakeFirebaseFirestore();
await db.doc('fandoms/bts').set({'stage': 3, 'level': 20});
final event = db.doc('fandoms/bts/eclipseRounds/one');
await event.set({
'status': 'active',
'startAt': Timestamp.now(),
'defended': false,
});
final repos = Repositories(
live: true,
backend: Backend(firestore: db, auth: MockFirebaseAuth()),
);
final states = <PlanetVisualState>[];
final sub = repos.watchPlanet('bts').listen(states.add);
addTearDown(sub.cancel);
await Future<void>.delayed(const Duration(milliseconds: 20));
expect(states.last.isEclipsed, true);
expect(states.last.stage, 3);
await event.update({'defended': true});
await Future<void>.delayed(const Duration(milliseconds: 20));
expect(states.last.isEclipsed, false);
expect(states.last.stage, 3);
expect(states.last.level, 20);
});
testWidgets('Eclipse 밝기는 점진 복귀하며 행성 표면과 단계는 유지한다', (tester) async {
Future<void> show(bool danger) => tester.pumpWidget(
MaterialApp(
home: Center(
child: Planet25D(
image: 'assets/figma/planet-home.png',
size: 200,
stage: 3,
isEclipsed: danger,
),
),
),
);
final opacity = find.byKey(const ValueKey('planet-surface-light'));
await show(true);
expect(tester.widget<Opacity>(opacity).opacity, 0.5);
await show(false);
await tester.pump(const Duration(seconds: 1));
expect(tester.widget<Opacity>(opacity).opacity, greaterThan(0.5));
expect(tester.widget<Opacity>(opacity).opacity, lessThan(1));
await tester.pump(const Duration(seconds: 1));
expect(tester.widget<Opacity>(opacity).opacity, 1);
expect(tester.widget<Planet25D>(find.byType(Planet25D)).stage, 3);
expect(find.text('이상 반응'), findsNothing);
expect(tester.takeException(), isNull);
});
test('HERO TIME은 실제 남은 Pool과 서버 종료시각을 따르고 Stage는 유지한다', () async {
final db = FakeFirebaseFirestore();
await db.doc('fandoms/bts').set({'stage': 3, 'level': 20});
final drop = db.doc('fandoms/bts/heroDrops/one');
await drop.set({
'heroNickname': '공개팬',
'claimedCount': 3,
'triggeredBy': 'private-uid',
'email': 'private@example.com',
'remainingPop': 100,
'endsAt': Timestamp.fromDate(
DateTime.now().add(const Duration(hours: 1)),
),
});
final repos = Repositories(
live: true,
backend: Backend(firestore: db, auth: MockFirebaseAuth()),
);
final states = <PlanetVisualState>[];
final sub = repos.watchPlanet('bts').listen(states.add);
addTearDown(sub.cancel);
await Future<void>.delayed(const Duration(milliseconds: 20));
expect(states.last.isHeroTime, true);
expect(states.last.heroEndsAt, isNotNull);
expect(states.last.heroNicknames, ['공개팬']);
expect(states.last.heroClaimCount, 3);
// 닉네임 없는 영웅은 서버가 「팬」으로 싣는다 — 이름 줄에는 띄우지 않는다(M-7 이전과 같음).
await db.doc('fandoms/bts/heroDrops/nameless').set({
'heroNickname': '팬',
'claimedCount': 0,
'remainingPop': 50,
'endsAt': Timestamp.fromDate(
DateTime.now().add(const Duration(hours: 1)),
),
});
await Future<void>.delayed(const Duration(milliseconds: 20));
expect(states.last.heroNicknames, ['공개팬']);
await db.doc('fandoms/bts/heroDrops/nameless').delete();
await Future<void>.delayed(const Duration(milliseconds: 20));
expect(states.last.stage, 3);
await drop.update({'remainingPop': 0});
await Future<void>.delayed(const Duration(milliseconds: 20));
expect(states.last.isHeroTime, false);
expect(states.last.heroEndsAt, isNull);
await drop.update({
'remainingPop': 100,
'endsAt': Timestamp.fromDate(
DateTime.now().subtract(const Duration(seconds: 1)),
),
});
await Future<void>.delayed(const Duration(milliseconds: 20));
expect(states.last.isHeroTime, false);
expect(states.last.level, 20);
});
test('마지막 HERO TIME 종료시각에 추가 스냅샷 없이 축제를 해제한다', () async {
final db = FakeFirebaseFirestore();
await db.doc('fandoms/bts').set({'stage': 3});
await db.doc('fandoms/bts/heroDrops/one').set({
'remainingPop': 100,
'endsAt': Timestamp.fromDate(
DateTime.now().add(const Duration(seconds: 1)),
),
});
final repos = Repositories(
live: true,
backend: Backend(firestore: db, auth: MockFirebaseAuth()),
);
final states = <PlanetVisualState>[];
final sub = repos.watchPlanet('bts').listen(states.add);
addTearDown(sub.cancel);
await Future<void>.delayed(const Duration(milliseconds: 20));
expect(states.last.isHeroTime, true);
await Future<void>.delayed(const Duration(milliseconds: 1100));
expect(states.last.isHeroTime, false);
expect(states.last.stage, 3);
});
testWidgets('첫 실시간 값은 캐시와 달라도 진화로 연출하지 않는다', (tester) async {
final states = StreamController<PlanetVisualState>();
await tester.pumpWidget(
MaterialApp(
home: Center(
child: LivePlanet25D(
fandomId: 'bts',
watch: (_) => states.stream,
stage: 0,
image: 'assets/figma/planet-home.png',
size: 240,
),
),
),
);
states.add(const PlanetVisualState(stage: 2, level: 10));
await tester.pump();
await tester.pump();
expect(tester.widget<Planet25D>(find.byType(Planet25D)).stage, 2);
expect(find.text('이상 반응'), findsNothing);
states.add(const PlanetVisualState(stage: 3, level: 20));
await tester.pump();
await tester.pump();
expect(find.text('이상 반응'), findsOneWidget);
await tester.pump(const Duration(seconds: 4));
await tester.pumpWidget(const SizedBox());
await tester.runAsync(states.close);
});
testWidgets('서버 Stage 상승은 5구간 진화로 공개하고 재수신에는 반복하지 않는다', (tester) async {
Future<void> show(int stage) => tester.pumpWidget(
MaterialApp(
home: Center(
child: Planet25D(
image: 'assets/figma/planet-home.png',
size: 240,
stage: stage,
),
),
),
);
await show(1);
expect(find.text('에너지 집중'), findsNothing);
await show(2);
expect(find.text('이상 반응'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 600));
expect(find.text('에너지 집중'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 650));
expect(find.text('전환'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 400));
expect(find.text('Stage 2 · 성장'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 800));
expect(find.text('행성 진화 완료'), findsOneWidget);
await tester.pumpAndSettle();
await show(2);
expect(find.text('이상 반응'), findsNothing);
await show(1); // 정정된 낮은 값 이후 같은 최고 Stage도 재축하하지 않는다.
await show(2);
expect(find.text('이상 반응'), findsNothing);
});
testWidgets('Level 상승은 Stage Evolution과 구분된 짧은 결과를 표시한다', (tester) async {
Future<void> show(int level) => tester.pumpWidget(
MaterialApp(
home: Center(
child: Planet25D(
image: 'assets/figma/planet-home.png',
size: 240,
stage: 2,
level: level,
),
),
),
);
await show(5);
await show(6);
expect(find.text('Lv.6'), findsOneWidget);
expect(find.text('이상 반응'), findsNothing);
await tester.pumpAndSettle();
expect(find.text('Lv.6'), findsNothing);
});
testWidgets('Reduce Motion에서도 확정된 새 Stage를 즉시 알린다', (tester) async {
Future<void> show(int stage) => tester.pumpWidget(
MaterialApp(
home: MediaQuery(
data: const MediaQueryData(disableAnimations: true),
child: Center(
child: Planet25D(
image: 'assets/figma/planet-home.png',
size: 240,
stage: stage,
),
),
),
),
);
await show(0);
await show(1);
expect(find.text('이상 반응'), findsNothing);
expect(find.text('Stage 1 · 시작'), findsOneWidget);
await tester.pumpAndSettle();
});
}