FPI 사용자·국가 랭킹 서버 집계 · YouTube 스밍 · Music Shake 참여
This commit is contained in:
@@ -310,6 +310,22 @@ void main() {
|
||||
final snapshot = await FirestoreGalaxyRepository(_backend(db)).load();
|
||||
final rank = {for (final p in snapshot.planets) p.id: p.rank};
|
||||
expect(rank, {'bts': 1, 'svt': 2, 'neb': 3});
|
||||
expect(snapshot.planets.firstWhere((p) => p.id == 'neb').rankLabel, '-');
|
||||
});
|
||||
|
||||
// 원장 B-17 — 최근 7일 유효 활동이 없는 정식 행성(서버 rankingEligible: false)도 순위 밖이다.
|
||||
test('최근 7일 활동이 없는 행성은 순위 없이 바깥 궤도에 서고 Ranking View 중심이 되지 않는다', () async {
|
||||
final db = await _seed();
|
||||
await db.doc('fandoms/idle').set({
|
||||
'name': '휴면', 'stage': 2, 'fpiScore': 999999, 'rankingEligible': false,
|
||||
});
|
||||
await db.doc('fandoms/svt').set({
|
||||
'name': 'SEVENTEEN', 'stage': 1, 'fpiScore': 120000, 'rankingEligible': true,
|
||||
});
|
||||
final snapshot = await FirestoreGalaxyRepository(_backend(db)).load();
|
||||
final rank = {for (final p in snapshot.planets) p.id: (p.rank, p.rankLabel)};
|
||||
expect(rank, {'bts': (1, '1'), 'svt': (2, '2'), 'idle': (3, '-')});
|
||||
expect(snapshot.rankingView.planets.firstWhere((p) => p.isCenter).id, 'bts');
|
||||
});
|
||||
|
||||
test('대표 팬덤은 primaryFandomId 가 정본이고 가입 목록은 최신 가입순이다(C-15 · §2.58)', () async {
|
||||
@@ -466,13 +482,12 @@ void main() {
|
||||
expect((await repo.load(nebula: false)).totalFpi, 300000);
|
||||
});
|
||||
|
||||
test('내 기여 상위 집계가 없으면 Mock 사유를 화면에 넘긴다 — 대조표 3-5', () async {
|
||||
test('서버가 내 기여 상위 % 를 적지 않은 축은 null 이다 — 「상위 100%」를 짓지 않는다', () async {
|
||||
final db = await seedPlanet();
|
||||
await db.doc('fandoms/bts/members/$_uid')
|
||||
.set({'nickname': '나'});
|
||||
final detail = await FirestorePlanetRepository(_backend(db), fandomId: 'bts')
|
||||
.load(nebula: false);
|
||||
expect(detail.mockNote, isNotNull);
|
||||
expect(detail.scores.every((s) => s.myTopPercent == null), isTrue);
|
||||
});
|
||||
|
||||
@@ -551,6 +566,24 @@ void main() {
|
||||
expect(referral.mine!.rank, 1);
|
||||
});
|
||||
|
||||
test('사용자 순위 · 순위 변동은 서버 5분 집계(rankByAxis · rankPrevByAxis)를 읽는다', () async {
|
||||
final db = await seedPlanet();
|
||||
await db.doc('fandoms/bts/members/other').set({
|
||||
'rankByAxis': {'bpm': 1}, 'rankPrevByAxis': {'bpm': 2},
|
||||
}, SetOptions(merge: true));
|
||||
await db.doc('fandoms/bts/members/$_uid').set({
|
||||
'rankByAxis': {'bpm': 2}, 'rankPrevByAxis': {'bpm': 1},
|
||||
}, SetOptions(merge: true));
|
||||
final repo = FirestorePlanetRepository(_backend(db), fandomId: 'bts');
|
||||
final bpm = await repo.userRanking(metric: FpiMetric.bpm, fandomId: 'bts');
|
||||
expect(bpm.rows.map((r) => (r.name, r.rank, r.change)).toList(), [
|
||||
('버니즈', 1, 1),
|
||||
('나', 2, -1),
|
||||
]);
|
||||
expect(bpm.mine!.change, -1);
|
||||
expect(bpm.mockNote, isNull);
|
||||
});
|
||||
|
||||
test('국가 랭킹은 사전 집계 1건을 읽고, 없으면 Mock 이라고 알린다', () async {
|
||||
final db = await seedPlanet();
|
||||
final repo = FirestorePlanetRepository(_backend(db), fandomId: 'bts');
|
||||
|
||||
@@ -309,14 +309,14 @@ void main() {
|
||||
expect(find.text('광고 추가 보상 +10 POP'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('회신 대기 유형은 참여 버튼이 잠기고 사용자 문구로만 알린다', (tester) async {
|
||||
testWidgets('스트리밍은 무보상 참여로 열려 있고 보상 · 광고 화면 없이 참여만 알린다(C-04)', (tester) async {
|
||||
await pumpWide(tester, const QuestDetailScreen(questId: 'q6', repository: _sample));
|
||||
await tester.pump();
|
||||
final button = tester.widget<FilledButton>(
|
||||
find.widgetWithText(FilledButton, '준비 중입니다.'),
|
||||
);
|
||||
expect(button.onPressed, isNull);
|
||||
expect(find.text('이 퀘스트는 아직 참여할 수 없습니다.'), findsOneWidget);
|
||||
expect(find.text('이 퀘스트는 아직 참여할 수 없습니다.'), findsNothing);
|
||||
await tester.tap(find.widgetWithText(FilledButton, '퀘스트 시작하기'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('참여가 기록되었어요. 이 퀘스트는 POP 보상이 없어요.'), findsOneWidget);
|
||||
expect(find.text('기본 보상 받기'), findsNothing);
|
||||
// 내부 질의 번호 · 「발주사」 · MOCK 은 화면에 내보내지 않는다.
|
||||
expect(find.textContaining('Q-QC-'), findsNothing);
|
||||
expect(find.textContaining('발주사'), findsNothing);
|
||||
|
||||
@@ -320,16 +320,67 @@ service cloud.firestore {
|
||||
expect(backend.calls, isEmpty);
|
||||
});
|
||||
|
||||
test('회신 대기 유형은 어느 함수도 부르지 않는다', () async {
|
||||
test('시청 · 스트리밍은 무보상 참여 함수만 부른다(C-04) — 보상은 0', () async {
|
||||
final db = await _seed();
|
||||
final backend = _RecordingBackend(db);
|
||||
final backend = _RecordingBackend(db, reply: const {'rewarded': 0, 'remaining': 0});
|
||||
final repo = FirestoreQuestRepository(backend);
|
||||
final cards = await repo.load();
|
||||
final card = _card(cards, 'stream');
|
||||
final card = _card(await repo.load(), 'stream');
|
||||
|
||||
expect(card.type.verifiable, isFalse);
|
||||
expect(card.type.verifiable, isTrue);
|
||||
// 문서에 rewardPop 10 이 남아 있어도 화면에 보상을 약속하지 않는다.
|
||||
expect(card.rewardPop, 0);
|
||||
final reward = await repo.verify(card);
|
||||
expect(backend.calls.single.name, 'fnQuestStreamParticipate');
|
||||
expect(backend.calls.single.payload, {'questId': 'stream'});
|
||||
expect(reward.rewardedPop, 0);
|
||||
});
|
||||
|
||||
test('Music Shake — 박마다 구간을 세고 판정표 · 곡 ID 를 서버에 보낸다', () async {
|
||||
final db = await _seed();
|
||||
// 600 BPM = 박 100ms · 3 구간. 박 0 과 박 1 근처에서만 흔든다.
|
||||
await db.doc('quests/shake').set({
|
||||
'type': 'musicshake', 'title': '응원 흔들기', 'dailyLimit': 1,
|
||||
'trackIds': ['track-a'], 'totalSegments': 3, 'bpm': 600,
|
||||
});
|
||||
final backend = _RecordingBackend(db);
|
||||
final repo = FirestoreQuestRepository(
|
||||
backend,
|
||||
shakes: () async* {
|
||||
final start = DateTime.now();
|
||||
yield start;
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
yield start.add(const Duration(milliseconds: 140));
|
||||
},
|
||||
);
|
||||
final card = _card(await repo.load(), 'shake');
|
||||
expect(card.type.verifiable, isTrue);
|
||||
expect(card.goal, 3);
|
||||
expect(card.progress, 0);
|
||||
|
||||
// 판을 끝내기 전에는 서버를 부르지 않는다.
|
||||
await expectLater(repo.verify(card), throwsA(isA<BackendException>()));
|
||||
expect(backend.calls, isEmpty);
|
||||
|
||||
expect(await repo.progressStream(card).toList(), [1, 2, 3]);
|
||||
await repo.verify(card);
|
||||
final call = backend.calls.single;
|
||||
expect(call.name, 'fnQuestVerifyMusicShake');
|
||||
expect(call.payload['questId'], 'shake');
|
||||
expect(call.payload['trackId'], 'track-a');
|
||||
expect(call.payload['totalSegments'], 3);
|
||||
final j = call.payload['judgement'] as Map;
|
||||
expect(j['perfect']! + j['good']! + j['miss']!, 3);
|
||||
expect(j['miss'], 1);
|
||||
});
|
||||
|
||||
test('ShakeJudge — 박과의 거리로 perfect · good · miss 를 가르고 최장 연속을 센다', () {
|
||||
final start = DateTime(2026, 10, 5);
|
||||
final judge = ShakeJudge(bpm: 60, segments: 4, start: start)
|
||||
..add(start.add(const Duration(milliseconds: 100))) // 박 0 · perfect
|
||||
..add(start.add(const Duration(milliseconds: 1400))) // 박 1 · good
|
||||
..add(start.add(const Duration(milliseconds: 3000))); // 박 3 · perfect, 박 2 miss
|
||||
expect(judge.judgement(), {'perfect': 2, 'good': 1, 'miss': 1, 'maxCombo': 2});
|
||||
expect(judge.end, start.add(const Duration(milliseconds: 3500)));
|
||||
});
|
||||
|
||||
test('생성은 fn-quest-create 로 — 종료일은 날짜 + 그 시각의 UTC 오프셋(C-06)', () async {
|
||||
|
||||
Reference in New Issue
Block a user