퀘스트 일시 정지 · 이어하기 · 진행 상태 서버 저장 · 목록 보상받기

This commit is contained in:
Sn-Kinos
2026-10-06 03:20:46 +09:00
parent ecf5998725
commit abe5f1ae50
28 changed files with 1023 additions and 118 deletions
+120 -1
View File
@@ -1,5 +1,5 @@
import 'package:fake_cloud_firestore/fake_cloud_firestore.dart';
import 'package:cloud_firestore/cloud_firestore.dart' show SetOptions;
import 'package:cloud_firestore/cloud_firestore.dart' show SetOptions, Timestamp;
import 'package:fancake/core/data/backend.dart';
import 'package:fancake/features/quest/quest_data.dart';
import 'package:fancake/features/quest/quest_evidence.dart';
@@ -627,4 +627,123 @@ service cloud.firestore {
expect(live.capacity, 5);
});
});
group('진행 상태 저장 · 일시 정지 · 최신순 (원장 C-83 · 명세 518:88395)', () {
Future<FakeFirebaseFirestore> shakeSeed() async {
final db = await _seed();
await db.doc('quests/shake').set({
'type': 'musicshake', 'title': '응원 흔들기', 'dailyLimit': 1,
'totalSegments': 60, 'bpm': 120,
'link': {'url': 'https://youtu.be/WMweEpGlu_U'},
});
return db;
}
test('목록은 createdAt 내림차순으로 서버 정렬해 커서로 이어 읽는다', () async {
final db = FakeFirebaseFirestore();
await db.doc('users/$_uid').set({'uid': _uid});
await db.doc('config/policy').set({'attendPop': 10});
for (var i = 0; i < 12; i++) {
await db.doc('quests/q$i').set({
'type': 'attend', 'title': 'Q$i', 'dailyLimit': 1,
'createdAt': Timestamp.fromMillisecondsSinceEpoch(1000000 + i * 1000),
});
}
final repo = FirestoreQuestRepository(_RecordingBackend(db));
final first = await repo.load();
final next = await repo.more(offset: first.length);
expect([...first, ...next].map((c) => c.id).toList(),
[for (var i = 11; i >= 0; i--) 'q$i']);
});
test('Music Shake 세션 — 화면 밖 진행중은 일시 정지 · 진행완료는 보상받기 · 진행중 탭이 받는다', () async {
final db = await shakeSeed();
final ref = db.doc('users/$_uid/questProgress/shake');
final repo = FirestoreQuestRepository(_RecordingBackend(db));
await ref.set({'session': {'state': 'inProgress', 'progress': 43, 'total': 60}});
var card = _card(await repo.load(), 'shake');
// 앱이 꺼진 진행중도 진행 화면 밖이므로 일시 정지다.
expect(card.stage, QuestStage.paused);
expect(card.stage.buttonLabel, '이어하기');
expect(QuestTab.inProgress.accepts(card), isTrue);
await ref.set({'session': {
'state': 'completed', 'progress': 60, 'total': 60,
'judgement': {'perfect': 50, 'good': 5, 'miss': 5, 'maxCombo': 30},
}});
card = _card(await repo.load(), 'shake');
expect(card.stage, QuestStage.completed);
expect(QuestTab.inProgress.accepts(card), isTrue);
});
test('목록에서 바로 받는 Music Shake 는 서버 세션의 판정표를 보낸다', () async {
final db = await shakeSeed();
await db.doc('users/$_uid/questProgress/shake').set({'session': {
'state': 'completed', 'progress': 60, 'total': 60,
'judgement': {'perfect': 50, 'good': 5, 'miss': 5, 'maxCombo': 30},
}});
final backend = _RecordingBackend(db);
final repo = FirestoreQuestRepository(backend);
await repo.verify(_card(await repo.load(), 'shake'));
final call = backend.calls.single;
expect(call.name, 'fnQuestVerifyMusicShake');
expect(call.payload['totalSegments'], 60);
expect(call.payload['judgement'], {'perfect': 50, 'good': 5, 'miss': 5, 'maxCombo': 30});
});
test('Walk 는 일시 정지 없이 오늘 걸음으로 진행중 · 진행완료를 낸다(C-62)', () async {
final db = await _seed();
await db.doc('users/$_uid/questProgress/steps').set({
'session': {'state': 'inProgress', 'progress': 0, 'total': 10000},
});
var steps = 4_000;
final repo = FirestoreQuestRepository(
_RecordingBackend(db),
todaySteps: () async => steps,
);
var card = _card(await repo.load(), 'steps');
expect((card.stage, card.progress), (QuestStage.inProgress, 4_000));
steps = 10_200; // 화면 밖에서 걸었다.
card = _card(await repo.load(), 'steps');
expect((card.stage, card.progress), (QuestStage.completed, 10_200));
});
test('진행 상태는 fnQuestProgress 로만 쓰고 중단은 none 이다', () async {
final db = await _seed();
final backend = _RecordingBackend(db);
final repo = FirestoreQuestRepository(backend);
await repo.advance('shake', QuestStage.paused,
session: const QuestSession(
state: QuestStage.paused, progress: 43, total: 60,
position: Duration(milliseconds: 21500), hits: [0.1, -1],
));
await repo.advance('steps', QuestStage.notStarted);
expect(backend.calls.map((c) => c.name), ['fnQuestProgress', 'fnQuestProgress']);
expect(backend.calls.first.payload, {
'questId': 'shake', 'state': 'paused', 'progress': 43, 'total': 60,
'positionMs': 21500, 'hits': [0.1, -1.0],
});
expect(backend.calls.last.payload, {'questId': 'steps', 'state': 'none'});
});
test('캡슐은 가장 최근에 멈춘 Quest 1건이다(QA 3-34)', () async {
final db = await shakeSeed();
await db.doc('quests/shake2').set({
'type': 'musicshake', 'title': '두 번째', 'totalSegments': 30, 'bpm': 100,
});
await db.doc('users/$_uid/questProgress/shake').set({'session': {
'state': 'paused', 'progress': 43, 'total': 60,
'updatedAt': Timestamp.fromMillisecondsSinceEpoch(2000),
}});
await db.doc('users/$_uid/questProgress/shake2').set({'session': {
'state': 'paused', 'progress': 3, 'total': 30,
'updatedAt': Timestamp.fromMillisecondsSinceEpoch(1000),
}});
final repo = FirestoreQuestRepository(_RecordingBackend(db));
final card = await repo.pausedQuest();
expect(card?.id, 'shake');
expect((card?.session?.progress, card?.session?.total), (43, 60));
});
});
}