264 lines
9.6 KiB
Dart
264 lines
9.6 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:fancake/core/data/repositories.dart' show eclipseLook;
|
|
import 'package:fancake/core/push/push_messaging.dart';
|
|
import 'package:fancake/core/theme/app_theme.dart';
|
|
import 'package:fancake/core/ui/fx_intensity.dart';
|
|
import 'package:fancake/core/ui/planet_25d.dart';
|
|
import 'package:fancake/features/campaign/campaign_detail_screen.dart'
|
|
show campaignJustCompleted;
|
|
import 'package:fancake/features/eclipse/eclipse_data.dart';
|
|
import 'package:fancake/features/eclipse/eclipse_defend_screen.dart';
|
|
import 'package:fancake/features/eclipse/widgets/live_proof.dart';
|
|
import 'package:fancake/features/home/widgets/live_ticker.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'eclipse_stubs.dart';
|
|
|
|
/// UX·FX 정렬 기준 v1.5 2차(10/8) — 햅틱 5단계 · 티커 집약 · Push 위계 · Shield Pulse ·
|
|
/// Orbit Burst · 실패 Glow 감소 · 전광판 HERO (진척현황 부록 B-3 1-1 · 1-3 · 1-4 · 5-3 · 5-6 · 5-7 · 6-2).
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
test(
|
|
'강도 5단계 햅틱 — Micro·Small Light · Medium Medium · Large·Legendary Strong (v1.5 §1)',
|
|
() async {
|
|
final calls = <Object?>[];
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockMethodCallHandler(SystemChannels.platform, (call) async {
|
|
if (call.method == 'HapticFeedback.vibrate')
|
|
calls.add(call.arguments);
|
|
return null;
|
|
});
|
|
addTearDown(
|
|
() => TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockMethodCallHandler(SystemChannels.platform, null),
|
|
);
|
|
for (final level in FxIntensity.values) {
|
|
await fxHaptic(level);
|
|
}
|
|
expect(calls, [
|
|
'HapticFeedbackType.lightImpact',
|
|
'HapticFeedbackType.lightImpact',
|
|
'HapticFeedbackType.mediumImpact',
|
|
'HapticFeedbackType.heavyImpact',
|
|
'HapticFeedbackType.heavyImpact',
|
|
]);
|
|
},
|
|
);
|
|
|
|
test('Campaign 100% Legendary 는 이번 후원으로 목표를 넘긴 때만', () {
|
|
expect(campaignJustCompleted(900, 1000, 1000), isTrue);
|
|
expect(campaignJustCompleted(900, 999, 1000), isFalse);
|
|
expect(
|
|
campaignJustCompleted(1000, 1200, 1000),
|
|
isFalse,
|
|
reason: '이미 달성한 캠페인',
|
|
);
|
|
});
|
|
|
|
group('앱 사용 중 Push — C-78 우선순위 · 같은 알림 중복 금지 (v1.5 §1.1 · 9장)', () {
|
|
PushNotice n(String kind, [String title = 't']) =>
|
|
PushNotice(title: title, body: 'b', kind: kind, fandomId: 'bts');
|
|
|
|
test('떠 있는 것이 없으면 띄운다', () {
|
|
expect(shouldShowInApp(n('briefing'), null), isTrue);
|
|
});
|
|
test('같은 알림은 다시 띄우지 않는다', () {
|
|
expect(shouldShowInApp(n('eclipse'), n('eclipse')), isFalse);
|
|
});
|
|
test('낮은 순위는 높은 순위를 덮지 않고, 높은 순위는 덮는다', () {
|
|
expect(shouldShowInApp(n('briefing'), n('eclipse')), isFalse);
|
|
expect(shouldShowInApp(n('evolution'), n('timeattack')), isFalse);
|
|
expect(shouldShowInApp(n('eclipse'), n('briefing')), isTrue);
|
|
expect(shouldShowInApp(n('evolution', 'x'), n('quest', 'y')), isTrue);
|
|
});
|
|
test('위기 → 공동행동 → 성취 순', () {
|
|
expect(n('eclipse').tier, PushTier.crisis);
|
|
expect(n('timeattack').tier, PushTier.collective);
|
|
expect(n('evolution').tier, PushTier.achievement);
|
|
expect(n('briefing').tier, PushTier.other);
|
|
});
|
|
});
|
|
|
|
group('라이브 띠 — 위기 우선 · 방어 집약 · HERO 전광판', () {
|
|
final start = DateTime.now().subtract(const Duration(seconds: 5));
|
|
final join = LiveEvent(id: 'e', nickname: '아미', at: DateTime.now());
|
|
TickerLine? line({
|
|
EclipseState? eclipse,
|
|
bool hero = false,
|
|
List<String> names = const [],
|
|
List<LiveEvent> events = const [],
|
|
}) => tickerLine(
|
|
eclipse: eclipse,
|
|
heroTime: hero,
|
|
heroNicknames: names,
|
|
events: events,
|
|
fandomName: 'BTS',
|
|
now: DateTime.now(),
|
|
fallback: 'fallback',
|
|
);
|
|
|
|
test('발생 직후는 발생 소식', () {
|
|
expect(
|
|
line(eclipse: eclipseFixture(startAt: start))!.text,
|
|
'BTS 행성에 이클립스가 발생했어요!',
|
|
);
|
|
});
|
|
test('방어 참여가 오면 고유 참여자 2명 이상부터 집약한다', () {
|
|
expect(
|
|
line(
|
|
eclipse: eclipseFixture(startAt: start, defense: 3),
|
|
events: [join],
|
|
)!.text,
|
|
'[LIVE] 지금 3명의 팬이 함께 방어 중',
|
|
);
|
|
expect(
|
|
line(
|
|
eclipse: eclipseFixture(startAt: start, defense: 1),
|
|
events: [join],
|
|
)!.text,
|
|
'[LIVE] 아미님이 이클립스 방어에 합류했습니다!',
|
|
);
|
|
});
|
|
test('HERO TIME 은 공개 닉네임 + 팬덤으로 오르고 참여 진입이 된다', () {
|
|
final hero = line(hero: true, names: ['아미'], events: [join])!;
|
|
expect(hero.text, '[LIVE] BTS 행성 HERO TIME · by 아미');
|
|
expect(hero.hero, isTrue);
|
|
expect(line(hero: true)!.text, '[LIVE] BTS 행성 HERO TIME 진행 중');
|
|
});
|
|
test('위기(이클립스)가 성취(HERO)보다 먼저', () {
|
|
final l = line(eclipse: eclipseFixture(startAt: start), hero: true)!;
|
|
expect(l.hero, isFalse);
|
|
});
|
|
test('아무것도 없으면 폴백', () {
|
|
expect(line()!.text, 'fallback');
|
|
});
|
|
});
|
|
|
|
testWidgets('HERO 전광판은 눌러서 참여로 간다', (tester) async {
|
|
var tapped = 0;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: LiveTicker(text: 'HERO', action: 'GO', onTap: () => tapped++),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.byType(LiveTicker));
|
|
expect(tapped, 1);
|
|
});
|
|
|
|
test('이클립스 회차 → 행성 표현 (진행 · 실패 · 성공 · 판정 대기)', () {
|
|
final now = DateTime.now();
|
|
final past = now.subtract(const Duration(seconds: 1));
|
|
expect(eclipseLook(null, now), (false, false, null));
|
|
expect(eclipseLook(eclipseFixture(), now), (true, false, null));
|
|
expect(
|
|
eclipseLook(
|
|
eclipseFixture(endsAt: past, result: EclipseResult.failure),
|
|
now,
|
|
),
|
|
(false, true, null),
|
|
);
|
|
expect(
|
|
eclipseLook(
|
|
eclipseFixture(endsAt: past, result: EclipseResult.success),
|
|
now,
|
|
),
|
|
(false, false, 'ecl-1'),
|
|
);
|
|
expect(eclipseLook(eclipseFixture(endsAt: past), now), (
|
|
false,
|
|
false,
|
|
null,
|
|
), reason: '판정 대기를 실패처럼 보이지 않는다');
|
|
expect(
|
|
eclipseLook(
|
|
eclipseFixture(endsAt: past, result: EclipseResult.cancelled),
|
|
now,
|
|
),
|
|
(false, false, null),
|
|
);
|
|
});
|
|
|
|
group('행성 — 실패 감소 · 성공 Orbit Burst (v1.5 §5)', () {
|
|
Widget planet({bool failed = false, String? success}) => MaterialApp(
|
|
home: Center(
|
|
child: Planet25D(
|
|
image: 'assets/figma/planet-home.png',
|
|
size: 200,
|
|
stage: 3,
|
|
eclipseFailed: failed,
|
|
eclipseSuccessId: success,
|
|
),
|
|
),
|
|
);
|
|
double light(WidgetTester tester) => tester
|
|
.widget<Opacity>(find.byKey(const ValueKey('planet-surface-light')))
|
|
.opacity;
|
|
|
|
testWidgets('실패 확정이면 밝기를 줄인다(파괴 표현 없음)', (tester) async {
|
|
await tester.pumpWidget(planet(failed: true));
|
|
expect(light(tester), 0.5);
|
|
await tester.pumpWidget(planet());
|
|
await tester.pumpAndSettle();
|
|
expect(light(tester), 1);
|
|
});
|
|
|
|
testWidgets('성공 회차가 새로 오면 복원 뒤 Orbit Burst 한 번 · 첫 값은 기준점', (tester) async {
|
|
Planet25D.animationsEnabled = true;
|
|
addTearDown(() => Planet25D.animationsEnabled = false);
|
|
await tester.pumpWidget(planet(success: 'old'));
|
|
await tester.pump();
|
|
expect(find.byKey(const ValueKey('eclipse-orbit-burst')), findsNothing);
|
|
await tester.pumpWidget(planet(success: 'ecl-2'));
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
expect(find.byKey(const ValueKey('eclipse-orbit-burst')), findsOneWidget);
|
|
await tester.pumpWidget(const SizedBox());
|
|
});
|
|
});
|
|
|
|
testWidgets('유효 흔들기마다 Shield Pulse (v1.5 §5 수동 방어)', (tester) async {
|
|
tester.view.physicalSize = const Size(1080, 2400);
|
|
tester.view.devicePixelRatio = 2;
|
|
addTearDown(tester.view.reset);
|
|
final shakes = StreamController<DateTime>.broadcast();
|
|
addTearDown(shakes.close);
|
|
final repo = FakeEclipseRepository(
|
|
state: eclipseFixture(
|
|
startAt: DateTime.now().subtract(const Duration(seconds: 1)),
|
|
endsAt: DateTime.now().add(const Duration(seconds: 59)),
|
|
),
|
|
);
|
|
addTearDown(repo.dispose);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: buildAppTheme(),
|
|
home: EclipseDefendScreen(
|
|
repository: repo,
|
|
fandomId: 'bts',
|
|
shakes: () => shakes.stream,
|
|
),
|
|
),
|
|
);
|
|
for (var i = 0; i < 4; i++) {
|
|
await tester.pump(const Duration(milliseconds: 120));
|
|
}
|
|
expect(find.byKey(const ValueKey('shield-pulse-1')), findsNothing);
|
|
shakes.add(DateTime.now());
|
|
await tester.pump(const Duration(milliseconds: 50));
|
|
expect(find.byKey(const ValueKey('shield-pulse-1')), findsOneWidget);
|
|
shakes.add(DateTime.now());
|
|
await tester.pump(const Duration(milliseconds: 50));
|
|
expect(find.byKey(const ValueKey('shield-pulse-2')), findsOneWidget);
|
|
for (var i = 0; i < 10; i++) {
|
|
await tester.pump(const Duration(milliseconds: 120));
|
|
}
|
|
await tester.pumpWidget(const SizedBox());
|
|
});
|
|
}
|