퀘스트 일시 정지 · 이어하기 · 진행 상태 서버 저장 · 목록 보상받기
This commit is contained in:
@@ -7,6 +7,7 @@ import 'package:fancake/features/quest/quest_data.dart';
|
||||
import 'package:fancake/features/quest/quest_detail_screen.dart';
|
||||
import 'package:fancake/features/quest/quest_list_screen.dart';
|
||||
import 'package:fancake/features/quest/quest_reward_screen.dart';
|
||||
import 'package:fancake/features/quest/widgets/paused_quest_capsule.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@@ -516,6 +517,50 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('일시 정지 · 이어하기 · 목록 보상받기 (원장 C-83 · 명세 518:88405)', () {
|
||||
testWidgets('목록 「보상받기」는 상세를 거쳐 바로 수령 흐름(보상 안내 모달)을 연다', (tester) async {
|
||||
await pumpWide(tester, const QuestListScreen(repository: _sample));
|
||||
await tester.pump();
|
||||
await tester.tap(
|
||||
find.descendant(of: find.byType(QuestTabStrip), matching: find.text(QuestTab.inProgress.label)),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.tap(find.widgetWithText(FilledButton, '보상받기'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('+10 기여 POP'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('일시 정지 Quest — 상세에 멈춘 진행값 · 「이어하기」, 진행중 탭 카드에 「일시 정지」', (tester) async {
|
||||
await pumpWide(tester, const QuestDetailScreen(questId: 's1', repository: _PausedRepo()));
|
||||
await tester.pump();
|
||||
expect(find.text('일시 정지'), findsOneWidget);
|
||||
expect(find.text('43 / 60'), findsOneWidget);
|
||||
expect(find.widgetWithText(FilledButton, '이어하기'), findsOneWidget);
|
||||
|
||||
await pumpWide(tester, const QuestListScreen(repository: _PausedRepo()));
|
||||
await tester.pump();
|
||||
await tester.tap(
|
||||
find.descendant(of: find.byType(QuestTabStrip), matching: find.text(QuestTab.inProgress.label)),
|
||||
);
|
||||
await tester.pump();
|
||||
expect(find.text('Music Shake · 일시 정지 · 43/60'), findsOneWidget);
|
||||
expect(find.widgetWithText(FilledButton, '이어하기'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('탭 위 캡슐 — 일시 정지 Quest 가 있을 때만 한 줄 · 누르면 그 상세', (tester) async {
|
||||
await pumpWide(tester, const Scaffold(body: PausedQuestCapsule(repository: _PausedRepo())));
|
||||
await tester.pump();
|
||||
expect(find.text('일시 정지 · 응원 흔들기 · 43/60'), findsOneWidget);
|
||||
await tester.tap(find.byKey(const ValueKey('paused-quest-capsule')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(QuestDetailScreen), findsOneWidget);
|
||||
|
||||
await pumpWide(tester, const Scaffold(body: PausedQuestCapsule(repository: _sample)));
|
||||
await tester.pump();
|
||||
expect(find.byKey(const ValueKey('paused-quest-capsule')), findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('F4 화면 문구에 내부 경로가 새지 않는다', (tester) async {
|
||||
for (final screen in <Widget>[
|
||||
const QuestListScreen(repository: _sample),
|
||||
@@ -700,3 +745,28 @@ class _ReviewQuests extends SampleQuestRepository {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 일시 정지한 Music Shake 1건(43/60)이 있는 표본.
|
||||
class _PausedRepo extends SampleQuestRepository {
|
||||
const _PausedRepo();
|
||||
|
||||
static const _card = QuestCard(
|
||||
id: 's1', title: '응원 흔들기', categoryName: 'Music Shake', type: QuestType.musicShake,
|
||||
author: QuestAuthor.admin, stage: QuestStage.paused, goal: 120, progress: 0,
|
||||
rewardPop: 3, participants: 0,
|
||||
session: QuestSession(state: QuestStage.paused, progress: 43, total: 60),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<List<QuestCard>> load({int limit = 10}) async => const [_card];
|
||||
|
||||
@override
|
||||
Future<List<QuestCard>> more({required int offset, int limit = 10}) async => const [];
|
||||
|
||||
@override
|
||||
Future<QuestDetail> detail(String questId) async =>
|
||||
const QuestDetail(card: _card, imageLabel: '', description: '');
|
||||
|
||||
@override
|
||||
Future<QuestCard?> pausedQuest() async => _card;
|
||||
}
|
||||
|
||||
@@ -124,4 +124,51 @@ void main() {
|
||||
expect(shared!.text, contains('PERFECT 5 · GOOD 2 · MISS 1 · MAX COMBO 4'));
|
||||
expect(shared!.text, contains('#FANUZU'));
|
||||
});
|
||||
|
||||
testWidgets('중간에 나가면 null · 넘긴 판정표에 판정과 영상 시각이 남고, 이어하면 그 판정을 잇는다(C-83)', (tester) async {
|
||||
final shakes = StreamController<DateTime>();
|
||||
addTearDown(shakes.close);
|
||||
late ValueChanged<ShakePlayerEvent> player;
|
||||
// 이어하기 — 박 0 은 멈추기 전에 perfect 였다.
|
||||
final judge = ShakeJudge(bpm: 60, segments: 3, hits: const [0, -1, -1], position: const Duration(seconds: 1));
|
||||
Object? result = 'unset';
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: buildAppTheme(),
|
||||
home: Builder(
|
||||
builder: (context) => TextButton(
|
||||
onPressed: () async => result = await Navigator.of(context).push<ShakeJudge>(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MusicShakePlayScreen(
|
||||
setup: setup,
|
||||
shakes: shakes.stream,
|
||||
judge: judge,
|
||||
player: (onEvent) {
|
||||
player = onEvent;
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Text('go'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.text('go'));
|
||||
await tester.pumpAndSettle();
|
||||
player((playing: true, position: const Duration(milliseconds: 1000), ended: false));
|
||||
shakes.add(DateTime.now());
|
||||
await tester.pump(const Duration(milliseconds: 60));
|
||||
player((playing: true, position: const Duration(milliseconds: 1600), ended: false));
|
||||
await tester.pump(const Duration(milliseconds: 60));
|
||||
expect(find.text('2 COMBO'), findsOneWidget);
|
||||
|
||||
await tester.pageBack();
|
||||
await tester.pumpAndSettle();
|
||||
expect(result, isNull);
|
||||
expect(judge.position, const Duration(milliseconds: 1600));
|
||||
expect(judge.hits.take(2).every((h) => h >= 0), isTrue);
|
||||
expect(judge.live(judge.position).judged, 2);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user