Files
fanuzu/app/test/quest_react_push_test.dart
T

341 lines
12 KiB
Dart

import 'package:fancake/core/push/push_messaging.dart';
import 'package:fancake/core/theme/tokens.dart';
import 'package:fancake/core/ui/fx_intensity.dart';
import 'package:fancake/features/eclipse/widgets/live_proof.dart';
import 'package:fancake/features/galaxy/galaxy_data.dart';
import 'package:fancake/features/galaxy/widgets/zap_feedback.dart';
import 'package:fancake/features/galaxy/zap_controller.dart';
import 'package:fancake/features/notification/notification_screen.dart';
import 'package:fancake/features/quest/quest_data.dart';
import 'package:fancake/features/quest/quest_reward_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'eclipse_stubs.dart';
/// UX·FX v1.5 2차 약속분(10/8) — 진척현황 부록 B-3 1-2 · 4-2 · 4-3(퀘스트 완료 → 성공 햅틱 · 행성 반응),
/// 1-4 · 8-6(Push ↔ 라이브 띠 중복 제어 · 알림 위계 · Push 탭 실제 화면 이동), 원장 C-78 단계 배정.
void main() {
group('C-78 단계 배정 — Push · 알림함 같은 표', () {
test('위기 · 공동행동 · 성취 · 결과 · 그 밖', () {
expect(pushTierOf('eclipse'), PushTier.crisis);
expect(pushTierOf('eclipse', phase: 'start'), PushTier.crisis);
expect(pushTierOf('eclipse', phase: 'defended'), PushTier.result);
expect(pushTierOf('timeattack'), PushTier.collective);
expect(pushTierOf('evolution'), PushTier.achievement);
expect(
pushTierOf('item_used'),
PushTier.achievement,
reason: 'HERO TIME 발동 = 성취',
);
expect(pushTierOf('quest'), PushTier.result);
expect(pushTierOf('campaign'), PushTier.result);
expect(pushTierOf('briefing'), PushTier.other);
expect(pushTierOf(''), PushTier.other);
});
test('알림함 항목도 같은 단계 — 표본(eventType 없음)은 종류로 읽는다', () {
AppNotification n(
NotificationKind kind, {
String? event,
String phase = '',
}) => AppNotification(
id: 'x',
kind: kind,
title: 't',
at: DateTime(2026),
eventType: event,
phase: phase,
);
expect(n(NotificationKind.itemUsed).tier, PushTier.achievement);
expect(
n(NotificationKind.eclipse, event: 'eclipse', phase: 'defended').tier,
PushTier.result,
);
// 승계 알림처럼 eventType 이 빈 문서는 아이템 사용으로 떨어지지만 위계는 올리지 않는다.
expect(n(NotificationKind.itemUsed, event: '').tier, PushTier.other);
});
});
group('Push 탭 → 실제 화면 (v1.5 8장 Deep Link · 9장)', () {
PushNotice n(
String kind, {
String? fandomId,
String phase = '',
String? targetPath,
String? questId,
String? campaignId,
}) => PushNotice(
title: 't',
body: 'b',
kind: kind,
fandomId: fandomId,
phase: phase,
targetPath: targetPath,
questId: questId,
campaignId: campaignId,
);
test('종류마다 해당 화면', () {
expect(n('quest', targetPath: 'quests/q1').target, 'quests/q1');
expect(n('campaign', campaignId: 'c1').target, 'campaigns/c1');
expect(
n('timeattack', questId: 'q2', fandomId: 'bts').target,
'quests/q2',
);
expect(n('eclipse', fandomId: 'bts').target, 'eclipse/bts');
expect(
n('eclipse', fandomId: 'bts', phase: 'defended').target,
'planets/bts',
);
expect(n('evolution', fandomId: 'bts').target, 'planets/bts');
expect(n('item_used', fandomId: 'bts').target, 'planets/bts');
expect(
n('briefing', fandomId: 'bts').target,
isNull,
reason: 'HOME 브리핑 블록이 그 화면',
);
expect(n('eclipse').target, isNull, reason: '행성을 모르면 탭만');
});
test('같은 사건은 문구가 달라도 한 번 — 진화 토픽 Push + 참여자 알림함 Push', () {
final topic = PushNotice(
title: '진화',
body: 'a',
kind: 'evolution',
fandomId: 'bts',
);
const inbox = PushNotice(
title: 'FANUZU',
body: 'b',
kind: 'evolution',
fandomId: 'bts',
);
expect(shouldShowInApp(inbox, topic), isFalse);
expect(
shouldShowInApp(
const PushNotice(
title: 'FANUZU',
body: 'b',
kind: 'evolution',
fandomId: 'twice',
),
topic,
),
isTrue,
);
});
test('라이브 띠가 이미 알리는 사건은 띄우지 않는다 (v1.5 §1.1 Push ↔ 인앱 티커)', () {
final ticker = liveTickerKey('eclipse', 'bts');
expect(
shouldShowInApp(
n('eclipse', fandomId: 'bts', phase: 'start'),
null,
ticker: ticker,
),
isFalse,
);
expect(
shouldShowInApp(n('eclipse', fandomId: 'twice'), null, ticker: ticker),
isTrue,
reason: '다른 행성',
);
expect(
shouldShowInApp(
n('eclipse', fandomId: 'bts', phase: 'defended'),
null,
ticker: ticker,
),
isTrue,
reason: '방어 성공은 띠에 없다',
);
expect(
shouldShowInApp(
n('item_used', fandomId: 'bts'),
null,
ticker: liveTickerKey('item_used', 'bts'),
),
isFalse,
reason: 'HERO TIME 전광판',
);
});
test('띠 한 줄은 같은 사건의 Push 종류를 들고 있다', () {
final start = DateTime.now().subtract(const Duration(seconds: 5));
TickerLine? line({eclipse, bool hero = false}) => tickerLine(
eclipse: eclipse,
heroTime: hero,
heroNicknames: const [],
events: const [],
fandomName: 'BTS',
now: DateTime.now(),
fallback: 'f',
);
expect(line(eclipse: eclipseFixture(startAt: start))!.push, 'eclipse');
expect(line(hero: true)!.push, 'item_used');
expect(line()!.push, isNull);
});
});
testWidgets('알림함 위계 — 위기 · 공동행동 · 성취만 배지 획 색을 올린다 (v1.5 8장)', (tester) async {
final now = DateTime.now();
AppNotification item(
String id,
NotificationKind kind,
String event, {
String phase = '',
}) => AppNotification(
id: id,
kind: kind,
title: id,
at: now,
eventType: event,
phase: phase,
);
await tester.pumpWidget(
MaterialApp(
home: NotificationScreen(
repository: SampleNotificationRepository(
items: [
item(
'crisis',
NotificationKind.eclipse,
'eclipse',
phase: 'start',
),
item('achieve', NotificationKind.itemUsed, 'item_used'),
item('result', NotificationKind.quest, 'quest'),
],
),
),
),
);
await tester.pump();
Color ring(String title) {
final row = find
.ancestor(of: find.text(title), matching: find.byType(Row))
.last;
final badge = tester.widget<Container>(
find.descendant(of: row, matching: find.byType(Container)).first,
);
return ((badge.decoration! as BoxDecoration).border! as Border).top.color;
}
expect(ring('crisis'), AppColors.neonMagenta);
expect(ring('achieve'), AppColors.gold);
expect(ring('result'), const Color(0xFF333333), reason: 'Figma 기본 획');
});
group('퀘스트 완료 → 성공 햅틱 · 행성 반응 (v1.5 4장)', () {
test('Music/Location 은 Medium, 그 밖은 Small (v1.5 §1)', () {
expect(questSuccessFx(QuestType.musicShake), FxIntensity.medium);
expect(questSuccessFx(QuestType.place), FxIntensity.medium);
expect(questSuccessFx(QuestType.steps), FxIntensity.small);
expect(questSuccessFx(QuestType.attend), FxIntensity.small);
});
testWidgets('보상 화면 「확인」이 HOME 행성 반응을 올린다', (tester) async {
const card = QuestCard(
id: 'a',
title: 't',
categoryName: 'c',
author: QuestAuthor.admin,
stage: QuestStage.completed,
goal: 1,
progress: 1,
rewardPop: 20,
participants: 0,
);
tester.view.physicalSize = const Size(1080, 2600);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(
const MaterialApp(
home: QuestRewardScreen(
detail: QuestDetail(card: card, imageLabel: '', description: ''),
reward: QuestReward(rewardedPop: 20, balanceAfter: 0),
),
),
);
final before = questPlanetReaction.value;
await tester.tap(find.byType(FilledButton));
await tester.pump();
expect(questPlanetReaction.value, before + 1);
});
testWidgets('행성 반응 — 에너지 이동 뒤 흡수 · 퀘스트 반영 문구, 잔액은 그대로', (tester) async {
final zap = ZapController(SampleGalaxyRepository(), onFailure: (_) {});
addTearDown(zap.dispose);
zap.balance = 100;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ListenableBuilder(
listenable: zap,
builder: (_, _) => ZapFeedback(
fandomName: 'BTS',
quest: zap.questReaction,
touchSerial: zap.touchSerial,
pending: zap.pending > 0 || zap.sending,
result: zap.result,
source: Offset.zero,
target: const Offset(100, 100),
radius: 40,
),
),
),
),
);
final reacting = zap.react();
await tester.pump();
expect(zap.touchSerial, 1, reason: '먼저 에너지 이동');
expect(zap.result, isNull, reason: '흡수는 이동 뒤');
await tester.pump(const Duration(milliseconds: 400));
await reacting;
await tester.pump();
expect(find.text('퀘스트 완료가 BTS 성장에 반영되었습니다'), findsOneWidget);
expect(zap.balance, 100);
await tester.pump(const Duration(seconds: 2));
});
testWidgets('HOME 반영 문구는 행성 상자 위쪽 — 아래 성장 카드(행성 이름 · 말풍선)를 덮지 않는다(10/8 r11)', (tester) async {
final zap = ZapController(SampleGalaxyRepository(), onFailure: (_) {});
addTearDown(zap.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ListenableBuilder(
listenable: zap,
builder: (_, _) => ZapFeedback(
fandomName: 'BTS',
quest: zap.questReaction,
touchSerial: zap.touchSerial,
pending: zap.pending > 0 || zap.sending,
result: zap.result,
source: Offset.zero,
target: const Offset(200, 400),
radius: 80,
messageAboveTarget: true,
),
),
),
),
);
final reacting = zap.react();
await tester.pump(const Duration(milliseconds: 400));
await reacting;
await tester.pump();
final text = find.text('퀘스트 완료가 BTS 성장에 반영되었습니다');
expect(text, findsOneWidget);
// 행성 상자(반 변 80 / 0.4 / 2 = 100) 윗변 y300 에서 시작해 행성 중심(y400) 위에서 끝난다.
final box = tester.getRect(
find.ancestor(of: text, matching: find.byType(DecoratedBox)).first,
);
expect(box.top, 300);
expect(box.bottom, lessThan(400));
await tester.pump(const Duration(seconds: 2));
});
});
}