퀘스트 완료 햅틱 · HOME 행성 반응 · Push·라이브 띠 중복 제어 · 알림함 위계 · Push 탭 화면 이동
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
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));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user