라이브 티커 · 갤럭시 검색 색인 · 주간 랭킹 스냅샷 · 연속 ZAP 상한 · 가입 행성 Zap · 성운 목표 · 랭킹 갱신 안내 · 행성 터치 반응
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user