261 lines
9.5 KiB
Dart
261 lines
9.5 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:share_plus/share_plus.dart';
|
|
|
|
import 'package:fancake/core/theme/app_theme.dart';
|
|
import 'package:fancake/features/quest/music_shake_play_screen.dart';
|
|
import 'package:fancake/features/quest/quest_data.dart';
|
|
import 'package:fancake/features/quest/quest_evidence.dart';
|
|
import 'package:fancake/features/quest/quest_reward_screen.dart';
|
|
|
|
/// Music Shake 진행 화면 — 영상 시각으로 판정하고 Perfect · Good · Miss · Combo 를 띄운다(PRD-04).
|
|
void main() {
|
|
const setup = MusicShakeSetup(
|
|
videoId: 'WMweEpGlu_U',
|
|
title: 'BTS - Butter',
|
|
bpm: 60,
|
|
segments: 3,
|
|
);
|
|
|
|
test('첫 박 · 판 길이가 바뀐 뒤의 옛 일시 정지는 이어하지 않는다 (10/6 대표 지적)', () {
|
|
const fresh = MusicShakeSetup(
|
|
videoId: 'v', title: 't', bpm: 114, segments: 368,
|
|
firstBeat: Duration(milliseconds: 24276),
|
|
);
|
|
// 첫 박 도입 전(32박, 첫 박 0)에 저장된 판.
|
|
const old = QuestSession(state: QuestStage.paused, progress: 19, total: 32);
|
|
expect(old.fitsShake(fresh), isFalse);
|
|
const same = QuestSession(
|
|
state: QuestStage.paused, progress: 19, total: 368,
|
|
firstBeat: Duration(milliseconds: 24276),
|
|
);
|
|
expect(same.fitsShake(fresh), isTrue);
|
|
});
|
|
|
|
test('새 판은 첫 박 leadIn 전부터 틀고, 첫 박이 더 이르면 0 이다 (10/6 B안)', () {
|
|
const s = MusicShakeSetup(
|
|
videoId: 'v', title: 't', bpm: 114, segments: 368,
|
|
firstBeat: Duration(milliseconds: 24276),
|
|
);
|
|
expect(s.startAt, const Duration(milliseconds: 21276));
|
|
expect(
|
|
const MusicShakeSetup(
|
|
videoId: 'v', title: 't', bpm: 60, segments: 3,
|
|
firstBeat: Duration(seconds: 2),
|
|
).startAt,
|
|
Duration.zero,
|
|
);
|
|
});
|
|
|
|
testWidgets('재생 중 흔들기만 영상 시각으로 판정 · 끝나면 판정표를 돌려준다', (tester) async {
|
|
final shakes = StreamController<DateTime>();
|
|
addTearDown(shakes.close);
|
|
late ValueChanged<ShakePlayerEvent> player;
|
|
ShakeJudge? result;
|
|
|
|
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,
|
|
countdown: Duration.zero,
|
|
shakes: shakes.stream,
|
|
player: (onEvent) {
|
|
player = onEvent;
|
|
return const SizedBox();
|
|
},
|
|
),
|
|
),
|
|
),
|
|
child: const Text('go'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.tap(find.text('go'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('BTS - Butter'), findsOneWidget);
|
|
|
|
// 재생 전 흔들기는 세지 않는다.
|
|
shakes.add(DateTime.now());
|
|
await tester.pump();
|
|
Future<void> at(int ms, {bool shake = false, bool playing = true}) async {
|
|
player((
|
|
playing: playing,
|
|
position: Duration(milliseconds: ms),
|
|
ended: false,
|
|
));
|
|
if (shake) shakes.add(DateTime.now());
|
|
await tester.pump(const Duration(milliseconds: 60));
|
|
}
|
|
|
|
await at(0, shake: true); // 박 0 perfect
|
|
await at(1000, shake: true); // 박 1 perfect
|
|
await at(1600);
|
|
expect(find.text('PERFECT'), findsOneWidget);
|
|
expect(find.text('2 COMBO'), findsOneWidget);
|
|
|
|
await at(3000); // 마지막 박(2) 창이 닫힘 → 종료
|
|
await tester.pumpAndSettle();
|
|
expect(result, isNotNull);
|
|
expect(result!.judgement(), {
|
|
'perfect': 2,
|
|
'good': 0,
|
|
'miss': 1,
|
|
'maxCombo': 2,
|
|
});
|
|
});
|
|
|
|
testWidgets('결과 화면 — 판정 합계와 결과 카드 SNS 공유(이미지 + 문구)', (tester) async {
|
|
ShareParams? shared;
|
|
const card = QuestCard(
|
|
id: 'shake',
|
|
title: '응원 흔들기',
|
|
categoryName: 'Music Shake',
|
|
type: QuestType.musicShake,
|
|
author: QuestAuthor.admin,
|
|
stage: QuestStage.completed,
|
|
goal: 120,
|
|
progress: 0,
|
|
rewardPop: 3,
|
|
participants: 0,
|
|
);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: buildAppTheme(),
|
|
home: QuestRewardScreen(
|
|
detail: const QuestDetail(
|
|
card: card,
|
|
imageLabel: 'Music Shake',
|
|
description: '',
|
|
),
|
|
reward: const QuestReward(rewardedPop: 3, balanceAfter: 10),
|
|
shake: const {'perfect': 5, 'good': 2, 'miss': 1, 'maxCombo': 4},
|
|
share: (p) async => shared = p,
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
expect(find.text('PERFECT'), findsOneWidget);
|
|
expect(find.text('MAX COMBO'), findsOneWidget);
|
|
expect(find.text('5'), findsOneWidget);
|
|
await tester.runAsync(() async {
|
|
await tester.tap(find.byType(OutlinedButton));
|
|
await Future<void>.delayed(const Duration(milliseconds: 300));
|
|
});
|
|
await tester.pump();
|
|
expect(shared, isNotNull);
|
|
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,
|
|
countdown: Duration.zero,
|
|
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);
|
|
});
|
|
|
|
test('이어하기면 플레이어가 준비되자마자 저장 위치로 옮겨 튼다 — 처음 시작은 사용자가 누른다 (10/6 A-2)', () {
|
|
final resume = YoutubeEmbed.html(
|
|
'vid',
|
|
startSeconds: 9,
|
|
resumeAt: const Duration(milliseconds: 9910),
|
|
);
|
|
expect(resume, contains('player.seekTo(9.91,true);player.playVideo();'));
|
|
expect(YoutubeEmbed.html('vid'), isNot(contains('playVideo')));
|
|
});
|
|
|
|
testWidgets('재생을 시작하면 3·2·1 을 세는 동안 판정하지 않고, 0 이 되면 판정이 시작된다', (tester) async {
|
|
final shakes = StreamController<DateTime>();
|
|
addTearDown(shakes.close);
|
|
late ValueChanged<ShakePlayerEvent> player;
|
|
final judge = ShakeJudge(bpm: 60, segments: 3);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: buildAppTheme(),
|
|
home: MusicShakePlayScreen(
|
|
setup: setup,
|
|
countdown: const Duration(milliseconds: 500), // 화면은 단말 시계로 센다
|
|
shakes: shakes.stream,
|
|
judge: judge,
|
|
player: (onEvent) {
|
|
player = onEvent;
|
|
return const SizedBox();
|
|
},
|
|
),
|
|
),
|
|
);
|
|
player((playing: true, position: Duration.zero, ended: false));
|
|
await tester.pump(const Duration(milliseconds: 60));
|
|
expect(find.text('1'), findsOneWidget);
|
|
shakes.add(DateTime.now()); // 세는 동안의 흔들기는 세지 않는다.
|
|
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 700)));
|
|
await tester.pump(const Duration(milliseconds: 60));
|
|
await tester.pump(const Duration(milliseconds: 60));
|
|
expect(find.text('1'), findsNothing);
|
|
player((playing: true, position: Duration.zero, ended: false));
|
|
shakes.add(DateTime.now());
|
|
await tester.pump(const Duration(milliseconds: 60));
|
|
expect(judge.hits[0], inInclusiveRange(0, 0.01));
|
|
await tester.pumpWidget(const SizedBox());
|
|
});
|
|
|
|
test('박은 firstBeat 부터 — 인트로의 흔들기는 박이 아니다', () {
|
|
final j = ShakeJudge(bpm: 60, segments: 2, firstBeat: const Duration(seconds: 17))
|
|
..add(const Duration(seconds: 1))
|
|
..add(const Duration(seconds: 17))
|
|
..add(const Duration(milliseconds: 18300));
|
|
expect(j.end, const Duration(milliseconds: 18500));
|
|
expect(j.judgement(), {'perfect': 1, 'good': 1, 'miss': 0, 'maxCombo': 2});
|
|
});
|
|
}
|