Files
fanuzu/app/test/uxfx_round2_test.dart
T

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('evolution', '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());
});
}