라이브 티커 · 갤럭시 검색 색인 · 주간 랭킹 스냅샷 · 연속 ZAP 상한 · 가입 행성 Zap · 성운 목표 · 랭킹 갱신 안내 · 행성 터치 반응

This commit is contained in:
Sn-Kinos
2026-10-05 22:38:42 +09:00
parent 000c058097
commit 4bb92c2ee7
44 changed files with 727 additions and 78 deletions
+35
View File
@@ -121,6 +121,24 @@ void main() {
expect(home.growth.requiredSfpi, 16575);
});
test('갤럭시 검색은 맵 상위 12개 밖의 행성도 이름 색인으로 찾는다 (Figma 353:217832)', () async {
final db = await _seed();
for (var i = 0; i < 15; i++) {
await db.doc('fandoms/f$i').set({'name': 'Star$i', 'stage': 1, 'fpiScore': 1000 - i});
await db.doc('fandomNames/star$i').set({'fandomId': 'f$i'});
}
await db.doc('fandoms/low').set({'name': 'Moonlight', 'stage': 1, 'fpiScore': 1});
await db.doc('fandomNames/moonlight').set({'fandomId': 'low'});
final repo = FirestoreGalaxyRepository(_backend(db));
final map = await repo.load();
expect(map.planets.any((p) => p.id == 'low'), isFalse);
final hits = await repo.search('MOON');
expect(hits.single.name, 'Moonlight');
expect((await repo.search('star1')).map((p) => p.id).toSet(),
{'f1', 'f10', 'f11', 'f12', 'f13', 'f14'});
expect(await repo.search(' '), isEmpty);
});
test('홈과 상세는 같은 팬덤의 서버 Stage를 표시하고 개인 Stage를 섞지 않는다', () async {
final db = await _seed();
await db.doc('fandoms/bts').update({'stage': 4, 'colorKey': 'gold'});
@@ -134,10 +152,13 @@ void main() {
expect(detail.stage, 4);
expect(detail.isNebula, false);
await db.doc('fandoms/bts').update({'stage': 0});
// 성운 전환 목표는 앱 상수가 아니라 서버와 같은 운영값(B-10).
await db.doc('config/policy').set({'nebulaMinPop': 2000, 'nebulaMinMembers': 12});
final nebula = await FirestorePlanetRepository(_backend(db), fandomId: 'bts')
.load(nebula: false);
expect(nebula.stage, 0);
expect(nebula.isNebula, true);
expect([nebula.nebulaTargetPop, nebula.nebulaTargetMembers], [2000, 12]);
});
test('서버에 단계가 없으면 시작 단계로 지어내지 않는다', () async {
@@ -559,6 +580,20 @@ void main() {
expect(ranking.mine!.change, 1);
});
test('랭킹은 마지막 갱신 시각과 갱신 주기를 함께 준다 — 별첨 1 ② 예외처리', () async {
final db = await seedPlanet();
final at = DateTime.utc(2026, 10, 7, 3, 4);
await db.doc('fandoms/bts').update({
'fpiUpdatedAt': Timestamp.fromDate(at),
'memberRankedAt': Timestamp.fromDate(at),
});
final repo = FirestorePlanetRepository(_backend(db), fandomId: 'bts');
final planets = await repo.planetRanking(metric: FpiMetric.bpm);
expect([planets.updatedAt, planets.refreshMinutes], [at.toLocal(), 1]);
final users = await repo.userRanking(metric: FpiMetric.bpm, fandomId: 'bts');
expect([users.updatedAt, users.refreshMinutes], [at.toLocal(), 5]);
});
test('랭킹 모집단은 최근 7일 유효 활동 행성뿐이다 — 별첨 B', () async {
final db = await seedPlanet();
final ranking = await FirestorePlanetRepository(_backend(db), fandomId: 'bts')
+4 -4
View File
@@ -247,7 +247,7 @@ void main() {
await _settle(tester);
expect(
find.text('[LIVE] 버니즈님이 이클립스 방어에 참여했습니다! (+30)'),
find.text('[LIVE] 버니즈님이 이클립스 방어에 합류했습니다!'),
findsOneWidget,
);
});
@@ -269,7 +269,7 @@ void main() {
expect(find.textContaining('버니즈'), findsNothing);
expect(
find.text('[LIVE] 캐럿님이 이클립스 방어에 참여했습니다! (+50)'),
find.text('[LIVE] 캐럿님이 이클립스 방어에 합류했습니다!'),
findsOneWidget,
);
});
@@ -304,7 +304,7 @@ void main() {
// 이벤트가 오면 그 문구가 폴백을 대체한다.
expect(find.textContaining('아이템 사용으로 버프가 발동'), findsNothing);
expect(
find.text('[LIVE] 무민님이 이클립스 방어에 참여했습니다! (+20)'),
find.text('[LIVE] 무민님이 이클립스 방어에 합류했습니다!'),
findsOneWidget,
);
});
@@ -313,7 +313,7 @@ void main() {
final at = DateTime(2026, 9, 14);
expect(
liveEventMessage(LiveEvent(id: 'x', nickname: '나', defense: 1200, at: at)),
'[LIVE] 나님이 이클립스 방어에 참여했습니다! (+1,200)',
'[LIVE] 나님이 이클립스 방어에 합류했습니다!',
);
// 별첨 B 「Global Ticker」 — 다른 종류가 같은 컬렉션에 들어온다.
expect(
+4
View File
@@ -197,6 +197,10 @@ class RecordingGalaxyRepository implements GalaxyRepository {
@override
Future<void> setPrimary(String fandomId) async {}
@override
Future<List<PlanetNode>> search(String query) =>
const SampleGalaxyRepository().search(query);
@override
Future<GalaxySnapshot> load() async =>
snapshot ?? await const SampleGalaxyRepository().load();
+3
View File
@@ -67,6 +67,9 @@ class _TopOrbitRepository implements GalaxyRepository {
@override
Future<void> setPrimary(String fandomId) async {}
@override
Future<List<PlanetNode>> search(String query) async => const [];
@override
Future<ZapResult> zap({
required String fandomId,
+15 -4
View File
@@ -104,7 +104,7 @@ void main() {
expect(repository.counts, [2, 2]);
repository.replies.last.complete(const ZapResult(txId: 'home-confirmed', balanceAfter: 900, contributedPop: 37));
await tester.pump();
expect(find.text('37 POP 기여 완료'), findsOneWidget);
expect(find.textContaining('37 POP 이 '), findsOneWidget);
expect(tester.widget<ZapButton>(find.byType(ZapButton)).myPop, 900);
expect(tester.takeException(), isNull);
await tester.pumpWidget(const SizedBox());
@@ -135,6 +135,17 @@ void main() {
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});
@@ -156,14 +167,14 @@ void main() {
);
repository.replies.last.complete(receipt);
await tester.pump();
expect(find.text('37 POP 기여 완료'), findsOneWidget);
expect(find.textContaining('37 POP 이 '), findsOneWidget);
await tester.pump(const Duration(seconds: 2));
expect(find.text('37 POP 기여 완료'), findsNothing);
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.text('37 POP 기여 완료'), findsNothing);
expect(find.textContaining('37 POP 이 '), findsNothing);
});
testWidgets('ZAP 옵션은 2초 전에 열리지 않고 길게 누르면 차감하지 않는다', (tester) async {
+10
View File
@@ -7,6 +7,16 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('진화 5% 전 아우라는 두 조건 중 낮은 쪽이 95% 이상일 때 (Figma 2024:20758)', () {
bool imminent(num pop, num sfpi) => PlanetVisualState.fromDocument({
'levelPop': pop, 'nextStagePop': 100, 'sfpi': sfpi, 'nextStageSfpi': 100,
}).evolutionImminent;
expect(imminent(96, 99), isTrue);
expect(imminent(99, 94), isFalse);
expect(imminent(100, 100), isFalse, reason: '도달은 진화 판정의 몫');
expect(PlanetVisualState.fromDocument({'stage': 2}).evolutionImminent, isFalse);
});
test('컬러는 마스터(config/planetColors)가 정한다 — 추가 색은 그리고 숨긴 색은 고르지 못한다 (10/3 #6)', () {
final master = planetColorsFrom({'colors': [
{'key': 'gold', 'hex': '#FFC500', 'visible': false},
+15
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:fancake/core/theme/app_theme.dart';
import 'package:fancake/core/ui/live_planet.dart';
import 'package:fancake/features/home/widgets/home_hero.dart';
import 'package:fancake/features/home/widgets/growth_conditions.dart';
import 'package:fancake/features/home/widgets/planet_growth_card.dart';
@@ -7,6 +8,20 @@ import 'package:fancake/features/home/home_data.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('행성 본체를 누르면 흔들린 뒤 커뮤니티로 간다 (Figma 2024:20758)', (tester) async {
const growth = PlanetGrowth(fandomName: 'BTS', contributorName: '팬', stage: 2,
contributedPop: 0, growthPercent: null, currentPop: null, requiredPop: null);
var opened = 0;
await tester.pumpWidget(MaterialApp(theme: buildAppTheme(),
home: Scaffold(body: HomeHero(growth: growth, fandomId: 'bts', onTapPlanet: () => opened++))));
await tester.pump(const Duration(milliseconds: 100));
await tester.tap(find.byType(LivePlanet25D), warnIfMissed: false);
await tester.pump(const Duration(milliseconds: 100));
expect(opened, 0, reason: '흔들림이 끝난 뒤 이동한다');
await tester.pump(const Duration(milliseconds: 400));
expect(opened, 1);
});
testWidgets('기여 기록이 없으면 두 성장 영역에 가짜 0 POP 기여를 표시하지 않는다', (tester) async {
const growth = PlanetGrowth(fandomName: 'BTS', contributorName: '팬',
contributedPop: 0, growthPercent: null, currentPop: null, requiredPop: null);