import 'package:fancake/features/quest/quest_evidence.dart'; import 'package:flutter_test/flutter_test.dart'; Duration ms(int v) => Duration(milliseconds: v); void main() { test('Music Shake 실시간 판정 — 영상 시각 기준 · 창이 닫힌 박만 센다 · 콤보', () { // 60 BPM = 박 1초 · 창 perfect ±0.2 · good ±0.5 박(임시 운영값). final j = ShakeJudge(bpm: 60, segments: 4) ..add(ms(100)) // 박 0 · perfect ..add(ms(1400)) // 박 1 · good ..add(ms(3050)); // 박 3 · perfect, 박 2 miss expect(j.live(ms(400)).judged, 0); final mid = j.live(ms(1600)); expect( (mid.judged, mid.perfect, mid.good, mid.combo, mid.last), (2, 1, 1, 2, ShakeHit.good), ); final after2 = j.live(ms(2600)); expect((after2.miss, after2.combo, after2.last), (1, 0, ShakeHit.miss)); expect(j.end, ms(3500)); expect(j.judgement(), {'perfect': 2, 'good': 1, 'miss': 1, 'maxCombo': 2}); }); test('흔들기는 가장 가까운 박 하나에만 붙고 · 오지 않은 박은 miss 다', () { final j = ShakeJudge(bpm: 60, segments: 3) ..add(ms(500)) ..add(ms(600)); // 0.5 는 박 1 쪽으로 반올림(창 경계 0.5 포함) · 0.6 도 박 1 — 박 1 good 하나. expect(j.judgement(), {'perfect': 0, 'good': 1, 'miss': 2, 'maxCombo': 1}); }); test('판정 창은 운영값 config/policy.musicShake 가 덮는다', () { final w = ShakeWindows.from({ 'musicShake': {'perfectWindow': 0.1, 'goodWindow': 0.3}, }); expect((w.perfect, w.good), (0.1, 0.3)); final d = ShakeWindows.from(const {}); expect((d.perfect, d.good), (0.2, 0.5)); }); test('YouTube 영상 ID', () { expect( youtubeVideoId('https://www.youtube.com/watch?v=WMweEpGlu_U'), 'WMweEpGlu_U', ); expect(youtubeVideoId('https://youtu.be/WMweEpGlu_U?si=x'), 'WMweEpGlu_U'); expect( youtubeVideoId('https://music.youtube.com/watch?v=WMweEpGlu_U'), 'WMweEpGlu_U', ); expect( youtubeVideoId('https://youtube.com/shorts/WMweEpGlu_U'), 'WMweEpGlu_U', ); expect(youtubeVideoId('https://vimeo.com/123'), isNull); expect(youtubeVideoId('https://www.youtube.com/watch?v=short'), isNull); }); }