303 lines
12 KiB
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();
|
|
});
|
|
}
|